第一章 你真的了解C#吗
1.什么是C#,
微软公司,面向对象,运行于.NET Framework之上,
2.C#能编写哪些应用程序,
Windows应用桌面程序,Web应用程序,Web服务,
3.什么是.NET Framework,
全面的类库,内存管理,通用类型系统,开发结构和技术,语言互操作性,
4..NET Framework组成,
公共语言运行时(CLR)和类库(FCL),
5.什么是CLR,
可以将CLR看成一个在执行时管理代码的代理,它提供了内存管理,线程管理和异常处理等服务,而且还负责对代码实施严格的类型安全检查,保证了代码的正确性,将受CLR管理的代码称为托管代码,
6.CLR由哪几部分组成,
通用类型系统(CTS)和公共语言规范(CLS),
7.什么是CTS,
CTS用于解决不同语言之间数据类型不同的问题,CTS类型主要分为两大类,引用类型和值类型,两种类型之间存在着相互转化即装箱和拆箱,
8.什么是CLS,
CLS是一种最低的语言标准,它制定了以.NET平台为目标的语言所必须支持的最小特征,
9..NET Framework类库就是一组DLL程序集的集合,程序集分为两种.exe和.dll,
10.C#与.NET Framework的关系,
C#源代码文件 >> C#编译器 >> 中间语言IL >> JIT编译器 >> 本机代码
第三章 C#语言基础
1.C#标识符必须满足的条件,
只能包含字母,数字,@,_,首位不能为数字,不能为C#关键字,
2.C#关键字,
3.C#基础数据类型,
3.1数值类型,
3.1.1整型,
3.1.2浮点型,
3.1.3十进制型,
3.1.4 布尔类型,true/false,
3.2字符串类型,
3.3枚举类型,
3.4结构体类型,
3.5数组类型,
4.什么是变量和常量,
变量代表一块内存空间,它存储的值是可以变化的,因为有了变量,不需要再去记忆复杂的内存地址,而是转为通过变量名来完成内存数据的存取操作,常量一旦被初始化就不能再次改变,使用const关键字来对常量进行定义,因为后续无法更改,常量必须在声明时就进行初始化,常量无法接受变量的赋值,即使变量是readonly(只读)也不可以,常量默认是静态的,不允许显示使用static关键字来声明,
5.运算符和表达式,
6.C#中的语句,
条件语句(if/switch),循环语句(while/do-while/for/foreach),跳转语句(break/continue/goto/return),
第四章 C#中的类
1.如果class关键字前面没有显示的指定访问修饰符,则类的访问修饰符为internal,表示仅在当前项目内可被访问,
2.类的成员的访问权限,
2.1字段,
字段最好设置为private,这样可以防止客户端直接对字段进行篡改,
2.2属性,
属性是对字段的扩展,属性定义主要由get访问器和set访问器组成,get访问器负责对字段值进行读取,set访问器负责为字段进行赋值,可以理解为两个方法,可读属性,可写属性,Ctrl+R+E,
2.3方法,
public void Test (string s) //返回类型+参数,
{
~~~~~~
}
方法的访问级别,代码中为public,
方法的重载即方法的参数不同,返回类型要相同,
2.4构造函数,
构造函数主要用于创建类的实例对象,当调用构造函数创建一个对象时,构造函数会为对象分配内存空间,并初始化的类的成员,
2.4.1实例构造函数,new,
构造函数可以进行方法重载,C#编译器会自动生成空的默认无参的实例构造函数,可以对实例构造函数指定访问级别,如public,private等,构造函数必须和类同名,构造函数不允许有返回类型,
私有构造函数---------单例模式,
2.4.2静态构造函数
静态构造函数不能使用任何访问修饰符,静态构造函数不能带有任何参数,静态构造函数只会执行一次,不能直接调用静态构造函数,程序员无法控制执行静态构造函数的时机,
2.5析构函数,
析构函数用于在类销毁之前释放类实例所使用的托管和非托管资源,对于C#应用程序所创建的大多数对象,可以依靠.NET Framework的垃圾回收器(GC)来隐式的执行内存管理任务,但若创建封装了非托管资源的对象,在应用程序使用完这些非托管资源后,垃圾回收器将运行对象的析构函数即Finalize方法来释放这些资源,
析构函数不能在结构体中定义,只能对类使用,一个类只能有一个析构函数,无法继承或重载析构函数,无法显示的调用析构函数,析构函数是由垃圾回收器自动调用的,析构函数既没有修饰符也没有参数,
2.6索引器 ,
当一个类包含组成员时,索引器的使用将大大的简化对类中数组成员的访问,索引器定义类似属性,也具有get访问器和set访问器,
数据类型是类中要存取的数组的类型,索引类型表示该索引器使用哪一个类型的索引来存取数组元素,可以是整型也可以是字符串类型,this则表示所操作的是类对象的数组成员,
索引器也是一种针对私有字段进行访问的方式,但此时的私有字段是数组类型,而属性则一般只对简单数据类型的私有字段进行访问,
3.类如何实例化,
静态类中是不能定义实例构造函数的,
4.类与结构体的区别:
语法上,定义类使用关键字class,定义结构体则使用关键字struct,
结构体中不可对声明字段进行初始化,但类可以,
如果没有为类显示的定义构造函数,C#编译器会自动生成一个无参数的实例构造函数,称之为隐式构造函数,而一旦为类显示的定义了一个构造函数,C#编译器就不会在自动生成隐式构造函数了,但在结构体中,无论是否显示定义构造函数,隐式构造函数都是一直存在的,
结构体中不能显示定义无参的构造函数,这也是说明无参构造函数是一直存在的,所以不能在显示的为结构体添加一个无参的构造函数,而类可以,
在结构体的构造函数中,必须要为结构体中的所有字段赋值,
创建结构体对象可以不使用new关键字,但此时结构体对象中的字段是没有初始值的,而类必须使用new关键字来创建对象,
结构体不能继承结构或者类,但可以实现接口,而类可以继承类但不能继承结构,它也可以实现接口,
类是引用类型,而结构体是值类型,
结构体不能定义析构函数,而类可以有析构函数,
不能用abstract和sealed关键字修饰结构体,
第五章 C#中的面向对象编程
1.面向对象,
封装:把客观事物封装成类,并将类内部的实现隐藏,以保证数据的完整性,
继承:通过继承可以复用父类的代码,
多态:允许将子对象赋值给父对象的一种能力,
2.封装,
封装指的是把类内部的数据隐藏起来,不让对象实例直接对其操作,C#中提供了属性机制来对类内部的状态进行操作,封装可以通过public,private。protected和internal等关键字来体现,如一个人的年龄不可能为负数,直接对公有字段进行赋值显然是不合理的,于是将字段声明为private,为其配备public属性,在get和set两个方法中加一些逻辑判断即可,
在C#中,一个类可以继承另外一个已有的类(密封类除外),被继承的类称为基类(或父类),继承的类称为派生类(或子类),子类将获得基类除构造函数和析构函数以外的所有成员,此为,静态类是密封的,也不能被继承,子类还可以添加自己特有的成员,
子类并不能对父类的私有成员进行直接访问,它只可对保护成员和公有成员进行访问,私有成员也会被子类继承,但子类不能直接访问私有成员,子类可以通过调用保护或公有方法来间接的对私有成员进行访问,
密封类,关键字sealed,密封类不可以被另外一个类继承,
子类的初始化顺序:
初始化类的实例字段,
调用基类的构造函数,如果没有指明基类,则调用System.Object的构造函数,
调用子类的构造函数,
3.多态,
由于可以继承基类的所有成员,子类就都有了相同的行为,但有时子类的某些行为需要相互区别,子类需要覆写父类中的方法来实现子类特有的行为,这样的技术在面向对象编程中就是多态,
只有基类成员声明为virtual和abstract时,才能被派生类重写,如果子类想改变虚方法的实现行为,则必须使用override关键字,
如果子类还想继续访问基类定义的方法,可使用关键字base来完成调用,
使用abstract关键字来防止创建抽象类的实例,
阻止派生类重写虚成员也可使用关键字sealed,
如果想在派生类中定义与基类成员同名的成员,则可使用new关键字把基类成员隐藏起来,
如果此时仍然想访问基类的成员,则可使用强制类型转换,把子类强制转换成基类类型,从而访问隐藏的基类成员,
所有类的父类,System.object,
第六章 C#也有接口
1.C#提出了接口的方式,作为替代版的多重继承,
接口,
接口可以理解为对一组方法声明进行的统一命名,但这些方法没有提供任何实现,也就是说,把一组方法声明在一个接口中,然后继承于该接口的类都需要实现这些方法,
通过接口,你可以对方法进行统一管理,避免了在每种类型中重复定义这些方法,
接口使用关键字interface进行定义,
接口中定义方法不能添加任何访问修饰符,因为接口中的方法默认为public,如果显示的指定了修饰符,则会出现编译错误,
接口中除了可以定义方法外,还可以包含属性,事件,索引器,或者类,但接口中不能包含字段,运算符重载,实例构造函数和析构函数,
接口中的所有成员都默认是公共的,因此不能再使用public,private,protected等访问修饰符进行修饰,也不能使用static修饰符,
继承接口,: ICustomCompare
调用接口中的方法,
显示接口,即明确指出实现哪一个接口中的哪一个方法,
当多个接口中包含相同方法名称,相同返回类型和相同参数时,如果一个类同时实现了这些接口,隐式的接口实现就会出现命名冲突的问题,
使用显示接口需注意的问题:
若显示实现接口,方法不能使用任何访问修饰符,显示实现的成员都默认为私有,
显示实现的成员默认是私有的,所以这些成员都不能通过类的对象进行访问,正确的访问方式是把对象显示的转换为对应的接口,再来调用方法,
隐式接口与显示接口的区别及使用选择:
采用隐式接口实现时,类和接口都可以访问接口中的方法,而采用显示接口的实现方法,接口方法只能通过接口来完成访问,因为此时接口方法默认为私有,
当类实现单个接口时,通常使用隐式接口的实现方式,这样类的对象可以直接去访问接口方法,
当类实现了多个接口,并且接口中包含相同的方法名称,参数和返回类型时,则应使用显示接口实现方式,显示接口实现方式可以标识出哪个方法属于哪个接口,
接口与抽象类区别:
抽象类使用abstract关键字,接口使用interface关键字,它们都不能进行实例化,
抽象类中可以包含虚方法,非抽象方法和静态成员,但接口中不能包含虚方法和任何静态成员,并且接口中只能定义方法不能有具体实现,方法的具体实现由实现类完成,
抽象类不能实现多继承,接口则支持多继承,
抽象类是对一类对象的抽象,继承与抽象类的类与抽象类为属于的关系,而类实现接口只是代表实现类具有接口声明的方法,是一种CanDo的关系,所以一般接口后都带有able字段,表示“我能做”的意思,
面向对象编程的应用,
举个例子,animals抽象父类,dog子类,子类继承抽象父类后,dog可以吃可以跑,但specialDog还可以表演杂技,这个属于能力,就用接口来做,
第七章 IL语言
第八章 委托
委托使得C#中的函数可以作为参数来被传递,
委托的例子,
public delegate void MyDelegate(int para1 , string para2);
方法的签名必须与委托一致,方法的签名包括参数的个数,类型和顺序,
方法的返回类型要和委托一致,注意,方法的返回类型不属于方法签名的一部分,
委托的使用步骤,
定义委托类型,声明委托变量,实例化委托,作为参数传递给方法,调用委托,
委托链,
C#中的委托也同样可以封装多个方法,C#中把封装多个方法的委托称作委托链或多路广播委托,
委托链其实就是委托类型,只是委托链把多个委托连接在一起而已,
-=与+=相同,
第九章 事件揭秘
什么是事件,
事件涉及两类角色,事件发布者和事件订阅者,当某个事件发布后,事件发布者会发布消息,事件订阅者会接受到事件已发生的通知,并作出相应处理,其中触发事件的对象称为事件发布者,捕获事件并对其做出处理的对象称为事件订阅者,
如何定义事件,
访问修饰符一般定义为public,因为事件的订阅者需要对事件进行订阅与取消操作,定义为公共类型可使事件对其它类可见,事件定义还包含委托类型,它既可以是自定义的委托,也可以是.NET类库中预定义的委托类型EventHandler,
订阅和取消事件+=和-=,
第十章 深入理解类型
值类型主要包括简单类型,枚举类型,结构体类型等,值类型的实例通常被分配在线程的堆栈上,变量保存的内容就是实例数据本身,
引用类型的实例则被分配在托管堆上,变量保存的是实例数据的内存地址,引用类型主要包括类类型,接口类型,委托类型和字符串类型等,
值类型通常被分配到线程的堆栈上,引用类型则被分配到托管堆上,值类型的管理由操作系统负责,引用类型的管理则由垃圾回收器GC负责,管理主要是指对内存空间进行分配和释放,因为内存大小毕竟是有限的,
如上,值类型与引用类型的区别在于实际数据的存储位置,值类型的变量和实际数据都存储在堆栈中,而引用类型则只有变量存储在堆栈中,变量存储着实际数据的地址,实际数据存储在与地址相对应的托管堆中,
前面叙述值类型实例的存在位置时,用到了“通常”这个词,就是说值类型实例不一定总会分配在线程栈上,在引用类型中嵌套值类型时,或者在值类型装箱的情况下,值类型的实例就会被分配到托管堆中,
引用类型中嵌套定义值类型,
如果类的字段类型是值类型,它将作为引用类型实例的一部分,被分配到托管堆中,但那些作为局部变量的值类型,则仍然会被分配到线程堆栈中,
值类型中嵌套定义引用类型,
值类型嵌套定义引用类型时,堆栈上将保存该引用类型的引用,而实际的数据则依然保存在托管堆中,
结论:值类型实例总会被分配到它声明的地方,声明的是局部变量时,将被分配到栈上,而声明为引用类型成员时,则被分配到托管堆上,,,而引用类型实例总是分配到托管堆上,
值类型与引用类型的区别:
值类型继承自ValueType,ValueType又继承自System.Object,而引用类型则直接继承于System.Object,
值类型的内存不受GC控制,作用域结束后,值类型会被操作系统自行释放,从而减少了托管堆的压力,而引用类型的内存管理则由GC来完成,所以与引用类型相比,值类型在性能方面更具优势,
若值类型是密封的(sealed),你将不能把值类型作为其它任何类型的基类,而引用类型则一般具有继承性,这里指的是类和接口,
值类型不能为null值,它会被默认初始化为数值0,而引用类型在默认情况下会初始化为null值,表示不指向托管堆中的任何地址,
由于值类型变量包含其实际数据,因此在默认情况下,值类型之间的参数传递不会影响变量本身,而引用类型变量保存的是数据的引用地址,它们作为参数传递时,参数会发生改变,从而影响引用类型变量的值,
装箱与拆箱,
类型转换指的是将一种数据类型转换成另一种数据类型的过程,
隐式类型转换,由低级别类型向高级别类型的转换过程,如派生类可以隐式的转换为它的父类,装箱过程就属于这种隐式类型转换,
显示类型转换,也叫强制类型转换,这种转换这能会导致精度损失或者出现运行时异常,
通过is和as运算符进行安全的类型转换,
通过.NET类库中的Convert类来完成类型转换,
装箱指的是将值类型转换为引用类型的过程,而拆箱指的是将引用类型转换为值类型,装箱过程中,系统会在托管堆中生成一份堆栈中值类型对象的副本,而拆箱则是从托管堆中将引用类型所指向的已装箱数据复制回值类型对象的过程,
装箱三步骤:
内存分配,在托管堆中分配好内存空间以存放复制的实际数据,
完成实际数据的复制,将值类型实例的实际数据复制到新分配的内存中,
地址返回,将托管堆中的对象地址返回给引用类型变量,
拆箱三步骤:
检擦实例:首先检查要进行拆箱操作的引用类型变量是否为null,如果为null,则抛出NullReferenceException异常,如果不为null,则继续检查变量是否和拆箱后的类型是同一类型,若结果为否,会导致InvalidCastException,
地址返回,返回已装箱变量的实际数据部分的地址,
数据复制,将托管堆中的实际数据复制到栈中,
装箱和拆箱操作对性能有很大的影响,两个过程都需要进行复制,耗时,并且装箱拆箱过程中必然会产生多余对象,进一步加重GC的负担,导致程序性能降低,写代码时尽量避免装箱和拆箱,最好使用泛型编程,
第十一章 泛型
泛型提供了代码重用的另一种机制,它不同于面向对象中通过继承方式实现的代码重用,更确切的说,泛型所提供的代码重用是算法的重用,即某个方法实现不需要考虑所操作数据的类型,
泛型的英文表述是generic,这个单次意为“通用的”,从字面意思可知,泛型代表的就是“通用类型”,它可以代替任意的数据类型,使类型参数化,从而达到只实现一个方法就可以操作多种数据类型的目的,
List<T>是.NET类库中实现的泛型类型,T是泛型参数(可以理解为形参),如果想实例化一个泛型类型,必须传入实际的类型参数,如代码中的int和string,就是实际的类型参数,
为什么要引入泛型,
两个比较方法的大部分代码非常相似,如果只定义一个比较方法就能比较不同类型的大小该多好,
泛型,
泛型List<int>的Add( )和非泛型ArrayList的.Add(object valu)相比,同时for循环添加10000000个int类型,泛型比非泛型性能高,因为其避免了装箱操作,泛型还可以保证类型安全,
全面解析泛型,
类型参数,
T就是类型参数,无论调用类型方法还是初始化泛型实例,都需要用真实类型来代替T,你可以把T理解为类型的一个占位符,即告诉编译器,再调用泛型时必须为其指定一个实际类型,没有为类型参数提供实际类型称为未绑定的泛型,指定了实际类型作为参数称为已构造的泛型,
泛型中静态字段和静态函数问题,
普通类中定义一个静态字段x,不管之后创建了多少个该类的实例,也不管该类派生出多少实例,都只存在一个静态字段x,但泛型类并非如此,每个封闭的泛型类型中都有仅属于它自己的静态数据,
类型参数的推断,即省略<>符号,
以上代码中,编译器会根据传递的方法实参来判断传入的实际类型参数,如果编译器根据传入的参数不能推断出实际参数类型,就会出现编译错误,
从这段代码可以看出,适用类型推断后,可以省略<>符号,从而在泛型代码变多时,增强代码的可读性,类型推断只能用于泛型方法,它对泛型类则并不适用,因为编译器不能通过泛型类的构造函数推断实际的类型参数,
类型参数的约束,
where T : IComparable ,where语句用来使类型参数继承于IComparable接口,从而对类型参数进行约束,
让C#编译器知道这个类型参数一定会有CompareTo方法,
第十二章 可空类型 匿名方法和迭代器
可空类型也是值类型,但它是包含null值得值类型,如,int? nullable = null;
int?,就是可空的int类型,C#中肯定没有int?这个类型,对于编译器而言,int?会被编译成Nullable<int>类型,
可空类型两种情况,有值情况和无值情况,通过HasValue属性来判断可空类型是否有值,
??空合并操作符,它会对左右两个操作数进行判断,如果左边的数不为null,就返回左边的数,如果左边的数为null,就返回右边的数,这个操作符可以用于可空类型,也可以用于引用类型,但是不能用于值类型,因为??运算符会将其左边的值与null进行比较,但除了可空类型外,其它的值类型都是不能与null类型进行比较的,所以??运算符不能应用于值类型,
匿名方法,就是没有名字的方法,匿名方法只能在函数定义的时候被调用(匿名方法是把方法的定义和实现嵌套在了一起),在其它任何情况下都不能被调用,对于编译器来说,匿名方法并不是没有名字的,编译器在编译匿名方法时会为其生成一个方法名,
匿名方法不能在其它地方被调用,即不具有复用性,
迭代器,foreach就是在使用迭代器,C#1.0中,一个类型要想使用foreach关键字进行遍历,它必须实现IEnumerable或IEnumerable<T>接口,因为foreach是迭代语句,想要使用foreach就需要有一个迭代器,IEnumerable接口中定义了一个GetEnumerator方法用来返回迭代器,类型如果实现了IEnumerable接口,则也必须实现GetEnumerator方法,
要获得迭代器就必须实现IEnumerable接口中的GetEnumerator方法,而要实现一个迭代器,则必须实现IEnumerable接口中的MoveNext和Reset方法,C#2.0中,微软提供了yield关键字来简化迭代器的实现,
第十三章 C#3.0中的智能编译器(自动的属性 隐式类型 对象集合初始化器 匿名类型)
自动实现的属性,C#3.0之前,
C#3.0之后,
之所以可以这样定义属性,主要是因为编译器在编译时会帮助我们创建一个私有字段,
在结构体中struct使用自动属性时,需注意,所有构造函数都需要显示的调用无参构造函数this,否则会出现编译错误,
隐式类型,
这样是会报错的哦,无法将类型int隐式转换为string,
使用隐式类型时有一些限制:
被声明的变量是一个局部变量,不能为字段(包括静态字段和实例字段),
变量在声明时必须被初始化,因为编译器要根据变量的赋值来推断类型,如果未被初始化,编译器也就无法完成推断了,C#是静态语言,变量类型未知就会出现编译时的错误,
变量不能初始化为一个方法组,也不能为一个匿名函数,
变量不能初始化为null,因为null可以隐式的转换为任何引用类型或可空类型,编译器将不能推断出该变量到底为什么类型,
不能用一个正在声明的变量来初始化隐式类型,如,string s; var stringvariable = s;
不能用var来声明方法中的参数类型,
隐式类型的优点和缺点,
隐式类型数组,
对象集合初始化器,
C#3.0之前定义类,往往需要定义多个构造函数来完成不同情况下的初始化,C#3.0之后引入对象初始化器后,
要使用对象初始化器,必须确保类具有一个无参构造函数,如果自定义了一个有参构造函数而把无参构造函数覆盖,你则需要重新定义一个无参构造函数,否则会出现编译错误,
如果类中没有定义任何构造函数,编译器会帮我们生成一个无参的构造函数,
再说说集合初始化器,它用来完成对集合中某一元素的初始化,在集合初始化器提出之前,
有了集合初始化器特性之后,
匿名类型,顾名思义就是没有指明类型的类型,通过隐式类型和对象初始化器两种特性创建了一个类型未知的对象,使我们再不定义类型的情况下可以实现对象的创建,从而减少类定义过程的代码,减少了开发人员的工作量,
第十四章 Lambda表达式
Lambda表达式可以理解为一个匿名方法,它可以包含表达式和语句,并且用于创建委托或转换为表达式树,在使用Lambda表达式时,都会使用“=>”运算符,该运算符的左边是匿名方法的输入参数,右边则是表达式或语句块,
Lambda表达式的演变过程,
namespace _14._1 { class Program { static void Main(string[] args) { // Lambda表达式的演变过程 // 下面是C# 1中创建委托实例的代码 Func<string, int> delegatetest1 = new Func<string, int>(Callbackmethod); // ↓ // C# 2中用匿名方法来创建委托实例,此时就不需要额外定义回调方法Callbackmethod Func<string, int> delegatetest2 = delegate(string text) { return text.Length; }; // ↓ // C# 3中使用Lambda表达式来创建委托实例 Func<string, int> delegatetest3 = (string text) => text.Length; // ↓ // 可以省略参数类型string,把上面代码再简化为: Func<string, int> delegatetest4 = (text) => text.Length; // ↓ // 此时可以把圆括号也省略,简化为: Func<string, int> delegatetest = text => text.Length; // 调用委托 Console.WriteLine("使用Lambda表达式返回字符串的长度为: " + delegatetest("learning hard")); Console.Read(); } // 回调方法 private static int Callbackmethod(string text) { return text.Length; } } }
Lambda表达式的使用可以明显减少代码的书写量,从而有利于开发人员更好的维护代码,理清程序的结构,
Lambda表达式除了可以用来创建委托外,还可以转换成表达式树,表达式树是用来表示Lambda表达式逻辑的一种数据结构,它将代码表示成一个对象树,而非可执行的代码,你可以将表达式树理解为一种数据结构,即类似于数据结构课程中的栈和队列,只不过表达式树用于表示Lambda表达式的逻辑罢了,
动态的构造一个表达式树,
namespace _14._6 { class Program { // 构造a+b表达式树结构 static void Main(string[] args) { // 表达式的参数 ParameterExpression a = Expression.Parameter(typeof(int), "a"); ParameterExpression b = Expression.Parameter(typeof(int), "b"); // 表达式树主体部分 BinaryExpression be = Expression.Add(a, b); // 构造表达式树 Expression<Func<int, int, int>> expressionTree = Expression.Lambda<Func<int, int, int>>(be, a, b); // 分析树结构,获取表达式树的主体部分 BinaryExpression body = (BinaryExpression)expressionTree.Body; // 左节点,每个节点本身就是一个表达式对象 ParameterExpression left = (ParameterExpression)body.Left; // 右节点 ParameterExpression right = (ParameterExpression)body.Right; // 输出表达式树结构 Console.WriteLine("表达式树结构为:"+expressionTree); // 输出 Console.WriteLine("表达式树主体为:"); Console.WriteLine(expressionTree.Body); Console.WriteLine("表达式树左节点为:{0}{4} 节点类型为:{1}{4}{4} 表达式树右节点为:{2}{4} 节点类型为:{3}{4}", left.Name, left.Type, right.Name, right.Type, Environment.NewLine); Console.Read(); } } }
通过Lambda表达式来构造表达式树,
namespace _14._4 { class Program { static void Main(string[] args) { // 将lambda表达式构造成表达式树 Expression<Func<int, int, int>> expressionTree = (a, b) => a + b; // 获得表达式树参数 Console.WriteLine("参数1:{0},参数2: {1}", expressionTree.Parameters[0], expressionTree.Parameters[1]); // 获取表达式树的主体部分 BinaryExpression body = (BinaryExpression)expressionTree.Body; // 左节点,每个节点本身就是一个表达式对象 ParameterExpression left = (ParameterExpression)body.Left; // 右节点 ParameterExpression right = (ParameterExpression)body.Right; Console.WriteLine("表达式树主体为:"); Console.WriteLine(expressionTree.Body); Console.WriteLine("表达式树左节点为:{0}{4} 节点类型为:{1}{4}{4} 表达式树右节点为:{2}{4} 节点类型为:{3}{4}", left.Name, left.Type, right.Name, right.Type, Environment.NewLine); Console.Read(); } } }
如何把表达式树转换成可执行代码,
namespace _14._5 { class Program { static void Main(string[] args) { // 将lambda表达式构造成表达式树 Expression<Func<int, int, int>> expressionTree = (a, b) => a + b; // 通过调用Compile方法来生成Lambda表达式的委托 Func<int,int,int> delinstance =expressionTree.Compile(); // 调用委托实例获得结果 int result = delinstance(2, 3); Console.WriteLine("2和3的和为:" + result); Console.Read(); } } }
第十五章 扩展方法
扩展方法,首先是一种方法,它可以用来扩展已定义类型中的方法成员,
在扩展方法诞生前,如果想为一个已有类型自定义含有特殊逻辑的新方法时,你必须重新定义一个类型来继承已有类型,以这种方式来添加方法,如果基类有抽象方法,则还要重新去实现这些抽象方法,这样为了扩展一个方法,我们需要承担更多因继承而产生的开销,使用继承来扩展现有类型总有点大材小用的的感觉,并且值类型或密封类等也不能被继承,不能由此而获得扩展,
扩展方法的使用,
扩展方法的定义规则:
扩展方法必须在一个非嵌套,非泛型的静态类中定义,
它至少要有一个参数,
第一个参数必须加上this关键字作为前缀(第一个参数类型也称为扩展类型,即指方法对这个类型进行扩展),
第一个参数不能使用任何其它的修饰符,如ref,out等修饰符,
第一个参数的类型不能是指针类型,
编译器如何发现扩展方法,
对于C#3.0编译器而言,当它看到某个类型的变量再调用方法时,它会首先去该对象的实例方法中进行查找,如果没有找到与调用方法同名并参数一致的实例方法,编译器就会去查找是否存在合适的扩展方法,
在VS中,扩展方法前面都有一个向下的箭头标志,这只是人们识别扩展方法的方式,编译器则会根据System.Runtime.CompilerServices.ExtensionAttribute属性来识别扩展方法,
从编译器发现扩展方法的过程来看,方法调用的优先级顺序应为:
类的实例方法--》当前命名空间下的扩展方法--》导入命名空间的扩展方法,
根据下面代码思考一下,
namespace CurrentNamespace { // 要使用不同命名空间的扩展方法前要先引入该命名空间 using CustomNamesapce; class Program { static void Main(string[] args) { Person p = new Person { Name = "Learning hard" }; p.Print(); p.Print("Hello"); Console.Read(); } } // 自定义类型 public class Person { public string Name { get; set; } //public void Print() //{ // Console.WriteLine("Person类的实例方法"); //} } // 当前命名空间下的扩展方法定义 public static class Extensionclass { // 扩展方法定义 public static void Print(this Person per) { Console.WriteLine("调用的是当前命名空间下的扩展方法输出,姓名为: {0}", per.Name); } } } namespace CustomNamesapce { using CurrentNamespace; public static class CustomExtensionClass { // 扩展方法定义 public static void Print(this Person per) { Console.WriteLine("调用的是CustomNamesapce命名空间下扩展方法输出,姓名为: {0}", per.Name); } // 扩展方法定义 public static void Print(this Person per, string s) { Console.WriteLine("调用的是CustomNamesapce命名空间下扩展方法输出,姓名为: {0}, 附加字符串为{1}", per.Name, s); } } }
如果扩展的类型中定义了无参的Print实例方法,则在p后面键入“.”运算符时,VS智能提示将不会给出扩展方法,
使用扩展方法时需要特别注意,如果在同一个命名空间下的两个类中含有扩展类型相同的方法,编译器便不知道该调用哪个方法了,就会出现编译错误,
namespace CurrentNamespace { class Program { static void Main(string[] args) { Person p = new Person { Name = "Learning hard" }; // 由于同一命名空间下存在两个Print扩展方法,此时编译器不能判断调用哪个,则出现编译错误 //p.Print(); Console.Read(); } } // 自定义类型 public class Person { public string Name { get; set; } } // 当前命名空间下的扩展方法定义 public static class Extensionclass1 { /// <summary> /// 扩展方法定义 /// </summary> /// <param name="per"></param> public static void Print(this Person per) { Console.WriteLine("调用的是当前命名空间下的扩展方法输出,姓名为: {0}", per.Name); } } // 当前命名空间下的扩展方法定义 public static class Extensionclass2 { // 同一命名空间下定义了两个相同的扩展方法Print public static void Print(this Person per) { Console.WriteLine("调用的是当前命名空间下的扩展方法输出,姓名为: {0}", per.Name); } } }
空引用也可以调用扩展方法,
在c#中,空引用即null上调用实例方法是会引发NullReferenceException异常的,但在空引用上却可以调用扩展方法,
namespace CurrentNamespace { class Program { static void Main(string[] args) { Console.WriteLine("空引用上调用扩展方法演示:"); // s为空引用 string s = null; // 在空引用上调用扩展方法不会发生NullReferenceException异常 Console.WriteLine("字符串S为空字符串:{0}", s.IsNull()); Console.Read(); } } public static class NullExten { // 不规范定义扩展方法的方式 //public static bool IsNull(this object obj) //{ // return obj == null; //} public static bool IsNull(this string s) { return s == null; } } }
第十六章 LINQ解析
LINQ即语言集成查询的意思,
它主要包含4个组件,Linq to Objects,Linq to XML,Linq to DataSet和Linq to SQL,对不同的数据类型进行增删改查等操作,
查询表达式,
点标记方式,
使用Linq to Objects查询集合,
namespace _16._1 { class Program { static void Main(string[] args) { // 初始化查询的数据 List<int> inputArray = new List<int>(); for (int i = 1; i < 10; i++) { inputArray.Add(i); } Console.WriteLine("使用老方法来对集合对象查询,查询结果为:"); OldQuery(inputArray); Console.Read(); } // 使用foreach返回集合中为偶数的元素 private static void OldQuery(List<int> collection) { // 创建保存查询结果的集合 List<int> queryResults = new List<int>(); foreach (int item in collection) { // 判断元素是偶数情况 if (item % 2 == 0) { queryResults.Add(item); } } // 输出查询结果 foreach (int item in queryResults) { Console.Write(item+" "); } } } }
namespace _16._2 { class Program { static void Main(string[] args) { // 初始化查询的数据 List<int> inputArray = new List<int>(); for (int i = 1; i < 10; i++) { inputArray.Add(i); } Console.WriteLine("使用Linq方法来对集合对象查询,查询结果为:"); LinqQuery(inputArray); } // 使用Linq返回集合中为偶数的元素 private static void LinqQuery(List<int> collection) { // 创建查询表达式来获得集合中为偶数的元素 var queryResults = from item in collection where item % 2 == 0 select item; // 输出查询结果 foreach (var item in queryResults) { Console.Write(item+" "); } } } }
使用Linq to XML查询XML文件,
namespace _16._3 { class Program { // 初始化XML数据 private static string xmlString = "<Persons>" + "<Person Id='1'>" + "<Name>张三</Name>" + "<Age>18</Age>" + "</Person>" + "<Person Id='2'>" + "<Name>李四</Name>" + "<Age>19</Age>" + "</Person>" + "<Person Id='3'>" + "<Name>王五</Name>" + "<Age>22</Age>" + "</Person>" + "</Persons>"; static void Main(string[] args) { Console.WriteLine("使用XPath来对XML文件查询,查询结果为:"); OldLinqToXMLQuery(); Console.Read(); } // 使用XPath方式来对XML文件进行查询 private static void OldLinqToXMLQuery() { // 导入XML文件 XmlDocument xmlDoc = new XmlDocument(); xmlDoc.LoadXml(xmlString); // 创建查询XML文件的XPath string xPath = "/Persons/Person"; // 查询Person元素 XmlNodeList querynodes = xmlDoc.SelectNodes(xPath); foreach (XmlNode node in querynodes) { // 查询名字为李四的元素 foreach (XmlNode childnode in node.ChildNodes) { if (childnode.InnerXml == "李四") { Console.WriteLine("姓名为: " + childnode.InnerXml + " Id 为:" + node.Attributes["Id"].Value); } } } } } }
namespace _16._4 { class Program { // 初始化XML数据 private static string xmlString = "<Persons>"+ "<Person Id='1'>"+ "<Name>张三</Name>"+ "<Age>18</Age>"+ "</Person>" + "<Person Id='2'>"+ "<Name>李四</Name>"+ "<Age>19</Age>"+ "</Person>"+ "<Person Id='3'>" + "<Name>王五</Name>" + "<Age>22</Age>" + "</Person>"+ "</Persons>"; static void Main(string[] args) { Console.WriteLine("使用Linq方法来对XML文件查询,查询结果为:"); UsingLinqLinqtoXMLQuery(); Console.Read(); } // 使用Linq 来对XML文件进行查询 private static void UsingLinqLinqtoXMLQuery() { // 导入XML XElement xmlDoc = XElement.Parse(xmlString); // 创建查询,获取姓名为“李四”的元素 var queryResults = from element in xmlDoc.Elements("Person") where element.Element("Name").Value == "李四" select element; // 输出查询结果 foreach (var xele in queryResults) { Console.WriteLine("姓名为: " + xele.Element("Name").Value + " Id 为:" + xele.Attribute("Id").Value); } } } }
第十七章 C#4.0中微小改动(可选参数 命名实参 泛型的协变与逆变)
可选参数和命名参数就如同一对好基友,因为它们经常一起使用,
可选参数重在“可选”,即在调用方法时,该参数可以明确指定实参,也可以不指定,如下代码中定义的方法就包含3个参数,一个必备参数和两个可选参数,
namespace _17._1 { class Program { static void Main(string[] args) { TestMethod(2, 4, "Hello"); TestMethod(2, 14); TestMethod(2); Console.Read(); } // 带有可选参数的方法 static void TestMethod(int x, int y = 10, string name = "LearningHard") { Console.WriteLine("x={0} y={1} name={2}", x, y, name); } } }
在以上代码中,参数x是必选参数,即调用方法必须为其指定实参,而参数y和参数name为可选参数,即可以不用为它们指定实参,
在使用可选参数时,需注意以下几个约束条件:
所有可选参数必须位于必选参数之后,例如下面的方法定义将会导致编译错误,
可选参数的默认值必须为常量,如数字,常量字符串,null,const成员和枚举成员等,如下面方法定义不合法的,
参数数组不能为可选参数,
用ref,out关键字标识的参数不能被设置为可选参数,如下不合理示例,
命名实参,
当调用带有可选参数的方法时,如果我们省略了一个参数,编译器默认我们省略的是最后一个参数,但是如果我们只想省略第二个参数该怎么办呢,
namespace _17._2 { class Program { static void Main(string[] args) { // 省略name参数 TestMethod(2, 14); // 省略了y和name参数 TestMethod(2); // 为部分实参指定名称,使用命名实参只省略第二个参数 TestMethod(2, name : "Hello"); // 为所有实参指定名称 TestMethod(x: 2, y: 20, name: "Hello"); Console.Read(); } // 带有可选参数的方法 static void TestMethod(int x, int y = 10, string name = "LearningHard") { Console.WriteLine("x={0} y={1} name={2}", x, y, name); } } }
COM互操作的福音,
可选参数和命名实参是C#4.0中最简单的两个特性,它们最大的好处是简化了C#与COM组件的互操作,COM是微软曾经推崇的一种开发技术,自从有了高级语言,COM技术已经很少在用于实际开发了,如微软之前的产品,office和ie等,都是基于COM来开发的,
泛型的可变性,
协变性,协变性指的是泛型类型参数可以从一个派生类隐式的转化为基类,C#4.0引入out关键字来标记泛型参数,以示其支持协变性,下面以.NET类库中的public interface IEnumerable<out T>接口为示例,
namespace _17._5 { class Program { static void Main(string[] args) { // 初始化泛型实例 List<object> listobject = new List<object>(); List<string> liststrs = new List<string>(); listobject.AddRange(liststrs); //成功 //liststrs.AddRange(listobject); // 出错 } } }
逆变性,逆变性指的是泛型类型参数可以从一个基类隐式的转化为派生类,C#4.0引入in关键字来标记泛型参数,以示其支持逆变型,下面以.NET类库中的接口public interface IComparer<in T>为例进行演示,
namespace _17._6 { class Program { static void Main(string[] args) { // 初始化泛型实例 List<object> listobject = new List<object>(); List<string> liststrs = new List<string>(); // 初始化TestComparer实例 IComparer<object> objComparer = new TestComparer(); IComparer<string> stringComparer = new TestComparer(); liststrs.Sort(objComparer); // 正确 // 出错 //listobject.Sort(stringComparer); } } // 自定义类实现IComparer<object>接口 public class TestComparer : IComparer<object> { public int Compare(object obj1, object obj2) { return obj1.ToString().CompareTo(obj2.ToString()); } } }
协变和逆变的注意事项:
只有接口和委托才支持协变和逆变,如Func<out TResult>,Action<in T>,类或泛型方法的类型参数都不支持协变和逆变,
协变和逆变只适用于引用类型,值类型不支持协变和逆变,所以List<int>无法转化为IEnumerable<object>
必须显示的用in或out来标记类型参数,
委托的可变性不要在多播委托中使用,
第十八章 动态类型
1.