【C#变迁史】持续更新...
写在前面:查看vs中C#语言支持到的版本:项目属性->生成->高级

C#1.0篇
C#语言自2000年6月发布以来已有十几年的历史,在每次重大的版本升级中,微软都为这门年轻的语言添加了许多实用的特性,下面我们就来看看每个版本都有些什么。老实说,分清这些并没什么太大的实际意义,但是很多老资格的.NET程序员还是热衷在面试中咬文嚼字,所以茶余饭后了解一下也是不错的,所说的内容是对还是错,大家还是勤搜搜看,有误导的地方的还请见谅。
第一代C#只是具有了面向对象语言该有的大框架。这些元素,基本上是每种现代化面向对象语言都有的。
1. 面向对象的基本特征 - 继承,封装,多态
我相信任何一门现代化的面向对象的语言必然实现这些机制,这些东东必然是不可缺少的,这是面向对象编程的核心:继承机制复用了代码,多态机制表征了变化,封装机制隐藏了细节。
也许JavaScript的同仁们不太同意这个说法,确实是,以函数为第一类成员的JS确实淡化了类,没有了接口的概念,但是以Object作为单一根,原型链实现的对象的继承,还是隐约可以看到面向对象的特质。
在C#的实现中,本质上是函数,使用时却像是字段一样的Property比较特别,个人觉得这种设计使用上相当的方便。当然了,有些莫名其妙的面试者总是问C#中Attribute与Property有什么不同?我实在想问一下兄弟们:难道就因为这两个概念翻译过来都可以叫做"属性",就能生搬硬套搞出个辨析题?
2. 基本数据类型,值类型与引用类型
对于常用的的操作数,C#也提供了基本的数据类型:int, double, bool, byte...。这些类型由于使用的太多,而且特性简单,创建和销毁比较容易,于是被设计成值类型(值拷贝,不允许继承)。除了这几种类型,其他的所有类型,包括自定义的类,接口,集合等出于传递性能等因素都被设计成引用类型(地址拷贝)。
把一个类型设计成值类型,还是引用类型,这是一个问题。
C++把所有类型设计成了值类型,于是不得不引入引用的概念,来优化内存和传参时的效率问题。据称,Java把所有的类型设计成引用类型,也是带来显著的性能问题。到了C#的时候,分类处理了一下,既有使用最密集的值类型,也有扩展性最好的引用类型。
值类型是在定义的地方分配的,要么是线程堆栈上(函数参数),要么是托管堆上(作为引用类型的成员),引用类型只会在托管堆上分配。值类型传递的是拷贝,引用类型传递地址,这在传参时最为明显,传进来的值类型对象修改其值后并不会影响传经来之前的值,而引用类型传进来后,修改其值就是修改传进来之前的值,这个特点导致了很多隐藏的问题。当然值类型也可以传递地址给被调用的函数,这样修改传进来的值就是修改原来的值,这就是参数列表中ref与out关键字的作用,这个关键字显然对引用类型没什么效果。
分类处理想法很好,却带来了的一定的复杂行,作为.NET单一根的Object类是一个引用类型,而子类却有一部分是值类型,于是为了满足面向对象"使用基类的地方也可以使用子类"的约定,需要在需要Object类型的时候把值类型包装一下,添加上该有的一些指针,变成引用类型传出去,这就是装箱,在真正使用的时候再把值拷贝出来转成值类型,这是拆箱,在没有泛型之前,这种效率损失有时真的难以让人忍受,特别是某些效率至上的同仁。
说到效率,就不得不说说特殊的string类型,这种类型的使用范围很广,C#的根类Object都提供了ToString方法。从用户角度来说,任何的输出和输入都是字符串,这是字符串受到重视的原因。但是字符串的特性、实现与运算比较复杂,被设计成引用类型。string类的使用方式也是严重影响效率的一个方面。string类实现了字符串的恒定性,而且使用了驻留机制,任何字符串修改操作都是生成新的字符串,这使得如果高频率的使用字符串的修改方法(例如大规模的循环)时,内存会表示压力很大。StringBuilder恰到好处的解决了这些问题,因而成为了string类使用者的贴心伴侣。
此外,深拷贝与浅拷贝也是很常见的一个问题。深拷贝是把对象的所有数据全部拷贝一份,包括对象的成员指向的其他的对象也如此拷贝。而浅拷贝只是把当前对象的所有成员拷贝一份,如果该对象有成员指向别的对象,就不管了。
毫无疑问,所有值类型实现的是深拷贝,因为它们没有指针指向别的地方,拷贝的时候就是把自己复制一份出来。
引用类型就复杂了,因为引用类型内部的成员可能是引用类型,它会指向了别的内存空间。如果实现浅拷贝,那么引用类型的复制品中的引用成员还是与源对象一样,会指向同一个内存空间。如果实现深拷贝,那么被引用的内存空间也会复制一份,拷贝得到的新对象的成员会指向这个新的内存空间。
C#中ICloneable接口提供了拷贝接口,需要实现这个功能的类需要自己实现浅拷贝或者深拷贝。此外,C#中基类Object中提供了浅拷贝的实现,所以自己的类中实现浅拷贝的方法最简单的就是调用Object的MemberwiseClone方法。注意C#中不指定基类的类默认都是从Object继承的。实现深拷贝就需要自己去复制和创建新的成员对象了,当然借助序列化和反序列化也是一种实现深拷贝的方式,因为反序列化的时候会创建全新的对象和引用关系。
3. 集合
语言是解决实际问题的,实际中有群体的概念,程序中自然就有集合的概念,这是类型系统中不可或缺的重量级成员。集合是第一版C#中装箱拆箱的重灾区,使用集合的时候到处充斥着难以忍受的类型装换以及性能损失。
C#的常用集合如Array,ArrayList,Queue,Stack等等都是基于连续内存的一种实现,简单的说都是基于数组实现的,这个自然是访问性与插入删除这两种类型操作之间效率权衡的结果。当然了C#中也有像LinkedList这种链表类集合,实际使用中需要根据操作类型(读取为主?修改为主?)的频率选择合适的集合。
与集合密切相关的是迭代器模式,集合负责保存元素,遍历和枚举的过程就交给了迭代器,这是符合面向对象设计中单一职责原则的。在C#中,如果想使用foreach这种迭代语法的时候,需要先检查一下你自定义的集合是否实现了IEnumerable接口,当然了,内置的集合全部是实现了该接口的。
4. 消息机制,事件与代理
对象有了,那么下一步就是协同工作了。使用delegate定义回调函数的样式与使用event定义消息通知的接口构成了C#通信的基本基调:注册,通知,回调;这也是观察者模式的核心内容。
作为安全的函数指针,delegate近乎完美的完成了自己的工作,但是使用起来需要采用new实例的方式去初始化,似乎不够方便,后面的版本中微软提供了更方便的实现方式。
5. 多线程(Thread方式)
多线程与异步运行这是必须的,那么多的事情总不能一步接一步去做吧,有些事是没有严格的先后次序的,而且时间就是金钱啊,能省就省呗。
内存的运行速度是杠杠的,那些外设的速度完全赶不上,但是基本上所有的程序都是要与外设打交道的,为了不浪费内存的时间,异步势在必行。原本来说,异步指的就是内存与外设不同步这个意思。
同步执行大家都知道是程序一句接一句的执行,由于上面所说的异步是势在必行的,所以当编写程序的时候,为了不浪费内存的时间,有时候就需要异步的执行一些方法,这在C#中是允许的。
在.NET类库中有很多异步调用的方法。一般都是已Begin开头End结尾构成一对异步委托方法,外加两个回调函数和AsyncState参数,组成异步操作的结构。异步编程,就是借助delegate,BeginInvoke,EndInvoke,AsyncCallBack,AsyncState,IAsycResult等类或方式来完成异步操作。
多线程其实与异步没一毛钱关系,但是很多人习惯一起说,而且它们执行的方式有那么一点相同,那咱也一起说吧。多线程现在更多的是与多核联系在一起(单核的时候分时间片去运行不同的线程),既然咱不差钱,多买了几个核,不用总是浪费的,浪费可耻啊,那就用上,于是没有先后依赖关系的相对独立的一些任务就可以放到别的线程中做了,做好后以一定的方式(回调或者直接把结果塞回来)通知一下主线程就可以了。
以Thread类为核心的一组类提供了基本的多线程功能,它们功能多样,用户可控性高(可以随时申请中止线程等操作),是许多多线程用户的最爱。同时,C#还有一个叫ThreadPool的玩意儿,用户只要塞给它一些活就不用管了,喝杯咖啡等待结果即可,十分方便,不喜欢操控性的用户很喜欢这种感觉。
当然除了这些通用的元素,C#自身也存在很多闪光点:
1. Attribute特性
这是C#特有的,其他语言还真没有这个东东。元数据是C#程序的重要信息,这些数据携带了一些信息,根据这些信息,负责解析的类或者框架可以完成一些很特别的功能。比如enum类型,当它带上Flag这顶帽子的时候,乖乖不得了了,它居然可以参与位运算了。一定程度上说,Attribute是一套强大的系统,一套强大的自定义系统。
2. 自动垃圾回收
自动垃圾回收是通过GC这个类完成的,一般没什么人会去调用这个类的方法强制回收。自动垃圾回收以前可以要挂起相关线程,回收程序所有"根"没有引用的垃圾,压缩托管堆,重新修改引用的地址,然后再恢复现场的。效率应该不高,但是本人感受不深。据说现在可以回收的时候不挂起所有线程,不知道怎么样。
垃圾回收与代龄的问题一般只是停留在讨论的层次,很少有人实际写程序中使用。但是与之相关的Dispose模式就风光了,如何实现一个标准的Dispose模式据说是很多Senior考生的必考项目。
Dispose模式与using息息相关,using关键字后面的资源一定要实现IDisposible接口,这是using引用namespace之外的另一处用法,如何实现这个模式在网上一搜一大把,兄弟们自己看着办吧。
此外,与垃圾回收息息相关的还有"~"方法,这个在C++中称为析构函数的家伙在C#一跃称为Finalize方法,标准的Dispose模式是包含这个函数处理的。实现了这个方法的类是需要二次回收的,第一次回收的时候这些对象托管资源释放以后,需要进行第二次回收,通常是回收非托管资源。处于两次回收之间的这些对象是可以复活的,但是实际中好像做的人不是太多。
3. 反射与动态创建
动态创建时很酷的一个特性,很多语言比如Java中也有。通常是提供一个配置文件,XML可以,普通的TXT也可以,从里面读出一个字符串,然后从这个字符串变出一个类的实例然后使用,多炫啊。当然了反射不仅仅可以干这个,它还可以动态的加载一个dll,动态的创建类型,动态的调用类型的方法,这些都是字符串驱动的(参数都是字符串),是不是很帅?System.Activator,System.Reflection.Assembly,System.Type等是完成了这一特性的得力干将。
4. 中间语言的概念与CLR
这两个家伙与自动垃圾回收是托管语言(微软提出来的概念,所以托管语言指的就是基于.NET的几种开发语言)的基石,他们彻底隔离了机器码与编程语言,于是C#,VB.NET,托管C++等编译后万祖归宗,编译后都变成了中间代码,等到第一次运行的时候,JIT编译器会将这些中间代码转换成真正的机器代码,这样据说很好的提供了程序的移植性。
当然了为了提高程序启动的性能(总不能每次运行程序都需要JIT编译一下吧!),首次运行时JIT编译好的机器码会被秘密的保存在一个地方,这样下次运行程序的时候,就不需要JIT再次编译一下了。此外,很多高级玩家宣称:不懂中间语言IL,就不算真懂C#。对此,本人持保留态度,因为从我的角度来说,使用好语言才是目前的首要工作,刨根问底是以后的事,呵呵。
5. 统一而且完备的类库FCL
提供一个完整的类库是相当有必要的,和以前的MFC是一个道理。使用一门语言时,比如JavaScript,每次当你看到多如牛毛的第三方插件和类库的时候,难道你没有想吐的欲望?这是微软相当具有竞争力的一个做法,不管你信不信,反正我信了。完备的FCL是支撑.NET大厦的砖头,没有这些东西,就没有.NET的各项功能,也就没有各位大佬们面试时拍人的武器了。
C#2.0篇
第一代的值类型装箱与拆箱的效率极其低下,特别是在集合中的表现,所以第二代C#重点解决了装箱的问题,加入了泛型。
1. 泛型 - 珍惜生命,远离装箱
集合作为通用的容器,为了兼容各种类型,不得已使用根类Object作为成员类型,这在C#1.0中带来了很大的装箱拆箱问题。为了C#光明的前途,这个问题必须要解决,而且要解决好。
C++模板是一个有用的启迪,虽然C++模板的运行机制不一样,但是思路确实是正确的。
带有形参的类型,也就是C#中的泛型,作为一种方案,解决了装箱拆箱,类型安全,重用集合的功能,防止具有相似功能的类泛滥等问题。泛型最大的战场就是在集合中,以List<T>,Queue<T>,Stack<T>等泛型版本的集合基本取代了第一代中非泛型版本集合的使用场合。当然除了在集合中,泛型在其他的地方也有广泛的用途,因为程序员都是懒的,重用和应对变化是计算机编程技术向前发展最根本的动力。
为了达到类型安全(比如调用的方法要存在),就必须有约定。本着早发现,早解决的思路,在编译阶段能发现的问题最好还是在编译阶段就发现,所以泛型就有了约束条件。泛型的约束常见的是下面几种:
a. 构造函数约束(使用new关键字),这个约束要求实例化泛型参数的时候要求传入的类必须有公开的无参构造函数。
b. 值类型约束(使用struct关键字),这个约束要求实例化泛型参数的类型必须是值类型。
c. 引用类型约束(使用class关键字),这个约束要求实例化泛型参数的类型必须是引用类型。
d. 继承关系约束(使用具体制定的类或接口),这个约束要求实例化泛型参数的类型必须是指定类型或是其子类。
当然了,泛型参数的约束是可以同时存在多个的,参看下面的例子:
public class Employee { } class MyList<T, V> where T : Employee, IComparable<T> where V : new() { }
如果不指定约束条件,那么默认的约束条件是Object,这个就不多讲了。
当使用泛型方法的时候,需要注意,在同一个对象中,泛型版本与非泛型版本的方法如果编译时能明确关联到不同的定义是构成重载的。例如:
public void Function1<T>(T a); public void Function1<U>(U a); 这样是不能构成泛型方法的重载。因为编译器无法确定泛型类型T和U是否不同,也就无法确定这两个方法是否不同 public void Function1<T>(int x); public void Function1(int x); 这样可以构成重载 public void Function1<T>(T t) where T:A; public void Function1<T>(T t) where T:B; 这样不能构成泛型方法的重载。因为编译器无法确定约束条件中的A和B是否不同,也就无法确定这两个方法是否不同
使用泛型就简单了,直接把类型塞给形参,然后当普通的类型使用就可以了。例如:
List<int> ages = new List<int>(); ages.Add(0); ages.Add(1); ages.Remove(1);
2. 匿名函数delegate
在C# 2.0中,终于实例化一个delegate不再需要使用通用的new方式了。使用delegate关键字就可以直接去实例化一个delegate。这种没有名字的函数就是匿名函数。这个不知道是不是语法糖的玩意儿使用起来确实比先定义一个函数,然后new实例的方式要方便。不过最方便的使用方式将在下一版中将会到来。
delegate void TestDelegate(string s); static void M(string s) { Console.WriteLine(s); } //C# 1.0的方式 TestDelegate testDelA = new TestDelegate(M); //C# 2.0 匿名方法 TestDelegate testDelB = delegate(string s) { Console.WriteLine(s); };
谈到匿名函数,不得不说说闭包的概念。
如果把函数的工作范围比作一个监狱的话,函数内定义的变量就都是监狱中的囚犯,它们只能在这个范围内工作。一旦方法调用结束了,CLR就要回收线程堆栈空间,恢复函数调用前的现场;这些在函数中定义的变量就全部被销毁或者待销毁。但是有一种情况是不一样的,那就是某个变量的工作范围被人为的延长了,通俗的讲就像是某囚犯越狱了,它的工作范围超过了划定的监狱范围,这个时候它的生命周期就延长了。
闭包就是使用函数作为手段延长外层函数中定义的变量的作用域和生命周期的现象,作为手段的这个函数就是闭包函数。看一个例子:
class Program { static void Main(string[] args) { List<Action> actions = getActions(); foreach (var item in actions) { item.Invoke(); } } static List<Action> getActions() { List<Action> actions = new List<Action>(); for (int i = 0; i < 5; i++) { Action item = delegate() { Console.WriteLine(i); }; actions.Add(item); } return actions; } }
你可以试试运行这个例子,结果和你预想的一致吗?这个例子会输出5个5,而不是0到4,出现这个现象的原因就是闭包。getActions函数中的变量i被匿名函数引用了,它在getActions调用结束后还会一直存活到匿名函数执行结束。但是匿名函数是后面才调用的,执行它们的时候,i早就循环完毕,值是5,所以最终所有的匿名函数执行结果都是输出5,这是由闭包现象导致的一个bug。
要想修复这个由闭包导致的问题,方法基本上是破坏闭包引用,方式多种多样,下面是简单的利用值类型的深拷贝实现目的。
第一个方法:让闭包引用不再指向同一个变量
for (int i = 0; i < 5; i++) { int j = i; Action item = delegate() { Console.WriteLine(j); }; actions.Add(item); }
第二个方法:包上一层函数来构造新的作用域
static List<Action> getActions() { List<Action> actions = new List<Action>(); for (int i = 0; i < 5; i++) { Action item = ActionMethod(i); actions.Add(item); } return actions; } static Action ActionMethod(int p) { return delegate() { Console.WriteLine(p); }; }
闭包现象提醒我们使用匿名函数和3.0中的Lambda表达式时都要时刻注意变量的来源。
3. 迭代器
在C# 1.0中,集合实现迭代器模式是需要实现IEnumerable的,这个大家还记得吧,这个接口的核心就是GetEnumerator方法。实现这个接口主要是为了得到Enumerator对象,然后通过其提供的方法遍历集合(主要是Current属性和MoveNext方法)。自己去实现这些还是比较麻烦的,先需要定义一个Enumerator对象,然后在自定义的集合对象中还需要实现IEnumerable接口返回定义的Enumerator对象,于是一个新的语法糖就出现了: yield关键字。
在C# 2.0中,只需要在自定义的集合对象中还需要实现IEnumerable接口返回一个Enumerator对象就行了,这个创建Enumerator对象的工作就由编译器自己完成了。看一个简单的小例子:
public class Stack<T>:IEnumerable<T> { T[] items; int count; public void Push(T data){...} public T Pop(){...} public IEnumerator<T> GetEnumerator() { for(int i=count-1;i>=0;--i) { yield return items[i]; } } }
使用yield return创建一个Enumerator对象是不是很方便?编译器遇到yield return会创建一个Enumerator对象并自动维护这个对象。
当然了,多数时候foreach必要遍历集合中的每一个元素,这个时候使用yield return配合for循环枚举每个元素就可以了,但是有时候只需要返回满足条件的部分元素,这个时候就要结合yield break中断枚举了,看一下:
//使用yield break中断迭代: for(int i=count-1;i>=0;--i) { yield return items[i]; if(items[i]>10) { yield break; } }
4. 可空类型
这个特性我觉得又是把值类型设计成引用类型Object类子类后,微软生产的怪语法。空值是引用类型的默认值,0值是值类型的默认值。那么在某些场合,比如从数据库中的记录取到内存中以后,没有值代表的是空值,但是字段的类型却是值类型,怎么搞呢?于是整出了可空类型。当然了,这个问题可以通过在设计表的时候给字段设计一个默认值来解决,但是有的时候某些字段的设置默认值是没有意义的,比如年龄,0有意义吗?
可空类型的概念很简单,没什么可说的,不过一个相似的语法却让我感到很舒服:那就是"??"操作符。这是一个二元操作符,如果第一个操作数是空值,则执行第二个操作数代表的操作,并返回其结果。例如:
static void Main(string[] args) { int? age = null; age = age ?? 10; Console.WriteLine(age); }
5. 部分类与部分方法
这一特性还是比较好用的,终于不用把所有的内容挤到一起了,终于可以申明和实现相分离了,虽然好像以前也可以做到,但是现在这项权利也下放给人民群众了。partial关键字带来了这一切,也带来了一定的扩展性。这个特性也比较简单,就是使用partial关键字。编译的时候,这些文件中定义的部分类会被合并。部分方法是3.0的特性,不过没什么新意,就放到2.0一起说吧。
使用这个特性的时候需要注意:
a. 部分方法只能在部分类中定义。
b. 部分类和部分方法的签名应该是一致的。
c. partial用在类型上的时候只能出现在紧靠关键字 class、struct 或 interface 前面的位置。
d. public等访问修饰符必须出现在partial前面。
f. partial定义的东西必须是在同一程序集和模块中。
看一个简单的例子:
// File1.cs namespace PC { partial class A { int num = 0; void MethodA() { } partial void MethodC(); } } // File2.cs namespace PC { partial class A { void MethodB() { } partial void MethodC() { } } }
这里需要注意一下MethodC的申明和定义是分开的就可以了。
还有一点,很多人认为partial破坏了类的封装性,实际上谈不上。因为一个类能分部,就说明类的设计者认为是需要保留这个扩展性的,所以后面的人才可以给这个类添加一些新的东西。
6. 静态类
这个特性也是比较符合实际情况的,很多情况下,某些对象只需要实例化一次,然后到处使用,单件模式是可以实现这个目的,现在静态类也是一个新的选择。
静态类只能含有静态成员,所以构造函数也是静态的,既然是静态的,那么它与继承就没什么关系了。静态类从首次调用的时候创建,一直到程序结束时销毁。
简单看一个小例子:
public static class A { static string message = "Message"; static A() { Console.WriteLine("Initialize!"); } public static void M() { Console.WriteLine(message); } }
不过据经验讲,有没有静态构造函数对静态类的构造时间是有影响的,一个是出现在首次使用对象成员的时候,一个是程序集加载的时候,不过分清这个实在没什么意义,有兴趣的同学自己研究吧。
C#2.0的新特性绝不止这几个,但是对程序猿们影响比较大的都在这了,更多的就参看微软的MSDN吧。
C#3.0篇
C# 3.0 (.NET 3.5, VS2008) 第三代C#在语法元素基本完备的基础上提供了全新的开发工具和集合数据查询方式,极大的方便了开发。
1. WPF,WCF,WF
这3个工程类型奠定了新一代.NET开发的客户端模型,通信模型,工作流模型。
WPF即将取代Winform成为新一代的桌面程序开发工具,控件与代码的超低耦合给开发带来了极大的方便,脱胎于MVC的MVVM模式也展现了极强的生命力。WCF即将融合原来的Web Service的各种提供方式,彻底屏蔽掉各种细节,开启Service服务的篇章。WF似乎使用的比较少,目前不做评论。
这3个概念每个都可以写上几本书,这里就不出丑了。
2. Lambda表达式
Lambda 表达式是一种可用于创建委托或表达式树类型的匿名函数。 通过使用 lambda 表达式,可以创建可作为参数传递或作为函数调用值返回的本地函数。 Lambda 表达式主战场是 Linq 查询表达式,这个在下面会提及;由于Lambda表达式是创建委托和表达式树的,所以它在除Linq之外的地方使用也很广泛,使用Lambda表达式可以写出相当优雅的委托代码。
若要创建 Lambda 表达式,需要在 Lambda 运算符 => 左侧指定输入参数(如果有),然后在另一侧输入表达式或语句块。 例如,lambda 表达式 x => x * x 指定名为 x 的参数并返回 x 的平方值。
左侧的参数列表可以带参数类型,也可以不带。指定参数类型时通常是因为右侧的语句块中使用了该类型的相关方法;当不带参数类型的时候,就由编译器根据上下文去推断,推断成立则没问题,推断不成立则编译错误。左侧的参数列表需要带"()",但是当只有一个无类型的参数时可省略。
右侧的如果是单个表达式的话可以不带"{}"和";",是语句块的话则要加上。
你可以将此表达式分配给委托类型:
delegate int del(int i); static void Main(string[] args) { del myDelegate = x => x * x; int j = myDelegate(5); //j = 25 }
当然也可以用于创建表达式树类型:
using System.Linq.Expressions; namespace ConsoleApplication1 { class Program { static void Main(string[] args) { Expression<del> myET = x => x * x; } } }
表达式树是一种动态创建表达式的工具,表达式树可以编译和运行;使用它可以动态的创建表达式,它的使用场合主要是在LINQ中区创建动态的查询条件表达式。关于表达式树,有兴趣的同学可以参考MSDN:http://msdn.microsoft.com/zh-cn/library/bb397951.aspx 。
总的来说,Lambda表达式是基于delegate又高于delegate的东东,青出于蓝而胜于蓝。它更加弱化的约束极大了丰富了表达式的含义,所谓描述越抽象,意义越丰富嘛!难怪抽象派艺术家的作品生命力都很强。在C#中,约束最强的应该就是继承(包括类的继承,接口的实现),它要求子类扩展父类或接口的时候方法时签名要完全匹配,包括返回值类型,函数名,参数类型等;约束次之的是delegate,它不再要求方法名相同了,只要求返回值类型与参数类型相同;到了lambda表达式了,约束更加弱化,连参数与返回值的类型都不要求完全匹配了,难怪有人说如果当初现有lambda表达式的话,就不会有匿名函数的语法了。
3. 对象初始化器和集合初始化器
直接初始化的时候就可以初始化是很方便的事,这个终于有了,虽说是在LINQ中使用最多,但是在其它场合使用对象初始化器和集合初始化器编程还是显得特别优雅。这个比较简单不多说了,看例子:
// 基本用法: User user =new User { Id = 1, Name ="AA", Age = 22 }; //嵌套使用: User user =new User { Id = 1, Name ="AA", Age = 22, Address = new Address { City ="NanJing", Zip = 21000 } }; //类似于对象初始化器初始化一个对象,集合初始化器初始化一个集合, //一句话,有了它你就不用在将元素通过Add逐个添加了: //基本使用: List<int> num =new List<int> { 0, 1, 2, 6, 7, 8, 9 }; //结合对象初始化器,我们可以写出如下简洁的代码: List<User> user =new List<User>{ new User{Id=1,Name="AA",Age=22}, new User{Id=2,Name="BB",Age=25}, };
4. 匿名类型
很多时候,我们需要使用一个临时的对象,按通常的做法,我们要先定义一个类吧,这样才能实例化这个类得到对象,这样实在是太累了;而且往往这样的对象只需要使用在局部的场合,使用以后就不再使用了,例如从数据库中查询出来的临时结果。在3.0中,我们终于不再需要预先定义一个类型了,CLR会提供一种形式让你动态的生成一个无类型的对象。
匿名类型提供了一种方便的方法,可用来将一组只读属性封装到单个对象中,而无需首先显式定义一个类型。 类型名由编译器生成,并且不能在源代码级使用。 每个属性的类型由编译器推断。
在看具体的例子之前,先看一个新的关键字:var。这个关键字在Javascript中使用广泛,在C#中使用var声明一个局部变量(只能在函数中使用var定义变量,这种变量官方称之为隐式类型局部变量)时,编译器会自动根据其赋值语句推断这个局部变量的类型。赋值以后,这个变量的类型也就确定而不可以再进行更改。但是这个var关键字最主要的用途是去生成匿名类型,比如表示一个Linq查询的结果。这个结果可能是ObjectQuery<T>或IQueryable<T>类型的对象,也可能是一个简单的实体类型的对象。这时使用var声明这个对象可以节省很多代码书写上的时间。
看一下匿名类型的使用方式:
var people = new { Name="AA", Age = 10 }; Console.WriteLine(people.Name);
需要注意的是,上面创建的people的Name和Age是只读的,不能去修改。
5. 扩展方法
Linq扩展了原来的IEnumerable得到IQueryable,如何自然的融入原来的集合中却是一个问题,有了这个语法糖,只要符合规定的语法,引入定义扩展方法的namespace,新添加的方法就像是对象原来就有的方法那样方便使用,这样就在不破坏原对象封装性的前提下给该对象添加了新的行为,还是蛮符合面向对象Open-Close原则的。
看网上的一个例子:
using System; using System.Text.RegularExpressions; namespace ConsoleApplication3 { class Program { static void Main(string[] args) { string email = "someone@somewhere.com"; Console.WriteLine(email.IsValidEmailAddress()); } } public static class Extensions { public static bool IsValidEmailAddress(this string s) { Regex regex = new Regex(@"^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$"); return regex.IsMatch(s); } } }
如上代码所示,扩展方法为一静态方法,声明于一个静态类,其参数前加上一个this关键字,参数的类型表示这个扩展方法要对这个类型进行扩展。如上述代码表示其要对字符串类型进行扩展。使用起来也很方便,所有的string类型现在都多了一个叫IsValidEmailAddress的方法。
6. Linq
当所有的配角都到齐的时候,主角之一的Linq (Language Integrated Query,语言集成查询)应该要亮相了,它是3.0时代最为强悍的工具,从此查询集合数据有了最为便捷的方式,3.0中上面的许多特性基本上都是为了Linq 服务的。
Linq主要包含4个组件——Linq to Objects、Linq to XML、Linq to DataSet 和Linq to SQL,这4个组件对应4种查询的对象:内存集合中的对象,XML,SQL和Dataset。在我的经历中,我用的最多的是前三个,所以这里主要就是总结前三种查询。
在Linq之前查询数据,基本的方式就是循环遍历,按照条件比较,把得到的结果放到临时集合中。Linq彻底简化了这个过程,使用了简单的方式完成了查询。不管查询上述的哪些对象,方式其实还都是一样的:要么使用查询表达式,要么使用扩展方法。
以查询内存集合中的对象为例,看一下两种使用方式:
using System.Linq; List<string> collection = new List<string>(); for (int i = 0; i < 10; i++) { collection.Add("A" + i.ToString()); } // 创建查询表达式来获得序号为偶数的元素 var queryResults = from s in collection let index = int.Parse(s.Substring(1)) where index % 2 == 0 select s; // 使用扩展方法来获得序号为偶数的元素 var queryResults1 = collection.Where(item => { int index = int.Parse(item.Substring(1)); return index % 2 == 0; }); // 输出查询结果 foreach (string s in queryResults) { Console.WriteLine(s); }
查询Xml的方式比较相似,下面来自网上的例子比较了使用原来Class访问Xml的方式,和使用XLinq访问Xml的方式:
using System.Xml; using System.Xml.Linq; static void Main(string[] args) { Console.WriteLine("使用XPath来对XML文件查询,查询结果为:"); OldLinqToXMLQuery(); Console.WriteLine("使用Linq方法来对XML文件查询,查询结果为:"); UsingLinqLinqtoXMLQuery(); Console.ReadKey(); } // 初始化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>"; // 使用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); } } } } // 使用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); } }
需要注意的是使用上面Linq查询得到的结果,只有当使用foreach等方法去遍历的时候查询才会真正的执行并返回结果。
C#4.0篇
C# 4.0 (.NET 4.0, VS2010)第四代C#借鉴了动态语言的特性,搞出了动态语言运行时,真的是全面向“高大上”靠齐啊。
1. DLR动态语言运行时
C#作为静态语言,它需要编译以后运行,在编译的过程中,编译器要检查语法的正确性和类型的安全性,这是一个静态查找(编译时查找)的过程。确实,在运行之前发现问题总比在运行时发型问题要好的多,早发现早治疗嘛!但是这样做有时候会带来一些麻烦,比如类型在编译时无法获得时。
看网上经典的一个例子:动态计算器。
假设有一个计算器,它所在的程序集是动态加载进来的;当我们需要使用这个计算器计算数据时,通常是使用反射的方式:
object calc = GetCalculator(); Type calcType = calc.GetType(); object res = calcType.InvokeMember("Add", BindingFlags.InvokeMethod, null, new object[] { 10, 20 }); int sum = Convert.ToInt32(res);
还有一种情况出现在Office程序中,例如给某单元格赋值:
((Excel.Range)excel.Cells[1, 1]).Value2= "Hello";
因为Cells返回的类型要想使用Value2属性,需要进行类型转换。
在上面的这些例子中,因为C#是静态语言类型,就是强类型语言,所以要使用某个类的成员,就需要在编译的时候保证使用的是这个类的实例,或者是用反射。在这些场合下,写这样的代码无疑是不够优雅的。
在C# 4.0中,这个情况会得到改善,因为这个版本的C#天生支持运行时类型查找,那就是CLR级别的DLR特性与语法级别的dynamic类型。
在4.0中,程序可以直接写成:
// Calculator dynamic calc = GetCalculator(); int sum = calc.Add(10, 20); // Office excel.Cells[1, 1].Value2 = "Hello";
使用dynamic定义的对象,CLR将不再进行静态查找,而是交给DLR在运行时进行动态的查找。这样的做法无疑是拓展了程序的扩展性和约束,例如此前要实现某些公共的行为,通常是需要先定义一个接口,拥有这个行为的对象实现这个接口,这样在程序中就可以针对这个接口进行编程。但是使用dynamic以后,这个接口就可以省掉了,直接使用成员就可以了。
dynamic作为新的类型,可以用在任何类型允许出现的场合。当然也可以用在变量的传递中,Runtime会自动选择一个最匹配的方法。使用dynamic类型,就可以不去关心对象的实例是来源于COM, IronPython, HTML DOM或者反射,只要知道有什么方法可以调用就可以了,剩下的工作就交给DLR了。
其实在某种程度上,可以认为dynamic类型是object类型的一个特殊版本,除了具有object所有的特征外,还指出了对象可以动态地使用。选择是否使用动态行为很简单,任何对象都可以隐式转换为dynamic,直到运行时才动态绑定。反之,从dynamic到任何其他类型都存在隐式转换。例如:
dynamic d = 7; int i = d;
上面所谓的动态操作,不仅是指方法调用,字段和属性访问、索引器和运算符调用,甚至委托调用都可以动态地调用,例如:
dynamic d = GetDynamicObject(…); d.M(7); d.f = d.P; d["one"] = d["two"]; int i = d + 3; string s = d(5,7);
同时,任何动态操作的结果本身也是dynamic类型的,这个自然是很好理解。
但是需要注意dynamic也不是万能的:
1). 目前动态查找不支持扩展方法的调用(可能在未来的版本的C#中会提供支持)。
2). 匿名方法和Lambda表达式不能转换为dynamic,也就是说dynamic d = x=>x;是不合法的,事实上lambda表达式也不能转成object。一样的道理,因为lambda表达式会在上下文环境下要么被编译器解释成委托类型,要么被解释成表达式树,但是如果上下文缺乏类型信息,编译器会无法解析。
所以总的说来,还是那一条,编译器能认识的地方(能编译,能推断)就可以使用dynamic。
dynamic的实现是基于IDynamicObject接口和DynamicObject抽象类。而动态方法、属性的调用都被转为了GetMember、Invoke等方法的调用。如果想在自己的代码中实现一个动态类型对象,可以继承DynamicObject类,并实现自己的若干get和set方法。看一个网上的例子:
public class MyClass:DynamicObject { public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result) { result = binder.Name; return true; } }
上述代码在尝试invoke某个方法的时候直接返回该方法的名字。于是下面的代码将输出方法名:
dynamic d = new MyClass(); Console.WriteLine(d.AnyMember());
最后来谈谈DLR(Dynamic Language Runtime),它是.Net 4.0中一组全新的API。对于C#,DLR提供了Microsoft.CSharp.RuntimeBinder命名空间,它为C#提供了强大的运行时互操作(COM,Ironpython等)能力,DLR具有优秀的缓存机制,对象一旦被成功绑定,CLR在下一次调用的时候就可以直接对确定类型的对象进行操作,而不必再通过DLR去查找了。
2. 命名参数(Named Parameter)与可选参数(Optional Parameter)
// 方法声明 public void M(int x, int y = 5, int z = 7); // 方法使用方式 M(1, 2, 3); // 这个没什么可说的 M(1, 2); // 等价于(1, 2, 7) M(1); // 等价于 M(1, 5, 7)
1).可选参数必须有个编译时常量作为其默认值。如果是除string之外的引用类型(包括那个特殊的dynamic类型),默认值只能是null。下面的声明是不能通过编译的:
static void Foo(int a, string s = "aaa", dynamic b = 2, MyClass c = new MyClass())
2).可选参数必须在不可选参数之后。下面的声明是不能通过编译的:
static void Foo(string s = "aaa", int a, dynamic b = null, MyClass c = null)
3).可选参数不仅适用于普通的方法,还适用于构造器,索引器中,本质上它们没有什么不同。
说完可选参数,下面再谈谈命名参数。说的简单一点,命名参数就是在调用的时候指定了参数定义时的名称的参数,这样就能帮助有效编译器匹配实参和形参。
对于Required Parameter来说,调用的时候是严格按顺序来的,自然不需要指定参数名称了,但是指定了因为没关系。
对于Optional Parameter来说,调用时方法时,由于这些参数中某些参数使用了默认值,所以可能不出现在调用的实参列表中的,为了避免会歧义,这时就需要使用形参名称来避免误会。这是命名参数使用最多的场合。
而且使用了命名参数后,编译器可以很轻松的配对实参和形参,所以参数的顺序就可以不按照定义时的顺序了。
static void Main(string[] args) { M(1, "A"); M(x: 1, s: "A"); M(s:"B", x:2); M1(3, s1:"Hi", s2:"Dong"); M1(4, s2:"Dong", s1: "Hi"); M1(5, s2: "Dong"); } static void M(int x, string s) { Console.WriteLine(x); Console.WriteLine(s); } static void M1(int x, string s1 = "Hello", string s2="DXY") { Console.WriteLine(x); Console.WriteLine(s1 + " " + s2); }
3. 协变与逆变
协变与逆变(Covariance and contravariance)指的是基类与子类实例之间满足条件的隐式转换;简单来讲,所谓协变(Covariance)是指把类型从“小”升到“大”,比如从子类升级到父类;逆变则是指从“大”变到“小”,比如从父类降级到子类。
它们是面向对象语言的基本特征之一,与继承机制息息相关。继承机制与面向对象设计五大原则之一的里氏替换原则都要求所有使用基类的地方都可以使用子类,这包括传递参数的时候。
此外,好的面向对象设计也要求对象满足“宽进严出”,概括的说就是传进对象的对象要求要宽松一点,流出对象的对象要求要严格一点。
具体来说,这一原则体现在对象的初始化上,就是可以把子类的实例付给基类。这一原则体现在对象方法的实现上,就是方法的参数尽量使用能使用的基类(宽进,这样方法的灵活性就很好,所有基类的子类都可以传入该方法),方法的返回值尽量使用能使用的子类(严出,这样方法的返回值就容易明确方法的目的性,使用该方法的对象更容易处理返回值)。这一原则体现在代理delegate上,就是实例化代理类型的时候,使用的方法的参数可以是代理定义中参数类型的基类,使用的方法的返回值可以是代理定义中返回值类型的子类。比较绕吧,有时候我自己都会用错词,看看例子就会很清楚了:
delegate BaseResult MethodHandler(BaseParameter p); class Program { static void Main(string[] args) { // 协变: DerivedParameter -> Parameter Parameter p = new DerivedParameter(); // 完全匹配 MethodHandler m1 = M1; // 逆变: 参数Parameter -> BaseParameter MethodHandler m2 = M2; // 协变: 返回值DerivedResult -> BaseResult MethodHandler m3 = M3; } static BaseResult M1(BaseParameter p) { return null; } static BaseResult M2(Parameter p) { return null; } static DerivedResult M3(BaseParameter p) { return null; } } abstract class Parameter { } class BaseParameter : Parameter { } class DerivedParameter : BaseParameter { } abstract class Result { } class BaseResult : Result { } class DerivedResult : BaseResult { }
虽然大部分情况下,我们直接初始化的类型都与定义的类型时完全匹配的,但是上面的例子中的初始化其实都是合法的,不仅合法,而且通常使用协变和逆变的方式其实更符合面向接口编程的方式。
在4.0这个版本之前,泛型是不能满足协变和逆变的特性的,有兴趣的同学可以验证一下,虽然没什么实际意义。在4.0中,协变和逆变得到了改善,在泛型中的得到了进一步的支持;这是和out与in两个关键字密切相关的:out修饰的泛型参数只能作为函数的输出,in修饰的泛型参数只能作为函数的输入参数类型,使用了这两个关键字的泛型就满足协变和逆变的特性。看下面的例子:
delegate T ActionHandler<out T>(); class Program { static void Main(string[] args) { ActionHandler<string> a1 = M; ActionHandler<object> a2 = a1; IEnumerable<string> strings = new List<string>(); IEnumerable<object> objects = strings; } static string M() { return null; } }
例子中自定义泛型使用了out修饰泛型参数,因而例子中的用法是合法的。.NET Framework中的很多泛型都添加了这个修饰符,例如:
.Net4.0中使用out/in声明的Interface: System.Collections.Generic.IEnumerable< out T> System.Collections.Generic.IEnumerator< out T> System.Linq.IQueryable< out T> System.Collections.Generic.IComparer< in T> System.Collections.Generic.IEqualityComparer< in T> System.IComparable< in T> .Net4.0中使用out/in声明的Delegate: System.Func< in T, …, out R> System.Action< in T, …> System.Predicate< in T> System.Comparison< in T> System.EventHandler< in T>
其实,做这些本质上都是要在保证运行时类型安全的前提下提高代码的可重用性和灵活性。正是因为这个原因,IList<T>泛型没有添加out/in声明,所以下面的用法是不对的:
IList<string> strings = new List<string>(); IList<object> objects = strings;
究其根本原因,还是因为上面的使用无法保证运行时类型安全。例如下面的代码:
objects[0] = 5; string s = strings[0];
这会允许将int插入strings列表中,然后将其作为string取出,这会破坏类型安全,所以IList这种允许修改元素的集合没有添加out/in声明。
C#4.0中的协变和逆变使得泛型编程时的类型转换更加自然,不过要注意的是上面所说的协变和逆变都只作用于引用类型之间,例如,IEnumerable<int>不能作为IEnumerable<object>使用,因为从int到object的转换是装箱转换,而不是引用转换。而且在目前的泛型语法中,只能对泛型接口和委托使用协变和逆变。此外,一个泛型参数T只能是in或者是out,你如果即想你的委托参数逆变又想返回值协变,是做不到的。
C#5.0篇
1.绑定运算符,:=:
这个只是简化了数据绑定,跟ASP.NET MVC3不断改进一样,其实不是什么亮点改进。
comboBox1.Text :=: textBox1.Text; //将文本框的内容绑定到下拉框。
2.带参数的泛型构造函数:
这个的加入给一些设计增加了强大功能,泛型早在C#2.0加入后就有着强大的应用,一般稍微设计比较好的框架,都会用到泛型,c#5.0加入带参数泛型构造函数,则在原有基础上对C#泛型完善了很多。
public class T MyClass : T: class, new() public class T MyClass : T:class, new(int)
3.支持null类型运算:
注意对于Nullable Types,在C#2.0就加入进来了,但是不支持计算,比如:
int? x = null;
int? y = x + 40;
那么y值是多少?不支持计算,得到的是null。但C#5.0可以,40加一个null的整数 y=40
int x? = null; int y? = x + 40; Myobject obj = null; Myotherobj obj2 = obj.MyProperty ??? new Myotherobj();
4.case支持表达式:
以前case里只能写一个具体的常量,而现在可以加表达式了,灵活多了。
switch(myobj){ NullOrEmpty(myotherobj): //逻辑代码 case myotherobj.Trim().Lower: //逻辑代码 }
5.扩展属性:
我们在C#3.0里有扩展方法,那么在C#5.0里将会加入扩展属性的感念,对照扩展方法,不难理解扩展属性的概念了。以下为扩展属性的定义举例:
[Associate(string)] public static int Zivsoft_ExtensionProperty { get;set;}
6. 异步编程
C#5.0一段简单的异步操作的代码例子,注意(C#5.0两个新加的关键字async, await):
Task<Movie> GetMovieAsync(string title); Task PlayMovieAsync(Movie movie); async void GetAndPlayMoviesAsync(string[] titles) { foreach (var title in titles) { var movie = await GetMovieAsync(title); await PlayMovieAsync(movie); } }
在.Net 4.5中,通过async和await两个关键字,引入了一种新的基于任务的异步编程模型(TAP)。在这种方式下,可以通过类似同步方式编写异步代码,极大简化了异步编程模型。如下式一个简单的实例:
static async void DownloadStringAsync2(Uri uri) { var webClient = new WebClient(); var result = await webClient.DownloadStringTaskAsync(uri); Console.WriteLine(result); }
而之前的方式是这样的:
static void DownloadStringAsync(Uri uri) { var webClient = new WebClient(); webClient.DownloadStringCompleted += (s, e) => { Console.WriteLine(e.Result); }; webClient.DownloadStringAsync(uri); }
也许前面这个例子不足以体现async和await带来的优越性,下面这个例子就明显多了:
public void CopyToAsyncTheHardWay(Stream source, Stream destination) { byte[] buffer = new byte[0x1000]; Action<IAsyncResult> readWriteLoop = null; readWriteLoop = iar => { for (bool isRead = (iar == null); ; isRead = !isRead) { switch (isRead) { case true: iar = source.BeginRead(buffer, 0, buffer.Length, readResult => { if (readResult.CompletedSynchronously) return; readWriteLoop(readResult); }, null); if (!iar.CompletedSynchronously) return; break; case false: int numRead = source.EndRead(iar); if (numRead == 0) { return; } iar = destination.BeginWrite(buffer, 0, numRead, writeResult => { if (writeResult.CompletedSynchronously) return; destination.EndWrite(writeResult); readWriteLoop(null); }, null); if (!iar.CompletedSynchronously) return; destination.EndWrite(iar); break; } } }; readWriteLoop(null); } public async Task CopyToAsync(Stream source, Stream destination) { byte[] buffer = new byte[0x1000]; int numRead; while ((numRead = await source.ReadAsync(buffer, 0, buffer.Length)) != 0) { await destination.WriteAsync(buffer, 0, numRead); } }
关于基于任务的异步编程模型需要介绍的地方还比较多,不是一两句能说完的,有空的话后面再专门写篇文章来详细介绍下。另外也可参看微软的官方网站:Visual Studio Asynchronous Programming,其官方文档Task-Based Asynchronous Pattern Overview介绍的非常详细, VisualStudio中自带的CSharp Language Specification中也有一些说明。
C#6.0篇
1.Auto Property Initializers:自动属性初始化
2.字符串嵌入值(String interpolation) $
3.Using Static:使用static
4.空值运算符(Null-conditional operators) ?.
5.对象初始化器(Index Initializers)
6.异常过滤器(Exception filters)
7.nameof表达式 (nameof expressions)
8.在cath和finally语句块里使用await(Await in catch and finally blocks)
9.在属性里使用Lambda表达式(Expression bodies on property-like function members)
10.在方法成员上使用Lambda表达式
1.自动属性初始化(Auto-property initializers)
public class Account { public string Name { get; set; } = "summit"; public int Age { get; set; } = 22; public IList<int> AgeList { get; set; } = new List<int> { 10,20,30,40,50 }; }
对于只读属性也可以这样直接初始化(C#5.0不支持),也可以直接在构造函数里初始化
public class Customer { public string Name { get; } public Customer(string first, string last) { Name = first + " " + last; } }
2.字符串嵌入值(String interpolation)
在之前版本的String.Format.新版本中在字符串前用$来标识后边的字符可以使用{对象}来作为占位符.看个例子.
Console.WriteLine($"年龄:{account.Age} 生日:{account.BirthDay.ToString("yyyy-MM-dd")}"); Console.WriteLine($"年龄:{account.Age}"); Console.WriteLine($"{(account.Age<=22?"小鲜肉":"老鲜肉")}");
3.导入静态类(Using Static)
像之前使用Math这个静态类的时候要先导入System命名空间后才能使用它.现在可以直接导入这个静态,然后代码中直接使用其函数.
using static System.Math;//注意这里不是命名空间哦 Console.WriteLine($"之前的使用方式: {Math.Pow(4, 2)}"); Console.WriteLine($"导入后可直接使用方法: {Pow(4,2)}");
注意这里是using static ...
如果某个命名空间下有n个方法,用这种方式直接引入单个静态类而不用引用所有方法还是挺方便的.
4.空值运算符(Null-conditional operators)
之前写过无数个这样的判断代码
if (*** != null) { //不为null的操作 } return null;
现在使用可以简化这样方式.
var age = account.AgeList?[0].ToString(); Console.WriteLine("{0}", (person.list?.Count ?? 0));
如果account.AgeList为空则整个表达式返回空,否则后边的表达式的值.
5.对象初始化器(Index Initializers)
这种方式可以给字典或其他对象通过索引赋值初始化.
IDictionary<int, string> dict = new Dictionary<int, string>() { [1]="first", [2]="second" }; foreach(var dic in dict) { Console.WriteLine($"key: {dic.Key} value:{dic.Value}"); }
output:
key: 1 value:first
key: 2 value:second
6.异常过滤器(Exception filters)
private static bool Log(Exception e) { Console.WriteLine("log"); return true; } static void TestExceptionFilter() { try { Int32.Parse("s"); } catch (Exception e) when (Log(e)) { Console.WriteLine("catch"); return; } }
当when()里面返回的值不为true,将持续抛出异常,不会执行catch里面的方法.
7.nameof表达式 (nameof expressions)
在对方法参数进行检查时以前这样写:
private static void Add(Account account) { if (account == null) throw new ArgumentNullException("account"); }
如果某天参数的名字被修改了,下面的字符串很容易漏掉忘记修改.
使用nameof表达式后,编译的时候编译器将检查到有修改后自动导航与重构
private static void Add(Account account) { if (account == null) throw new ArgumentNullException(nameof(account)); }
8.在cath和finally语句块里使用await(Await in catch and finally blocks)
c#5.0里是不支持.
Resource res = null; try { res = await Resource.OpenAsync(…); // You could do this. … } catch(ResourceException e) { await Resource.LogAsync(res, e); // Now you can do this … } finally { if (res != null) await res.CloseAsync(); // … and this. }
9.在属性里使用Lambda表达式(Expression bodies on property-like function members)
public string Name =>string.Format("姓名: {0}", "summit"); public void Print() => Console.WriteLine(Name);
10.在方法成员上使用Lambda表达式
static int LambdaFunc(int x, int y) => x*y; public void Print() => Console.WriteLine(First + " " + Last);
关于C#6.0新增加的语言特性目前为止也就这么多.没有什么新功能,更多的是语法糖的改进.开发更舒服更快速.
C#7.0篇
1.out-variables(Out变量)
2.Tuples(元组)
3.Pattern Matching(匹配模式)
4.ref locals and returns (局部变量和引用返回)
5.Local Functions (局部函数)
6.More expression-bodied members(更多的函数成员的表达式体)
7.throw Expressions (异常表达式)
8.Generalized async return types (通用异步返回类型)
9.Numeric literal syntax improvements(数值文字语法改进)
1. out-variables(Out变量)
以前,我们使用out变量的时候,需要在外部先申明,然后才能传入方法,类似如下:
public void PrintCoordinates(Point p) { int x, y; // have to "predeclare" p.GetCoordinates(out x, out y); WriteLine($"({x}, {y})"); }
在C#7.0中我们可以不必声明,直接在参数传递的同时声明它,如下:
public void PrintCoordinates(Point p) { p.GetCoordinates(out int x, out int y); WriteLine($"({x}, {y})"); }
2.Tuples(元组)
曾今在.NET4.0中,微软对多个返回值给了我们一个解决方案叫元组,类似代码如下:
static void Main(string[] args) { var data = GetFullName(); Console.WriteLine(data.Item1); Console.WriteLine(data.Item2); Console.WriteLine(data.Item3); Console.ReadLine(); } static Tuple<string, string, string> GetFullName() { return new Tuple<string, string, string>("a", "b", "c"); }
上面代码展示了一个方法,返回含有3个字符串的元组,然而当我们获取到值,使用的时候 心已经炸了,Item1,Item2,Item3是什么鬼,虽然达到了我们的要求,但是实在不优雅
支持7个参数
private static Tuple<int, int, int, int, int, int, int> Tuple4() { return new Tuple<int, int, int, int, int, int, int>(1,1,1,1,1,1,1); }
那么,在C#7.0中,微软提供了更优雅的方案:(注意:需要通过nuget引用System.ValueTuple,Framework4.7及以上)如下:
static void Main(string[] args) { var data=TestTuple(); Console.WriteLine(data.age); //可用命名获取到值 Console.WriteLine(data.name); Console.ReadLine(); } //方法定义为多个返回值,并命名 private static (int age, string name) TestTuple() { return (18, "张三"); }
解构元组,有的时候我们不想用var匿名来获取,那么如何获取abc呢?我们可以如下:
static void Main(string[] args) { //定义解构元组 (int age, string name) = TestTuple(); Console.WriteLine(age); Console.WriteLine(name); Console.ReadLine(); } private static (int age, string name) TestTuple() { return (18, "张三"); }
3.Pattern Matching(匹配模式)
在C#7.0中,引入了匹配模式的玩法,先举个老栗子.一个object类型,我们想判断他是否为int如果是int我们就加10,然后输出,需要如下:
private static void TestPattern(object obj) { if (obj is int) //is判断 { int b = (int)obj; //拆 int d = b + 10; //加10 Console.WriteLine(d); //输出 } }
那么在C#7.0中,首先就是对is的一个小扩展,我们只需要这样写就行了,如下:
private static void TestPattern(object obj) { if (obj is int b) //is判断 { int d = b + 10; //加10 Console.WriteLine(d); //输出 } }
如果有多种类型需要匹配,那怎么办?多个if else?当然没问题,不过,微软也提供了switch的新玩法,我们来看看,如下:
我们定义一个Add的方法,以Object作为参数,返回动态类型
static dynamic Add(object a) { dynamic data; switch (a) { case int b: data=b++; break; case string c: data= c + "aaa"; break; default: data = null; break; } return data; }
下面运行,传入int类型:
object a = 1; var data= Add(a); Console.WriteLine(data.GetType()); Console.WriteLine(data);
输出如图:
我们传入String类型的参数,代码和输出如下:
object a = "bbbb"; var data= Add(a); Console.WriteLine(data.GetType()); Console.WriteLine(data);

通过如上代码,我们就可以体会到switch的新玩法是多么的顺畅和强大了.
匹配模式的Case When筛选
有的同学就要问了.既然我们可以在Switch里面匹配类型了,那我们能不能顺便筛选一下值?答案当然是肯定的.
我们把上面的Switch代码改一下,如下:
switch (a) { case int b when b < 0: data = b + 100; break; case int b: data=b++; break; case string c: data= c + "aaa"; break; default: data = null; break; }
在传入-1试试,看结果如下:

4.ref locals and returns(局部变量和引用返回)
首先我们知道 ref关键字是将值传递变为引用传递
那么我们先来看看ref locals(ref局部变量)
列子代码如下:
static void Main(string[] args) { int x = 3; ref int x1 = ref x; //注意这里,我们通过ref关键字 把x赋给了x1 x1 = 2; Console.WriteLine($"改变后的变量 {nameof(x)} 值为: {x}"); Console.ReadLine(); }
这段代码最终输出 "2"
大家注意注释的部分,我们通过ref关键字把x赋给了x1,如果是值类型的传递,那么对x将毫无影响 还是输出3.
好处不言而喻,在某些特定的场合,我们可以直接用ref来引用传递,减少了值传递所需要开辟的空间.
接下来我们看看ref returns (ref引用返回)
这个功能其实是非常有用的,我们可以把值类型当作引用类型来进行return
老规矩,我们举个栗子,代码如下:
很简单的逻辑..获取指定数组的指定下标的值
static ref int GetByIndex(int[] arr, int ix) => ref arr[ix]; //获取指定数组的指定下标
我们编写测试代码如下:
int[] arr = { 1, 2, 3, 4, 5 }; ref int x = ref GetByIndex(arr, 2); //调用刚才的方法 x = 99; Console.WriteLine($"数组arr[2]的值为: {arr[2]}"); Console.ReadLine();
我们通过ref返回引用类型,在重新赋值, arr数组中的值,相应也改变了.
总结一下:ref关键字很早就存在了,但是他只能用于参数,这次C#7.0让他不仅仅只能作为参数传递,还能作为本地变量和返回值了
5.Local Functions (局部函数)
大家都知道,局部变量是指:只在特定过程或函数中可以访问的变量。
那这个局部函数,顾名思义:只在特定的函数中可以访问的函数
使用方法如下:
public static void DoSomeing() { //调用Dosmeing2 int data = Dosmeing2(100, 200); Console.WriteLine(data); //定义局部函数,Dosmeing2. int Dosmeing2(int a, int b) { return a + b; } }
在DoSomeing中定义了一个DoSomeing2的方法,..在前面调用了一下.(注:值得一提的是局部函数定义在方法的任何位置,都可以在方法内被调用,不用遵循逐行解析的方式)
6.More expression-bodied members(更多的函数成员的表达式体)
C#6.0中,提供了对于只有一条语句的方法体可以简写成表达式。
如下:
public void CreateCaCheContext() => new CaCheContext(); //等价于下面的代码 public void CreateCaCheContext() { new CaCheContext(); }
但是,并不支持用于构造函数,析构函数,和属性访问器,那么C#7.0就支持了..代码如下:
// 构造函数的表达式写法 public CaCheContext(string label) => this.Label = label; // 析构函数的表达式写法 ~CaCheContext() => Console.Error.WriteLine("Finalized!"); private string label; // Get/Set属性访问器的表达式写法 public string Label { get => label; set => this.label = value ?? "Default label"; }
7.throw Expressions (异常表达式)
在C#7.0以前,我们想判断一个字符串是否为null,如果为null则抛除异常,我们需要这么写:
public string IsNull() { string a = null; if (a == null) { throw new Exception("异常了!"); } return a; }
这样,我们就很不方便,特别是在三元表达式 或者非空表达式中,都无法抛除这个异常,需要写if语句.
那么我们在C#7.0中,可以这样:
public string IsNull() { string a = null; return a ?? throw new Exception("异常了!"); }
8.Generalized async return types (通用异步返回类型)
异步方法必须返回 void,Task 或 Task<T>,这次加入了新的ValueTask<T>,来防止异步运行的结果在等待时已可用的情境下,对 Task<T> 进行分配。对于许多示例中设计缓冲的异步场景,这可以大大减少分配的数量并显著地提升性能。
官方的实例展示的主要是意思是:一个数据,在已经缓存的情况下,可以使用ValueTask来返回异步或者同步2种方案
public class CaCheContext { public ValueTask<int> CachedFunc() { return (cache) ? new ValueTask<int>(cacheResult) : new ValueTask<int>(loadCache()); } private bool cache = false; private int cacheResult; private async Task<int> loadCache() { // simulate async work: await Task.Delay(5000); cache = true; cacheResult = 100; return cacheResult; } }
调用的代码和结果如下:
//main方法可不能用async修饰,所以用了委托. static void Main(string[] args) { Action act = async () => { CaCheContext cc = new CaCheContext(); int data = await cc.CachedFunc(); Console.WriteLine(data); int data2 = await cc.CachedFunc(); Console.WriteLine(data2); }; // 调用委托 act(); Console.Read(); }
上面的代码,我们连续调用了2次,第一次,等待了5秒出现结果.第二次则没有等待直接出现结果和预期的效果一致.
9.Numeric literal syntax improvements(数值文字语法改进)
在C#7.0中,允许数字中出现"_"这个分割符号.来提高可读性,举例如下:
int a = 123_456; int b = 0xAB_CD_EF; int c = 123456; int d = 0xABCDEF; Console.WriteLine(a==c); Console.WriteLine(b==d); //如上代码会显示两个true,在数字中用"_"分隔符不会影响结果,只是为了提高可读性
当然,既然是数字类型的分隔符,那么 decimal、float、double都是可以这样被分割的..
C#8.0篇
对于C# 8,有吸引了大多数注意力的重大特性,如默认接口方法和可空引用,也有许多小特性被考虑在内。本文将介绍几例可能加入C#未来版本的小特性。
新的赋值运算符:&&=和||=
从第一个版本开始,C#就支持把等号和另外一种运算符合在一起的语法。这几乎包括除短路布尔运算符&&和||之外的所有的二元操作符(如+、-、&等)。“赋值运算符&&=和||=”提案是对这个列表的补充。
逐字内插字符串
逐字字符串以@"开头。内插字符串使用$"。但是,如果你想要一个既逐字又内插的字符串呢?是用@$"还是$@"?目前,其中一个有效,另一个会报编译器错误,但是,人们通常很难记住哪个是哪个。
在一个名为“逐字插入字符串”的中肯提案中,该语法将得到扩展,接受@$"。对它的解释将和逐字插入字符串($@")完全相同,因此,你再也不用担心弄反了。
关于这项修改,存在一些小争议,因为有人认为这没有必要或者会导致不一致。
允许using语句结构匹配IDisposable
接口与C#编译器之间存在着奇怪的关系。经常,你不必为了使用一个语言特性而真正实现一个具体的抽象接口;你只需在类似抽象接口的类上创建一个公开的API。
典型的例子是“foreach”和IEnumerable。如果类有一个方法GetEnumerator,返回一个包含Current属性和MoveNext方法的对象,那么你就可以使用foreach。实际的返回类型不重要,这使得List<T>这样的类似可以实现更快速的枚举器。这通常称为“结构匹配(structural matching)”。
在这个提案中,using语句也将支持结构匹配。这乍看之下没什么用,因为你不会希望看到一个可释放类没有实现IDisposable。不过,有一种名为ref struct的新类型。它们不能实现接口,因此,如果没有这项提案,你在使用它们时就无法使用using语句。
和foreach与using共用的扩展方法
这个提案是前述提案的补充,是为了能够把GetEnumerator或Dispose作为扩展方法添加,并且可以分别和“foreach”或“using”共用。再次,我们谈论的这项特性仅用于非常特殊的情况。例如,你可以从第三方库(这可能是调用Marshal.ReleaseComObject的好地方)向一个COM对象添加一个Dispose扩展方法。
这还不是一个正式的提案,可能会合并到前面提到的“using”语句修改中。
“using”语句隐式作用域
目前,“using”语句后面只能是一个显式作用域(即一对花括号)或者另一个“using”语句。如果这个提案通过,你就可以编写下面这样的代码:
using var a = new MyDisposable(); using var b = new MyDisposable(); using var c = new MyDisposable();
上述每个变量都会在当前作用域结束时以相反的顺序自动释放。从功能上说,这和下面的代码等价,但更简洁,也不会引入新作用域。
using (var a = new MyDisposable()) using (var b = new MyDisposable()) using (var c = new MyDisposable()) { // 一些代码 }
当需要多个可释放对象而又不同时创建时,这项修改会特别有用。你甚至可以在表达式中间创建可释放对象,而且可以确信,它将在当前作用域结束时释放。
var results = myExcelReader.ReadResults(using new MyExcelSheet(excelFilePath));
对于这项提案,其中一个批评是,它与某些语句如“goto”不兼容。

浙公网安备 33010602011771号