在此重申一下,本文仅代表个人观点,如有不妥之处,还请自己辨别。

  第一代的值类型装箱与拆箱的效率极其低下,特别是在集合中的表现,所以第二代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();
ages.Add();
ages.Remove();

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 = ; i < ; 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 = ; i < ; 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 = ; i < ; 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-;i>=;--i)
{
yield return items[i];
}
}
}

  使用yield return创建一个Enumerator对象是不是很方便?编译器遇到yield return会创建一个Enumerator对象并自动维护这个对象。

  当然了,多数时候foreach必要遍历集合中的每一个元素,这个时候使用yield return配合for循环枚举每个元素就可以了,但是有时候只需要返回满足条件的部分元素,这个时候就要结合yield break中断枚举了,看一下:

//使用yield break中断迭代:
for(int i=count-;i>=;--i)
{
yield return items[i]; if(items[i]>)
{
yield break;
}
}

4. 可空类型

  这个特性我觉得又是把值类型设计成引用类型Object类子类后,微软生产的怪语法。空值是引用类型的默认值,0值是值类型的默认值。那么在某些场合,比如从数据库中的记录取到内存中以后,没有值代表的是空值,但是字段的类型却是值类型,怎么搞呢?于是整出了可空类型。当然了,这个问题可以通过在设计表的时候给字段设计一个默认值来解决,但是有的时候某些字段的设置默认值是没有意义的,比如年龄,0有意义吗?

  可空类型的概念很简单,没什么可说的,不过一个相似的语法却让我感到很舒服:那就是"??"操作符。这是一个二元操作符,如果第一个操作数是空值,则执行第二个操作数代表的操作,并返回其结果。例如:

static void Main(string[] args)
{
int? age = null;
age = age ?? ;
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 = ;
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#的变迁史 - C# 2.0篇的更多相关文章

  1. C#的变迁史 - C# 4.0篇

    C# 4.0 (.NET 4.0, VS2010) 第四代C#借鉴了动态语言的特性,搞出了动态语言运行时,真的是全面向“高大上”靠齐啊. 1. DLR动态语言运行时 C#作为静态语言,它需要编译以后运 ...

  2. C#的变迁史 - C# 3.0篇

    C# 3.0 (.NET 3.5, VS2008) 第三代C#在语法元素基本完备的基础上提供了全新的开发工具和集合数据查询方式,极大的方便了开发. 1. WPF,WCF,WF 这3个工程类型奠定了新一 ...

  3. C#的变迁史 - C# 1.0篇

    C#与.NET平台诞生已有10数年了,在每次重大的版本升级中,微软都为这门年轻的语言添加了许多实用的特性,下面我们就来看看每个版本都有些什么.老实说,分清这些并没什么太大的实际意义,但是很多老资格的. ...

  4. C#的变迁史 - C# 4.0 之多线程篇

    在.NET 4.0中,并行计算与多线程得到了一定程度的加强,这主要体现在并行对象Parallel,多线程Task,与PLinq.这里对这些相关的特性一起总结一下. 使用Thread方式的线程无疑是比较 ...

  5. C#的变迁史 - C# 4.0 之线程安全集合篇

    作为多线程和并行计算不得不考虑的问题就是临界资源的访问问题,解决临界资源的访问通常是加锁或者是使用信号量,这个大家应该很熟悉了. 而集合作为一种重要的临界资源,通用性更广,为了让大家更安全的使用它们, ...

  6. C#的变迁史 - C# 5.0 之调用信息增强篇

    Caller Information CallerInformation是一个简单的新特性,包括三个新引入的Attribute,使用它们可以用来获取方法调用者的信息, 这三个Attribute在Sys ...

  7. C#的变迁史 - C# 5.0 之并行编程总结篇

    C# 5.0 搭载于.NET 4.5和VS2012之上. 同步操作既简单又方便,我们平时都用它.但是对于某些情况,使用同步代码会严重影响程序的可响应性,通常来说就是影响程序性能.这些情况下,我们通常是 ...

  8. C#的变迁史 - C# 4.0 之并行处理篇

    前面看完了Task对象,这里再看一下另一个息息相关的对象Parallel. Parallel对象 Parallel对象封装了能够利用多核并行执行的多线程操作,其内部使用Task来分装多线程的任务并试图 ...

  9. C#的变迁史 - C# 5.0 之其他增强篇

    1. 内置zip压缩与解压 Zip是最为常用的文件压缩格式之一,也被几乎所有操作系统支持.在之前,使用程序去进行zip压缩和解压要靠第三方组件去支持,这一点在.NET4.5中已有所改观,Zip压缩和解 ...

随机推荐

  1. [.NET自我学习]Delegate 泛型

    阅读导航 委托Delegate 泛型 1. 委托Delegate 继承自MulticastDelegate 声明委托定义签名: public delegate int DemoDelegate(int ...

  2. [nRF51822] 1、一个简单的nRF51822驱动的天马4线SPI-1.77寸LCD彩屏DEMO

    最近用nRF51822写了个天马4线SPI的1.77寸LCD彩屏驱动,效果如下: 屏幕的规格资料为:http://pan.baidu.com/s/1gdfkr5L 屏幕的驱动资料为:http://pa ...

  3. EF架构~CodeFirst自关联表的插入

    回到目录 这个文章对之前EF的一个补充,对于一些自关联表的添加,如果你建立了表约束确实有这种问题,一般主键为整形自增,父ID为可空,这时,在添加时如果不为ID赋值,结果就会出错. 错误: 无法确定依赖 ...

  4. 大叔最新课程~EF核心技术剖析

    EF核心技术剖析介绍 数据上下文(共享对象与实例对象的选择) 自动初始化(Initializer初始化的几种方式) 数据迁移(Migrations如何使用及其重要作用) 实体关系映射(一对一,一对多, ...

  5. 在ubuntu上安装nodejs[开启实时web时代]

    作为一名菜鸟,竟然在centos桌面上连输入命令行的地方都找不到,是在是对不起开山祖师,最后苍天不负苦心人,在ubuntu上找见了 [安装过程参考了http://cnodejs.org/topic/4 ...

  6. Fiddler (六) 最常用的快捷键

    使用QuickExec Fiddler2成了网页调试必备的工具,抓包看数据.Fiddler2自带命令行控制,并提供以下用法. Fiddler的快捷命令框让你快速的输入脚本命令. 键盘快捷键 按ALT+ ...

  7. iOS ---Extension编程指南

    当iOS 8.0和OS X v10.10发布后,一个全新的概念出现在我们眼前,那就是应用扩展.顾名思义,应用扩展允许开发者扩展应用的自定义功能和内容,能够让用户在使用其他app时使用该项功能.你可以开 ...

  8. javascript_basic_02之数据类型、分支结构

    1.弱类型:声明无需指定数据类型,由值决定,查看变量数据类型:typeof(变量): 2.隐式转换:任何数据类型与string类型相加,结果为string类型: 3.显式(强制)转换: ①toStri ...

  9. How Google TestsSoftware - Part Three

    Lots of questions in thecomments to the last two posts. I am not ignoring them. Hopefully many of th ...

  10. 《C与指针》第八章练习

    本章问题 1.根据下面给出的声明和数据,对每个表达式进行求值并写出它的值.在对每个表达式进行求值时使用原先给出的值(也就是说,某个表达式的结果不影响后面的表达式).假定ints数组在内存中的起始位置是 ...