(1)泛型。CLR和IL都支持泛型(Generics)。泛型类和泛型方法同时具备可重用性、类型安全和效率,这是非泛型类和非泛型方法不具备的。泛型主要用在非类型特定的通用操作上,例如集合、栈、队列等。使用泛型能够显著提高性能并得到更高质量的代码,因为可以重用数据处理算法,而无须复制类型特定的代码。泛型类型还可以使用where子句将参数强制转换为特定的类型。在概念上,泛型类似于 C++ 模板。
下面是一个简单的泛型用法示例。
public class GenericClass<T>
{
T _data = default(T);//初始化_data,如果T是值类型,_data初始化为0,否则初始化
为null
string _name = "";
public GenericClass(T data)
{
_data = data;
}
public GenericClass()
: this(default(T))
{
}
public T Data
{
get { return _data; }
}
public string Name
{
get { return _name; }
set { _name = value; }
}
}
(2)迭代器。迭代器(使用yield子句)规定了 foreach 循环将如何循环访问集合的内容。为了确保和现存程序的兼容性,yield并不是一个保留字,并且 yield只有在紧邻return或break关键词之前才具有特别的意义,而在其他上下文中,它可以被用做标识符。不过,yield语句所能出现的地方有几个限制,如下所述。
① yield语句不能出现在方法体、运算符体和访问器体之外。
② yield语句不能出现在匿名方法之内。
③ yield语句不能出现在try语句的finally语句中。
④ yield return语句不能出现在包含catch子语句的任何try语句中任何位置。
下面是一个简单的迭代器用法示例。
public class IteratorDemo<T>
{
T[] _data = default(T[]);
public IteratorDemo(T[] data)
{
_data = data;
}
public IEnumerator<T> GetEnumerator()
{
for (int i = _data.Length; --i >= 0; ) //从数组尾到数组头遍历元素
{
yield return _data[i];
}
}
}
下面给出一个简单的测试示例,代码如下:
void Test()
{
IteratorDemo<int> iter = new IteratorDemo<int>(new int[] { 1, 3, 2, 6, 7,
9, 34, 2 });
string s = "";
foreach (int p in iter)
{
s += p.ToString() + ";";
}
}
(3)分部类型。分部类型(partial)定义允许将单个类型(比如某个类)拆分为多个文件。Visual Studio 设计器使用此功能将它生成的代码与用户代码分离。C#编译器在编译的时候会将各个部分的分部类型合并成一个完整的类。对编写程序而言,分部类型不存在特别的差异,只需将各部分当成统一的整体对待即可。分部类型只适用于类、结构或接口,不支持委托或枚举。同一个类型的各个部分必须都有修饰符partial。使用分部类型时,一个类型的各个部分必须位于相同的名称空间中。一个类型的各个部分必须被同时编译。换言之,C#不支持先编译一个类型的某些部分,然后再编译一个类型的其他部分。
例如,有如下使用分部类型的代码:
[Attr1, Attr2("breeze")]
partial class C : IA, IB { }
[Attr3]
partial class C : IC { }
partial class C : IA, IB { }
与下面的代码是完全等价的:
[Attr1, Attr2("breeze"), Attr3]
class C : IA, IB, IC { }
(4)可空类型。可空类型(nullable,采用?来表示)允许变量包含未定义的值。在使用数据库和其他可能包含未含有具体值的元素的数据结构时,可以使用可空类型。可空类型的变量只能是值类型的。实际上,所有的可空类型均是System.Nullable<T>的一个类型特定的版本。打开对象浏览器就能清楚地看到它的定义:
public struct Nullable<T> where T : struct
在Nullable<T>中定义了T与T?之间的隐式转换,例如有如下代码:
int? i = null;
int j = 9;
i = 9; //等价于i = (int?)9;
j = i; //编译时错误
j = (int)i; //正确
i = null;
j = (int)i; //i为null,抛出InvalidOperationException异常
int k = i ?? j; //可空属性移除,等价于int k = i == null ? j : (int)i;
可空类型之间的运算和比较稍微复杂,可以总结成一点,即只要有可空类型参与逻辑比较运算,那么逻辑比较的结果都是false。鉴于篇幅不在此赘述,读者可查阅相关文档。
(5)匿名方法。匿名方法(Anonymous Method)实际上是内联的委托,而委托是一种指向函数签名的指针。匿名方法是非常有用和强大的,它能减少由于实例化委托和减少分离方法所导致的代码开销。例如它匿名方法可以消除为一段使用不是非常频繁的代码建立函数时对函数命名的烦恼,同时也消除了使用小型函数产生的混乱,使得程序更易于理解和维护。匿名方法可以按如下方式使用:
delegate anonymous-method-signature block
其中的匿名方法签名是可选的,如下示例演示了匿名方法的使用:
delegate void AnonymousMethod1(int x);
AnonymousMethod1 amOne1 = delegate { }; // 正确
AnonymousMethod1 amOne2 = delegate() { }; // 错误,签名不匹配
AnonymousMethod1 amOne3 = delegate(long x) { }; // 错误,签名不匹配
AnonymousMethod1 amOne4 = delegate(int x) { }; // 正确
AnonymousMethod1 amOne5 = delegate(int x) { return; }; // 正确
AnonymousMethod1 amOne6 = delegate(int x) { return x; };
// 错误,返回值不匹配
delegate void AnonymousMethod2(out int x);
AnonymousMethod2 amTwo1=delegate{ };// 错误,AnonymousMethod2带有一个输出参数
AnonymousMethod2 amTwo2 = delegate(out int x) { x = 1; }; // 正确
AnonymousMethod2 amTwo3 = delegate(ref int x) { x = 1; }; // 错误,签名不匹配
delegate int AnonymousMethod3(params int[] a);
AnonymousMethod3 amThree1 = delegate { }; // 错误,无返回值
AnonymousMethod3 amThree2 = delegate { return; }; // 错误,返回值类型不匹配
AnonymousMethod3 amThree3 = delegate { return 1; }; // 正确
AnonymousMethod3 amThree4 = delegate { return "Hello"; };
// 错误,返回值类型不匹配
AnonymousMethod3 amThree5 = delegate(int[] a) // 正确
{
return a[0];
};
AnonymousMethod3 amThree6 = delegate(params int[] a)
//错误,多余的params修饰符
{
return a[0];
};
(6)命名空间别名限定符。命名空间别名限定符(∷)对访问命名空间成员提供了更多控制。global :: 别名将从全局命名空间中搜索类型,因而允许访问可能被代码中的实体隐藏的根命名空间,以避免不同命名空间中的类型名称冲突问题。
using Space = System.Windows;
Space::Forms.Form form = new Space::Forms.Form();
(7)静态类。静态类(static class)只用于包含静态成员的类型,它既不能实例化,也不能被继承。它相当于一个sealed abstract类。若要声明那些包含不能实例化的静态方法的类,静态类就是一种安全而便利的方式。使用静态类时有如下限制。
① 静态类不能有任何实例成员。
② 静态类不能使用abstract或sealed修饰符。
③ 静态类默认继承自System.Object,不能显式指定任何其他基类。
④ 静态类不能指定任何接口实现。
⑤ 静态类的成员不能有protected或protected internal访问保护修饰符。
static class MyMath
{
public const float PI = 3.14f;
public static float Cubic(float radius)
{
return 4.0f / 3.0f * PI * radius * radius * radius; //计算球体的体积
}
}
(8)外部程序集别名。通过extern关键字的这种扩展用法引用包含在同一程序集中的同一组件的不同版本。有时可能有必要引用具有相同完全限定类型名的程序集的两个或更多个版本。通过使用外部程序集别名,来自每个程序集的命名空间可以在由别名命名的根级别命名空间内包装,从而可在同一文件中使用。
例如,有两个不同版本的程序集,分别为commonctls1.dll和commonctls2.dll。两个程序集中均实现了一个Windows窗体控件,其完全限定类型名均为Controls.ColorPicker。如果想同时使用这两个版本的控件,就得使用程序集别名。首先,必须修改引用该控件的代码文件,增加程序集别名的指定(必须放在所有代码之前):
extern alias Common1;
extern alias Common2;
using System;
M
Common1:: Controls.ColorPicker cp1;//使用正确版本的控件
Common2:: Controls.ColorPicker cp2;
M
其次,必须使用C#命令行编译器为这两个程序集创建别名,引入两个程序集别名,进而可以避免命名的二义性:
csc /t:exe /r:Common1=commonctls1.dll /r: Common2=commonctls2.dll *.cs
(9)属性访问器可访问性。在默认情况下,get/set访问器具有相同的可见性和访问级别。现在可以为属性的 get 和 set 访问器定义不同级别的可访问性。通常在保持get访问器可公开访问的情况下限制set访问器的可访问性。
//属性
string _code = "";
public string Code
{
get{return _code;}
protected set{_code = value;}
}
//索引器
string[] astr = new string[]{"A", "B", "C"};
public string this[int index]
{
get{return astr[index];}
protected set{astr[index] = value;}
}
在使用过程中必须注意以下几点。
① 定义接口时不能使用访问修饰符;在接口中定义的访问器实现时也不能使用访问修饰符;在接口中未定义的访问器实现时可以使用访问修饰符。
② 仅当同时具有get/set时,才能对其中的一个访问器使用访问修饰符。
③ 访问器的访问级别必须比所属索引器或属性的级别更严格。
④ 重写基类虚属性或索引器时,如果基访问器具有访问修饰符,则必须与访问修饰符 匹配。
(10)委托中的协变(Covariance)和逆变(Contravariance)。将委托方法与委托签名匹配时,协变和逆变提供了一定程度的灵活性。协变允许将带有派生返回类型的方法用做委托,逆变允许将带有派生参数的方法用做委托,这使得委托方法的创建变得更为灵活,并能够处理多个特定的类或事件。
协变:当委托方法的返回类型具有的派生程度比委托签名更大时,就称为协变委托方法。因为方法的返回类型比委托签名的返回类型更具体,所以可对其进行隐式转换,这样该方法就可用做委托。协变使得创建可被类和派生类同时使用的委托方法成为可能。
逆变:当委托方法签名具有一个或多个参数,并且这些参数的类型派生自方法参数的类型时,就称为逆变委托方法。因为委托方法签名参数比方法参数更具体,因此可以在将其传递给处理程序方法时对其进行隐式转换。逆变使得可由大量类使用的更通用的委托方法的创建变得更加简单。
下面为演示协变和逆变的简单示例:
class Vehicle
{
}
class Plane : Vehicle
{
}
class Factory
{
public delegate Vehicle MakeVehicle();
public delegate void FuelUpVehicle(Plane plane);
public static Vehicle MakeComVehicle()
{
return null;
}
public static Plane MakePlane()
{
return null;
}
public static void FuelUp(Vehicle vehicle)
{
}
public static void FuelUpPlane(Plane plane)
{
}
static void Sample()
{
MakeVehicle mv1 = MakeComVehicle;
MakeVehicle mv2 = MakePlane; // 协变
FuelUpVehicle fv1 = FuelUpPlane;
FuelUpVehicle fv2 = FuelUp; // 逆变
}
}
(11)固定大小的缓冲区。固定大小缓冲区的支持使C#更像C++了。在以前版本的C# 中,声明C++样式的固定大小结构是很困难的,因为包含数组的 C# 结构不包含数组元素,只包含对元素的引用。使用fixed语句进行固定大小缓冲区的声明,例如声明一个1000个整型元素的数组:
private fixed int number[1000] ;
(12)友元程序集。通过友元程序集可以从一个程序集访问另一个程序集中的内部类型或内部成员。若要使一个程序集(程序集 B)能够访问另一个程序集(程序集 A)的内部类型和成员,则应使用程序集 A 中的 InternalsVisibleToAttribute 属性。必须注意友元程序集可用于访问内部(internal)成员,而私有类型和私有成员仍然不可访问。
[assembly: InternalsVisibleTo("OtherAssembly")]
在对将要访问另一个程序集(程序集 A)的内部类型或内部成员的程序集(程序集 B)进行编译时,必须用 /out 编译器选项显式指定输出文件的名称(.exe 或 .dll)。这是必需的,因为当编译器将生成的程序集绑定到外部引用时,尚未为该程序集生成名称。
(13)内联警告控制。这是编译器提供的增强功能。#pragma警告指令可用于禁用和启用某些编译器警告。以下是使用pragma关键字的例子,可使编译器针对特定代码块禁用错误 报告。
using System;
class Program
{
[Obsolete("此方法已经不再使用,请使用FooEx")]
static void Foo() { }
static void Main()
{
#pragma warning disable 612
Foo();
#pragma warning restore 612
}
}
(14)volatile。在.NET 2.0中可以将 volatile 关键字应用于 IntPtr 和 UIntPtr。volatile 关键字表示字段可能被多个并发执行的线程修改。声明为 volatile 的字段不受编译器优化(假定由单个线程访问)的限制,这样可以确保该字段在任何时间呈现的都是最新的值。换句话说就是一个定义为volatile的变量可能会被意想不到地改变。这样,编译器就不会去假设这个变量的值了。精确地说就是,优化器在使用这个变量时必须每次都小心地重新读取这个变量的值,而不是使用保存在寄存器里的备份。
volatile修饰符通常用于由多个线程访问而不使用lock语句对访问进行序列化的字段。在使用中必须注意所涉及的类型必须是类或结构的字段,而且不能将局部变量声明为volatile。例如下面的函数可能不会返回正确的值:
volatile unsafe double* x;
unsafe double Square()
{
return *x * *x;
}
这段代码用来返回指针*x指向值的平方。但是,由于*x指向一个volatile型参数,编译器将产生类似下面的代码:
unsafe double Square()
{
double a = *x;
double b = *x;
return a * b;
}
由于*x的值可能被意想不到地改变,因此a和b可能是不同的。结果,这段代码返回的可能不是所期望的平方值。正确的代码如下:
unsafe double Square()
{
double a = *x;
return a * a;
}