C#的变迁史 - C# 1.0篇

      C#与.NET平台诞生已有10数年了,在每次重大的版本升级中,微软都为这门年轻的语言添加了许多实用的特性,下面我们就来看看每个版本都有些什么。老实说,分清这些并没什么太大的实际意义,但是很多老资格的.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的各项功能,也就没有各位大佬们面试时拍人的武器了。

posted @ 2014-03-13 13:40  沙场秋点兵  阅读(1039)  评论(0编辑  收藏  举报