首页 > 代码库 > C# 2.0 新特性(上)
C# 2.0 新特性(上)
C# 2.0新特性
1. 泛型
1.1 泛型介绍
泛型类和泛型方法同时具备可重用性、类型安全和效率,这是非泛型类和非泛型方法无法具备的。泛型通常用在集合和在集合上运行的方法中。.NET Framework 2.0 版类库提供一个新的命名空间 System.Collections.Generic,其中包含几个新的基于泛型的集合类。建议面向 2.0 版的所有应用程序都使用新的泛型集合类,而不要使用旧的非泛型集合类,如 ArrayList。有关更多信息,请参见 .NET Framework 类库中的泛型(C# 编程指南)。
1.2 泛型的优点
1. 避免集合操作时的强制装箱和拆箱操作,提高性能。
2. 强类型的数据类型检查,避免数据编译是通过,但运行时才能检测到数据类型错误。
1.3 泛型类型参数
在泛型类型或方法定义中,类型参数是客户端在实例化泛型类型的变量时指定的特定类型的占位符。泛型类(如泛型介绍(C# 编程指南)中列出的GenericList<T>)不可以像这样使用,因为它实际上并不是一个类型,而更像是一个类型的蓝图。若要使用 GenericList<T>,客户端代码必须通过指定尖括号中的类型参数来声明和实例化构造类型。此特定类的类型参数可以是编译器识别的任何类型。可以创建任意数目的构造类型实例,每个实例使用不同的类型参数,如下所示:
GenericList<float> list1 = new GenericList<float>(); GenericList<ExampleClass> list2 = new GenericList<ExampleClass>(); GenericList<ExampleStruct> list3 = new GenericList<ExampleStruct>();
在每个 GenericList<T> 实例中,类中出现的每个 T 都会在运行时替换为相应的类型参数。通过这种替换方式,我们使用一个类定义创建了三个独立的类型安全的有效对象。有关 CLR 如何执行此替换的更多信息,请参见运行库中的泛型(C# 编程指南)。
T:为值类型的时候,集合会对不同的值类型单独创建一个实例,同类型的值类型集合使用同一个实例。
T:为引用类型的时候,集合首次使用的时候创建实例,不同的引用类型使用同一个实例。
1.4 类型参数的约束
在定义泛型类时,可以对客户端代码能够在实例化类时用于类型参数的类型种类施加限制。如果客户端代码尝试使用某个约束所不允许的类型来实例化类,则会产生编译时错误。这些限制称为约束。约束是使用 where 上下文关键字指定的。下表列出了六种类型的约束:
约束 | 说明 |
---|---|
T:结构 |
类型参数必须是值类型。可以指定除 Nullable 以外的任何值类型。有关更多信息,请参见使用可空类型(C# 编程指南)。 |
T:类 |
类型参数必须是引用类型,包括任何类、接口、委托或数组类型。 |
T:new() |
类型参数必须具有无参数的公共构造函数。当与其他约束一起使用时,new() 约束必须最后指定。 |
T:<基类名> |
类型参数必须是指定的基类或派生自指定的基类。 |
T:<接口名称> |
类型参数必须是指定的接口或实现指定的接口。可以指定多个接口约束。约束接口也可以是泛型的。 |
T:U |
为 T 提供的类型参数必须是为 U 提供的参数或派生自为 U 提供的参数。这称为裸类型约束。 |
1.5 泛型类
泛型类封装不是特定于具体数据类型的操作。泛型类最常用于集合,如链接列表、哈希表、堆栈、队列、树等,其中,像从集合中添加和移除项这样的操作都以大体上相同的方式执行,与所存储数据的类型无关。
一般情况下,创建泛型类的过程为:从一个现有的具体类开始,逐一将每个类型更改为类型参数,直至达到通用化和可用性的最佳平衡。创建您自己的泛型类时,需要特别注意以下事项:
-
-
将哪些类型通用化为类型参数。
一般规则是,能够参数化的类型越多,代码就会变得越灵活,重用性就越好。但是,太多的通用化会使其他开发人员难以阅读或理解代码。
-
如果存在约束,应对类型参数应用什么约束(请参见类型参数的约束(C# 编程指南))。
一个有用的规则是,应用尽可能最多的约束,但仍使您能够处理需要处理的类型。例如,如果您知道您的泛型类仅用于引用类型,则应用类约束。这可以防止您的类被意外地用于值类型,并允许您对 T 使用 as 运算符以及检查空值。
-
是否将泛型行为分解为基类和子类。
由于泛型类可以作为基类使用,此处适用的设计注意事项与非泛型类相同。有关从泛型基类继承的规则,请参见下面的内容。
-
是否实现一个或多个泛型接口。
例如,如果您设计一个类,该类将用于创建基于泛型的集合中的项,则可能需要实现一个接口,如 IComparable<T>,其中 T 是您的类的类型。
-
1.6 泛型接口
为泛型集合类或表示集合中项的泛型类定义接口通常很有用。对于泛型类,使用泛型接口十分可取,例如使用 IComparable<T> 而不使用IComparable,这样可以避免值类型的装箱和取消装箱操作。.NET Framework 2.0 类库定义了若干新的泛型接口,以用于 System.Collections.Generic命名空间中新的集合类。
将接口指定为类型参数的约束时,只能使用实现此接口的类型。下面的代码示例显示从 GenericList<T> 类派生的 SortedList<T> 类。有关更多信息,请参见泛型介绍(C# 编程指南)。SortedList<T> 添加了约束 where T : IComparable<T>。这将使 SortedList<T> 中的 BubbleSort 方法能够对列表元素使用泛型 CompareTo 方法。
1.7 泛型方法
泛型方法也可以使用约束,泛型方法中的T避免和泛型类中的类型替代符相同。
1.8 泛型和数组
在 C# 2.0 中,下限为零的一维数组自动实现 IList<T>。这使您可以创建能够使用相同代码循环访问数组和其他集合类型的泛型方法。此技术主要对读取集合中的数据很有用。IList<T> 接口不能用于在数组中添加或移除元素;如果试图在此上下文中调用 IList<T> 方法(如数组的 RemoveAt),将引发异常。
1.9 泛型委托
委托 可以定义自己的类型参数。引用泛型委托的代码可以指定类型参数以创建已关闭的构造类型,就像实例化泛型类或调用泛型方法一样
1.10 泛型代码中的默认关键字
在泛型类和泛型方法中产生的一个问题是,在预先未知以下情况时,如何将默认值分配给参数化类型 T:
-
-
T 是引用类型还是值类型。
-
如果 T 为值类型,则它是数值还是结构。
-
给定参数化类型 T 的一个变量 t,只有当 T 为引用类型时,语句 t = null 才有效;只有当 T 为数值类型而不是结构时,语句 t = 0 才能正常使用。解决方案是使用 default 关键字,此关键字对于引用类型会返回空,对于数值类型会返回零。对于结构,此关键字将返回初始化为零或空的每个结构成员,具体取决于这些结构是值类型还是引用类型。以下来自 GenericList<T> 类的示例显示了如何使用 default 关键字。有关更多信息,请参见泛型概述。
1.11 运行库中的泛型
将泛型类型或方法编译为 Microsoft 中间语言 (MSIL) 时,它包含将其标识为具有类型参数的元数据。泛型类型的 MSIL 的使用因所提供的类型参数是值类型还是引用类型而不同。
对于值类型:第一次用值类型作为参数来构造泛型类型时,运行库会创建专用泛型类型,将提供的参数代入到 MSIL 中的适当位置。对于每个用作参数的唯一值类型,都会创建一次专用泛型类型。
对于引用类型:泛型的工作方式略有不同。第一次使用任何引用类型构造泛型类型时,运行库会创建专用泛型类型,用对象引用替换 MSIL 中的参数。然后,每次使用引用类型作为参数来实例化构造类型时,无论引用类型的具体类型是什么,运行库都会重用以前创 建的泛型类型的专用版本。之所以可以这样,是因为所有引用的大小相同。
此外,使用类型参数实例化泛型 C# 类时,无论它是值类型还是引用类型,可以在运行时使用反射查询它,并且可以确定它的实际类型和类型参数。
1.12 .NET Framework 类库中的泛型
.NET Framework 2.0 版类库提供一个新的命名空间 System.Collections.Generic,其中包括几个随时可用的泛型集合类和关联接口。其他命名空间(如System)也提供新的泛型接口,如 IComparable<T>。与早期版本的 .NET Framework 所提供的非泛型集合类相比,这些类和接口更为高效和类型安全。在设计和实现自己的自定义集合类之前,请考虑是否能够使用基类库所提供的类,或是否能从基类库所提供的类派生。
1.13 泛型和反射
因为公共语言运行库 (CLR) 能够在运行时访问泛型类型信息,所以可以使用反射获取关于泛型类型的信息,方法与用于非泛型类型的方法相同。有关更多信息,请参见运行库中的泛型(C# 编程指南)。
在 .NET Framework 2.0 中,Type 类增添了几个新成员以启用泛型类型的运行时信息。有关如何使用这些方法和属性的更多信息,请参见关于这些类的文档。System.Reflection.Emit 命名空间还包含支持泛型的新成员。请参见如何:定义具有反射发出的泛型类型。
1.14 泛型和属性
属性可以应用于泛型类型中,方式与应用于非泛型类型相同。有关应用属性的更多信息,请参见属性(C# 编程指南)。
自定义属性只允许引用开放泛型类型(未提供类型参数的泛型类型)和封闭构造泛型类型(为所有类型参数提供参数)。
2. 迭代器
迭代器是 C# 2.0 中的新功能。迭代器是方法、get 访问器或运算符,它使您能够在类或结构中支持 foreach 迭代,而不必实现整个 IEnumerable 接口。您只需提供一个迭代器,即可遍历类中的数据结构。当编译器检测到迭代器时,它将自动生成 IEnumerable 或 IEnumerable<T> 接口的Current、MoveNext 和 Dispose 方法。
2.1迭代器概述
1.迭代器是可以返回相同类型的值的有序序列的一段代码。
2.迭代器可用作方法、运算符或 get 访问器的代码体。
3.迭代器代码使用 yield return 语句依次返回每个元素。yield break 将终止迭代。有关更多信息,请参见 yield。
4.可以在类中实现多个迭代器。每个迭代器都必须像任何类成员一样有唯一的名称,并且可以在 foreach 语句中被客户端代码调用,如下所示:foreach(int x in SampleClass.Iterator2){}
5.迭代器的返回类型必须为 IEnumerable、IEnumerator、IEnumerable<T> 或 IEnumerator<T>。
yield 关键字用于指定返回的值。到达 yield return 语句时,会保存当前位置。下次调用迭代器时将从此位置重新开始执行。
迭代器对集合类特别有用,它提供一种简单的方法来迭代不常用的数据结构(如二进制树)。
2.2 使用迭代器
创建迭代器最常用的方法是对 IEnumerable 接口实现 GetEnumerator 方法。
GetEnumerator 方法的存在使得类型成为可枚举的类型,并允许使用 foreach 语句。
foreach 语句调用 ListClass.GetEnumerator() 并使用返回的枚举数来循环访问值。有关如何创建返回 IEnumerator 接口的泛型迭代器的示例,请参见如何:为泛型列表创建迭代器块(C# 编程指南)。
还可以使用命名的迭代器以支持通过不同的方式循环访问同一数据集合。例如,您可以提供一个按升序返回元素的迭代器,而提供按降序返回元素的另一个迭代器。迭代器还可以带有参数,以便允许客户端控制全部或部分迭代行为。
可以在同一个迭代器中使用多个 yield 语句。
在 foreach 循环的每次后续迭代(或对 IEnumerator.MoveNext 的直接调用)中,下一个迭代器代码体将从前一个 yield 语句之后开始,并继续下一个语句直至到达迭代器体的结尾或遇到 yield break 语句。
3.分部类
可以将类、结构或接口的定义拆分到两个或多个源文件中。每个源文件包含类定义的一部分,编译应用程序时将把所有部分组合起来。在以下几种情况下需要拆分类定义:
处理大型项目时,使一个类分布于多个独立文件中可以让多位程序员同时对该类进行处理。
使用自动生成的源时,无需重新创建源文件便可将代码添加到类中。Visual Studio 在创建 Windows 窗体、Web 服务包装代码等时都使用此方法。您无需编辑 Visual Studio 所创建的文件,便可创建使用这些类的代码。
若要拆分类定义,请使用 partial 关键字修饰符
4. 可空类型
可空类型具有以下特性:
1.可空类型表示可被赋值为 null 值的值类型变量。无法创建基于引用类型的可空类型。(引用类型已支持 null 值。)。
2.语法 T? 是 System.Nullable<T> 的简写,此处的 T 为值类型。这两种形式可以互换。
3.为可空类型赋值与为一般值类型赋值的方法相同,如 int? x = 10; 或 double? d = 4.108;。
4.如果基础类型的值为 null,请使用 System.Nullable.GetValueOrDefault 属性返回该基础类型所赋的值或默认值,例如 int j = x.GetValueOrDefault();
5.请使用 HasValue 和 Value 只读属性测试是否为空和检索值,例如 if(x.HasValue) j = x.Value;
6.使用 ?? 运算符分配默认值,当前值为空的可空类型被赋值给非空类型时将应用该默认值,如 int? x = null; int y = x ?? -1;。
7.不允许使用嵌套的可空类型。将不编译下面一行:Nullable<Nullable<int>> n;
5. 匿名方法
在 2.0 之前的 C# 版本中,声明委托的唯一方法是使用命名方法。C# 2.0 引入了匿名方法。
如果使用匿名方法,则不必创建单独的方法,因此减少了实例化委托所需的编码系统开销。
6. 命名空间别名限定符
命名空间别名限定符 (::) 用于查找标识符。它通常放置在两个标识符之间,例如:
global::System.Console.WriteLine("Hello World");
命名空间别名限定符可以是 global。这将调用全局命名空间中的查找,而不是在别名命名空间中
7. 静态类和静态类成员
静态类和类成员用于创建无需创建类的实例就能够访问的数据和函数。
静态类成员可用于分离独立于任何对象标识的数据和行为:无论对象发生什么更改,这些数据和函数都不会随之变化。当类中没有依赖对象标识的数据或行为时,就可以使用静态类。
7.1 静态类
类可以声明为 static 的,以指示它仅包含静态成员。不能使用 new 关键字创建静态类的实例。静态类在加载包含该类的程序或命名空间时由 .NET Framework 公共语言运行库 (CLR) 自动加载。
使用静态类来包含不与特定对象关联的方法。例如,创建一组不操作实例数据并且不与代码中的特定对象关联的方法是很常见的要求。您应该使用静态类来包含那些方法。
静态类的主要功能如下:
1.它们仅包含静态成员。
2.它们不能被实例化。
3.它们是密封的。
4.它们不能包含实例构造函数(C# 编程指南)。
因此创建静态类与创建仅包含静态成员和私有构造函数的类大致一样。私有构造函数阻止类被实例化。
使用静态类的优点在于,编译器能够执行检查以确保不致偶然地添加实例成员。编译器将保证不会创建此类的实利。
静态类是密封的,因此不可被继承。静态类不能包含构造函数,但仍可声明静态构造函数以分配初始值或设置某个静态状态。有关更多信息,请参见静态构造函数(C# 编程指南)。
7.2 静态成员
即使没有创建类的实例,也可以调用该类中的静态方法、字段、属性或事件。如果创建了该类的任何实例,不能使用实例来访问静态成员。只存在静态字段和事件的一个副本,静态方法和属性只能访问静态字段和静态事件。静态成员通常用于表示不会随对象状态而变化的数据或计算;例如,数学库可能包含用于计算正弦和余弦的静态方法。
在成员的返回类型之前使用 static 关键字来声明静态类成员
C# 2.0 新特性(上)