• 设为首页
  • 点击收藏
  • 手机版
    手机扫一扫访问
    迪恩网络手机版
  • 关注官方公众号
    微信扫一扫关注
    公众号

C#2.0和下一代C#语言

原作者: [db:作者] 来自: [db:来源] 收藏 邀请
第一章 C#2.0简介
C# 2.0引入了很多语言扩展,最重要的就是泛型(Generics)、匿名方法(Anonymous Methods)、迭代器
(Iterators)和不完全类型(Partial Types)。
● 泛型允许类、结构、接口、委托和方法通过它们所存贮和操作的数据的类型来参数化。泛型是很有
用的,因为它提供了更为强大的编译期间类型检查,需要更少的数据类型之间的显式转换,并且减少了
对装箱操作的需要和运行时的类型检查。
● 匿名方法允许在需要委托值时能够以“内联(in-line)”的方式书写代码块。匿名方法与Lisp语言
中的拉姆达函数(lambda functions)类似。
● 迭代器是能够增量地计算和产生一系列值得方法。迭代器使得一个类能够很容易地解释foreach语句
将如何迭代他的每一个元素。
● 不完全类型允许类、结构和接口被分成多个小块儿并存贮在不同的源文件中使其容易开发和维护。
另外,不完全类型可以分离机器产生的代码和用户书写的部分,这使得用工具来加强产生的代码变得容
易。
这一章首先对这些新特性做一个简介。简介之后有四章,提供了这些特性的完整的技术规范。
C# 2.0中的语言扩展的设计可以保证和现有代码的高度的兼容性。例如,尽管C#2.0在特定的环境中对
单词where、yield和partial赋予了特殊的意义,这些单词还是可以被用作标识符。确实,C# 2.0没有增加一
个会和现有代码中的标识符冲突的关键字。
1.1 泛型
泛型允许类、结构、接口、委托和方法通过它们所存贮和操作的数据的类型来参数化。C#泛型对使
用Eiffel或Ada语言泛型的用户和使用C++模板的用户来说相当亲切,尽管它们也许无法忍受后者的复杂
性。
1.1.1 为什么泛型?
没有泛型,一些通用的数据结构只能使用object类型来存贮各种类型的数据。例如,下面这个简单的
Stack类将它的数据存放在一个object数组中,而它的两个方法,Push和Pop,分别使用object来接受和返回
数据:
public class Stack
{
object[] items;
int count;
public void Push(object item) {...}
public object Pop() {...}
}
尽管使用object类型使得Stack类非常灵活,但它也不是没有缺点。例如,可以向堆栈中压入任何类型
的值,譬如一个Customer实例。然而,重新取回一个值得时候,必须将Pop方法返回的值显式地转换为合
适的类型,书写这些转换变更要提防运行时类型检查错误是很乏味的:
Stack stack = new Stack();
stack.Push(new Customer());
Customer c = (Customer)stack.Pop();
如果一个值类型的值,如int,传递给了Push方法,它会自动装箱。而当待会儿取回这个int值时,必
file:///E|/mybook/C#2.0/C#2.0新特性/1.htm(第 1/11 页)2005-9-21 10:24:13
第一章 C#2.0简介
须显式的类型转换进行拆箱:
Stack stack = new Stack();
stack.Push(3);
int i = (int)stack.Pop();
这种装箱和拆箱操作增加了执行的负担,因为它带来了动态内存分配和运行时类型检查。
Stack类的另外一个问题是无法强制堆栈中的数据的种类。确实,一个Customer实例可以被压入栈
中,而在取回它的时候会意外地转换成一个错误的类型:
Stack stack = new Stack();
stack.Push(new Customer());
string s = (string)stack.Pop();
尽管上面的代码是Stack类的一种不正确的用法,但这段代码从技术上来说是正确的,并且不会发生
编译期间错误。为题知道这段代码运行的时候才会出现,这时会抛出一个InvalidCastException异常。
Stack类无疑会从具有限定其元素类型的能力中获益。使用泛型,这将成为可能。
1.1.2 建立和使用泛型
泛型提供了一个技巧来建立带有类型参数(type parameters)的类型。下面的例子声明了一个带有类
型参数T的泛型Stack类。类型参数又类名字后面的定界符“<”和“>”指定。通过某种类型建立的
Stack<T>的实例 可以无欲转换地接受该种类型的数据,这强过于与object相互装换。类型参数T扮演一个
占位符的角色,直到使用时指定了一个实际的类型。注意T相当于内部数组的数据类型、Push方法接受的
参数类型和Pop方法的返回值类型:
public class Stack<T>
{
T[] items;
int count;
public void Push(T item) {...}
public T Pop() {...}
}
使用泛型类Stack<T>时,需要指定实际的类型来替代T。下面的例子中,指定int作为参数类型T:
Stack<int> stack = new Stack<int>();
stack.Push(3);
int x = stack.Pop();
Stack<int>类型称为已构造类型(constructed type)。在Stack<int>类型中出现的所有T被替换为类型参
数int。当一个Stack<int>的实例被创建时,items数组的本地存贮是int[]而不是object[],这提供了一个实质
的存贮,效率要高过非泛型的Stack。同样,Stack<int>中的Push和Pop方法只操作int值,如果向堆栈中压
入其他类型的值将会得到编译期间的错误,而且取回一个值时不必将它显示转换为原类型。
泛型可以提供强类型,这意味着例如向一个Customer对象的堆栈上压入一个int将会产生错误。这是因
为Stack<int>只能操作int值,而Stack<Customer>也只能操作Customer对象。下面例子中的最后两行会导致
编译器报错:
Stack<Customer> stack = new Stack<Customer>();
stack.Push(new Customer());
Customer c = stack.Pop();
stack.Push(3); // 类型不匹配错误
int x = stack.Pop(); // 类型不匹配错误
泛型类型的声明允许任意数目的类型参数。上面的Stack<T>例子只有一个类型参数,但一个泛型的
Dictionary类可能有两个类型参数,一个是键的类型另一个是值的类型:
public class Dictionary<K,V>
{
file:///E|/mybook/C#2.0/C#2.0新特性/1.htm(第 2/11 页)2005-9-21 10:24:13
第一章 C#2.0简介
public void Add(K key, V value) {...}
public V this[K key] {...}
}
使用Dictionary<K,V>时,需要提供两个类型参数:
Dictionary<string,Customer> dict = new Dictionary<string,Customer>();
dict.Add("Peter", new Customer());
Customer c = dict["Peter"];
1.1.3 泛型类型实例化
和非泛型类型类似,编译过的泛型类型也由中间语言(IL, Intermediate Language)指令和元数据表
示。泛型类型的IL表示当然已由类型参数进行了编码。
当程序第一次建立一个已构造的泛型类型的实例时,如Stack<int>,.NET公共语言运行时中的即时编
译器(JIT, just-in-time)将泛型IL和元数据转换为本地代码,并在进程中用实际类型代替类型参数。后面
的对这个以构造的泛型类型的引用使用相同的本地代码。从泛型类型建立一个特定的构造类型的过程称
为泛型类型实例化(generic type instantiation)。
.NET公共语言运行时为每个由之类型实例化的泛型类型建立一个专门的拷贝,而所有的引用类型共
享一个单独的拷贝(因为,在本地代码级别上,引用知识具有相同表现的指针)。
1.1.4 约束
通常,一个泛型类不会只是存贮基于某一类型参数的数据,他还会调用给定类型的对象的方法。例
如,Dictionary<K,V>中的Add方法可能需要使用CompareTo方法来比较键值:
public class Dictionary<K,V>
{
public void Add(K key, V value)
{
...
if (key.CompareTo(x) < 0) {...} // 错误,没有CompareTo方法
...
}
}
由于指定的类型参数K可以是任何类型,可以假定存在的参数key具有的成员只有来自object的成员,
如Equals、GetHashCode和ToString;因此上面的例子会发生编译错误。当然可以将参数key转换成为一具
有CompareTo方法的类型。例如,参数key可以转换为IComparable:
public class Dictionary<K,V>
{
public void Add(K key, V value)
{
...
if (((IComparable)key).CompareTo(x) < 0) {...}
...
}
}
当这种方案工作时,会在运行时引起动态类型转换,会增加开销。更要命的是,它还可能将错误报
告推迟到运行时。如果一个键没有实现IComparable接口,会抛出InvalidCastException异常。
为了提供更强大的编译期间类型检查和减少类型转换,C#允许一个可选的为每个类型参数提供的约
束(constraints)列表。一个类型参数的约束指定了一个类型必须遵守的要求,使得这个类型参数能够作
file:///E|/mybook/C#2.0/C#2.0新特性/1.htm(第 3/11 页)2005-9-21 10:24:13
第一章 C#2.0简介
为一个变量来使用。约束由关键字where来声明,后跟类型参数的名字,再后是一个类或接口类型的列
表,或构造器约束new()。
要想使Dictionary<K,V>类能保证键值始终实现了IComparable接口,类的声明中应该对类型参数K指定
一个约束:
public class Dictionary<K,V> where K: IComparable
{
public void Add(K key, V value)
{
...
if (key.CompareTo(x) < 0) {...}
...
}
}
通过这个声明,编译器能够保证所有提供给类型参数K的类型都实现了IComparable接口。进而,在调
用CompareTo方法前不再需要将键值显式转换为一个IComparable接口;一个受约束的类型参数类型的值
的所有成员都可以直接使用。
对于给定的类型参数,可以指定任意数目的接口作为约束,但只能指定一个类(作为约束)。每一
个被约束的类型参数都有一个独立的where子句。在下面的例子中,类型参数K有两个接口约束,而类型
参数E有一个类约束和一个构造器约束:
public class EntityTable<K,E>
where K: IComparable<K>, IPersistable
where E: Entity, new()
{
public void Add(K key, E entity)
{
...
if (key.CompareTo(x) < 0) {...}
...
}
}
上面例子中的构造器约束,new(),保证了作为的E类型变量的类型具有一个公共、无参的构造器,并
允许泛型类使用new E()来建立该类型的一个实例。
类型参数约束的使用要小心。尽管它们提供了更强大的编译期间类型检查并在一些情况下改进了性
能,它还是限制了泛型类型的使用。例如,一个泛型类List<T>可能约束T实现IComparable接口以便Sort方
法能够比较其中的元素。然而,这么做使List<T>不能用于那些没有实现IComparable接口的类型,尽管在
这种情况下Sort方法从来没被实际调用过。
1.1.5 泛型方法
有的时候一个类型参数并不是整个类所必需的,而只用于一个特定的方法中。通常,这种情况发生
在建立一个需要一个泛型类型作为参数的方法时。例如,在使用前面描述过的Stack<T>类时,一种公共
的模式就是在一行中压入多个值,如果写一个方法通过单独调用它类完成这一工作会很方便。对于一个
特定的构造过的类型,如Stack<int>,这个方法看起来会是这样:
void PushMultiple(Stack<int> stack, params int[] values) {
foreach (int value in values) stack.Push(value);
}
这个方法可以用于将多个int值压入一个Stack<int>:
Stack<int> stack = new Stack<int>();
file:///E|/mybook/C#2.0/C#2.0新特性/1.htm(第 4/11 页)2005-9-21 10:24:13
第一章 C#2.0简介
PushMultiple(stack, 1, 2, 3, 4);
然而,上面的方法只能工作于特定的构造过的类型Stack<int>。要想使他工作于任何Stack<T>,这个
方法必须写成泛型方法(generic method)。一个泛型方法有一个或多个类型参数,有方法名后面
的“<”和“>”限定符指定。这个类型参数可以用在参数列表、返回至和方法体中。一个泛型的
PushMultiple方法看起来会是这样:
void PushMultiple<T>(Stack<T> stack, params T[] values) {
foreach (T value in values) stack.Push(value);
}
使用这个方法,可以将多个元素压入任何Stack<T>中。当调用一个泛型方法时,要在函数的调用中将
类型参数放入尖括号中。例如:
Stack<int> stack = new Stack<int>();
PushMultiple<int>(stack, 1, 2, 3, 4);
这个泛型的PushMultiple方法比上面的版本更具可重用性,因为它能工作于任何Stack<T>,但这看起
来并不舒服,因为必须为T提供一个类型参数。然而,很多时候编译器可以通过传递给方法的其他参数来
推断出正确的类型参数,这个过程称为类型推断(type inferencing)。在上面的例子中,由于第一个正式
的参数的类型是Stack<int>,并且后面的参数类型都是int,编译器可以认定类型参数一定是int。因此,在
调用泛型的PushMultiple方法时可以不用提供类型参数:
Stack<int> stack = new Stack<int>();
PushMultiple(stack, 1, 2, 3, 4);
1.2 匿名方法
实践处理方法和其他回调方法通常需要通过专门的委托来调用,而不是直接调用。因此,迄今为止
我们还只能将一个实践处理和回调的代码放在一个具体的方法中,再为其显式地建立委托。相反,匿名
方法(anonymous methods)允许将与一个委托关联的代码“内联(in-line)”到使用委托的地方,我们可
以很方便地将代码直接写在委托实例中。除了看起来舒服,匿名方法还共享对本地语句所包含的函数成
员的访问。如果想在命名方法(区别于匿名方法)中达成这种共享,需要手动创建一个辅助类并将本地
成员“提升(lifting)”到这个类的域中。
下面的例子展示了从一个包含一个列表框、一个文本框和一个按钮的窗体中获取一个简单的输入。
当按钮按下时文本框中的文本会被添加到列表框中。
class InputForm: Form
{
ListBox listBox;
TextBox textBox;
Button addButton;
public MyForm() {
listBox = new ListBox(...);
textBox = new TextBox(...);
addButton = new Button(...);
addButton.Click += new EventHandler(AddClick);
}
void AddClick(object sender, EventArgs e) {
listBox.Items.Add(textBox.Text);
}
}
尽管对按钮的Click事件的响应只有一条语句,这条语句也必须放到一个独立的具有完整的参数列表
的方法中,并且要手动创建引用该方法的EventHandler委托。使用匿名方法,事件处理的代码会变得更加
file:///E|/mybook/C#2.0/C#2.0新特性/1.htm(第 5/11 页)2005-9-21 10:24:13
第一章 C#2.0简介
简洁:
class InputForm: Form
{
ListBox listBox;
TextBox textBox;
Button addButton;
public MyForm() {
listBox = new ListBox(...);
textBox = new TextBox(...);
addButton = new Button(...);
addButton.Click += delegate {
listBox.Items.Add(textBox.Text);
};
}
}
一个匿名方法由关键字delegate和一个可选的参数列表组成,并将语句放入“{”和“}”限定符中。前
面例子中的匿名方法没有使用提供给委托的参数,因此可以省略参数列表。要想访问参数,你名方法应
该包含一个参数列表:
addButton.Click += delegate(object sender, EventArgs e) {
MessageBox.Show(((Button)sender).Text);
};
上面的例子中,在匿名方法和EventHandler委托类型(Click事件的类型)之间发生了一个隐式的转
换。这个隐式的转换是可行的,因为这个委托的参数列表和返回值类型和匿名方法是兼容的。精确的兼
容规则如下:
? 当下面条例中有一条为真时,则委托的参数列表和匿名方法是兼容的:
o 匿名方法没有参数列表且委托没有输出(out)参数。
o 匿名方法的参数列表在参数数目、类型和修饰符上与委托参数精确匹配。
? 当下面的条例中有一条为真时,委托的返回值与匿名方法兼容:
o 委托的返回值类型是void且匿名方法没有return语句或其return语句不带任何表达式。
o 委托的返回值类型不是void但和匿名方法的return语句关联的表达式的值可以被显式地转换为委
托的返回值类型。
只有参数列表和返回值类型都兼容的时候,才会发生匿名类型向委托类型的隐式转换。
下面的例子使用了匿名方法对函数进行了“内联(in-lian)”。匿名方法被作为一个Function委托类
型传递。
using System;
delegate double Function(double x);
class Test
{
static double[] Apply(double[] a, Function f) {
double[] result = new double[a.Length];
for (int i = 0; i < a.Length; i++) result[i] = f(a[i]);
return result;
}
static double[] MultiplyAllBy(double[] a, double factor) {
return Apply(a, delegate(double x) { return x * factor; });
}
static void Main() {
file:///E|/mybook/C#2.0/C#2.0新特性/1.htm(第 6/11 页)2005-9-21 10:24:13
第一章 C#2.0简介
double[] a = {0.0, 0.5, 1.0};
double[] squares = Apply(a, delegate(double x) { return x * x; });
double[] doubles = MultiplyAllBy(a, 2.0);
}
}
Apply方法需要一个给定的接受double[]元素并返回double[]作为结果的Function。在Main方法中,传
递给Apply方法的第二个参数是一个匿名方法,它与Function委托类型是兼容的。这个匿名方法只简单地
返回每个元素的平方值,因此调用Apply方法得到的double[]包含了a中每个值的平方值。
MultiplyAllBy方法通过将参数数组中的每一个值乘以一个给定的factor来建立一个double[]并返回。为
了产生这个结果,MultiplyAllBy方法调用了Apply方法,向它传递了一个能够将参数x与factor相乘的匿名方
法。
如果一个本地变量或参数的作用域包括了匿名方法,则该变量或参数称为匿名方法的外部变量
(outer variables)。在MultiplyAllBy方法中,a和factor就是传递给Apply方法的匿名方法的外部变量。通
常,一个局部变量的生存期被限制在块内或与之相关联的语句内。然而,一个被捕获的外部变量的生存
期要扩展到至少对匿名方法的委托引用符合垃圾收集条件时。
1.2.1 方法组转换
像前面章节中描述过的那样,一个匿名方法可以被隐式转换为一个兼容的委托类型。C# 2.0允许对一
组方法进行相同的转换,即所任何时候都可以省略一个委托的显式实例化。例如,下面的语句:
addButton.Click += new EventHandler(AddClick);
Apply(a, new Function(Math.Sin));
还可以写做:
addButton.Click += AddClick;
Apply(a, Math.Sin);
当使用短形式时,编译器可以自动地推断应该实例化哪一个委托类型,不过除此之外的效果都和长形式
相同。
1.3 迭代器
C#中的foreach语句用于迭代一个可枚举(enumerable)的集合中的元素。为了实现可枚举,一个集合
必须要有一个无参的、返回枚举器(enumerator)的GetEnumerator方法。通常,枚举器是很难实现的,因
此简化枚举器的任务意义重大。
迭代器(iterator)是一块可以产生(yields)值的有序序列的语句块。迭代器通过出现的一个或多个
yield语句来区别于一般的语句块:
? yield return语句产生本次迭代的下一个值。
? yield break语句指出本次迭代完成。
只要一个函数成员的返回值是一个枚举器接口(enumerator interfaces)或一个可枚举接口
(enumerable interfaces),我们就可以使用迭代器:
? 所谓枚举器借口是指System.Collections.IEnumerator和从System.Collections.Generic.IEnumerator<T>构造
的类型。
? 所谓可枚举接口是指System.Collections.IEnumerable和从System.Collections.Generic.IEnumerable<T>构造
的类型。
理解迭代器并不是一种成员,而是实现一个功能成员是很重要的。一个通过迭代器实现的成员可以
用一个或使用或不使用迭代器的成员覆盖或重写。
下面的Stack<T>类使用迭代器实现了它的GetEnumerator方法。其中的迭代器按照从顶端到底端的顺
序枚举了栈中的元素。
using System.Collections.Generic;
file:///E|/mybook/C#2.0/C#2.0新特性/1.htm(第 7/11 页)2005-9-21 10:24:13
第一章 C#2.0简介
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];
}
}
}
GetEnumerator方法的出现使得Stack<T>成为一个可枚举类型,这允许Stack<T>的实例使用foreach语
句。下面的例子将值0至9压入一个整数堆栈,然后使用foreach循环按照从顶端到底端的顺序显示每一个
值。
using System;
class Test
{
static void Main() {
Stack<int> stack = new Stack<int>();
for (int i = 0; i < 10; i++) stack.Push(i);
foreach (int i in stack) Console.Write("{0} ", i);
Console.WriteLine();
}
}
这个例子的输出为:
9 8 7 6 5 4 3 2 1 0
语句隐式地调用了集合的无参的GetEnumerator方法来得到一个枚举器。一个集合类中只能定义一个
这样的无参的GetEnumerator方法,不过通常可以通过很多途径来实现枚举,包括使用参数来控制枚举。
在这些情况下,一个集合可以使用迭代器来实现能够返回可枚举接口的属性和方法。例如,Stack<T>可
以引入两个新的属性——IEnumerable<T>类型的TopToBottom和BottomToTop:
using System.Collections.Generic;
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];
}
}
public IEnumerable<T> TopToBottom {
get {
return this;
}
file:///E|/mybook/C#2.0/C#2.0新特性/1.htm(第 8/11 页)2005-9-21 10:24:13
第一章 C#2.0简介
}
public IEnumerable<T> BottomToTop {
get {
for (int i = 0; i < count; i++) {
yield return items[i];
}
}
}
}
TopToBottom属性的get访问器只返回this,因为堆栈本身就是一个可枚举类型。BottomToTop属性使
用C#迭代器返回了一个可枚举接口。下面的例子显示了如何使用这两个属性来以任意顺序枚举栈中的元
素:
using System;
class Test
{
static void Main() {
Stack<int> stack = new Stack<int>();
for (int i = 0; i < 10; i++) stack.Push(i);
foreach (int i in stack.TopToBottom) Console.Write("{0} ", i);
Console.WriteLine();
foreach (int i in stack.BottomToTop) Console.Write("{0} ", i);
Console.WriteLine();
}
}
当然,这些属性还可以用在foreach语句的外面。下面的例子将调用属性的结果传递给一个独立的Print
方法。这个例子还展示了一个迭代器被用作一个带参的FromToBy方法的方法体:
using System;
using System.Collections.Generic;
class Test
{
static void Print(IEnumerable<int> collection) {
foreach (int i in collection) Console.Write("{0} ", i);
Console.WriteLine();
}
static IEnumerable<int> FromToBy(int from, int to, int by) {
for (int i = from; i <= to; i += by) {
yield return i;
}
}
static void Main() {
Stack<int> stack = new Stack<int>();
for (int i = 0; i < 10; i++) stack.Push(i);
Print(stack.TopToBottom);
Print(stack.BottomToTop);
Print(FromToBy(10, 20, 2));
}
}
file:///E|/mybook/C#2.0/C#2.0新特性/1.htm(第 9/11 页)2005-9-21 10:24:13
第一章 C#2.0简介
这个例子的输出为:
9 8 7 6 5 4 3 2 1 0
0 1 2 3 4 5 6 7 8 9
10 12 14 16 18 20
泛型和非泛型的可枚举接口都只有一个单独的成员,一个无参的GetEnumerator方法,它返回一个枚
举器接口。一个可枚举接口很像一个枚举器工厂(enumerator factory)。每当调用了一个正确地实现了可
枚举接口的类的GetEnumerator方法时,都会产生一个独立的枚举器。
using System;
using System.Collections.Generic;
class Test
{
static IEnumerable<int> FromTo(int from, int to) {
while (from <= to) yield return from++;
}
static void Main() {
IEnumerable<int> e = FromTo(1, 10);
foreach (int x in e) {
foreach (int y in e) {
Console.Write("{0,3} ", x * y);
}
Console.WriteLine();
}
}
}
上面的代码打印了一个从1到10的简单乘法表。注意FromTo方法只调用了一次用来产生可枚举接口
e。而e.GetEnumerator()被调用了多次(通过foreach语句)来产生多个相同的枚举器。这些枚举器都封装
了FromTo声明中指定的代码。注意,迭代其代码改变了from参数。不过,枚举器是独立的,因为对于
from参数和to参数,每个枚举器拥有它自己的一份拷贝。在实现可枚举类和枚举器类时,枚举器之间的过
渡状态(一个不稳定状态)是必须消除的众多细微瑕疵之一。C#中的迭代器的设计可以帮助消除这些问
题,并且可以用一种简单的本能的方式来实现健壮的可枚举类和枚举器类。
1.4 不完全类型
尽管在一个单独的文件中维护一个类型的所有代码是一项很好的编程实践,但有些时候,当一个类
变得非常大,这就成了一种不切实际的约束。而且,程序员经常使用代码生成器来生成一个应用程序的
初始结构,然后修改产生的代码。不幸的是,当以后需要再次发布原代码的时候,现存的修正会被重
写。
不完全类型允许类、结构和接口被分成多个小块儿并存贮在不同的源文件中使其容易开发和维护。
另外,不完全类型可以分离机器产生的代码和用户书写的部分,这使得用工具来加强产生的代码变得容
易。
要在多个部分中定义一个类型的时候,我们使用一个新的修饰符——partial。下面的例子在两个部分
中实现了一个不完全类。这两个部分可能在不同的源文件中,例如第一部分可能是机器通过数据库影射
工具产生的,而第二部分是手动创作的:
public partial class Customer
{
private int id;
private string name;
private string address;
file:///E|/mybook/C#2.0/C#2.0新特性/1.htm(第 10/11 页)2005-9-21 10:24:13
第一章 C#2.0简介
private List<Order> orders;
public Customer() {
...
}
}
public partial class Customer
{
public void SubmitOrder(Order order) {
orders.Add(order);
}
public bool HasOutstandingOrders() {
return orders.Count > 0;
}
}
当上面的两部分编译到一起时,产生的代码就好像这个类被写在一个单元中一样:
public class Customer
{
private int id;
private string name;
private string address;
private List<Order> orders;
public Customer() {
...
}
public void SubmitOrder(Order order) {
orders.Add(order);
}
public bool HasOutstandingOrders() {
return orders.Count > 0;
}
}
不完全类型的所有部分必须放到一起编译,才能在编译期间将它们合并。需要特别注意的是,不完
全类型并不允许扩展已编译的类型。
file:///E|/mybook/C#2.0/C#2.0新特性/1.htm(第 11/11 页)2005-9-21 10:24:13
第二章 泛型
第二章 泛型
20.泛型
20.1泛型类声明
泛型类声明是一个需要提供类型参数以形成实际类型的类的声明。
类声明可以有选择地定义类型参数。
class-declaration: (类声明)
attributesopt class-modifiersopt class identifieropt type-parameter-listopt class –baseopt type-parameterconstraints-
clauseopt class-body;opt (特性可选 类修饰符可选 类标识符可选 类型参数列表可选 基类可选
类型参数约束语句可选 类体; 可选 )
除非提供了类型参数列表,类声明可以不提供类型参数化约束语句。
提供了类型参数列表的类声明是一个泛型类声明。此外,任何嵌入到泛型类声明或泛型结构声明中的
类,自身是一个泛型类声明,因为必须提供包含类型的类型参数以创建构造类型(constructed type);
泛型类通过使用构造类型而被引用(§20.5)。给定泛型类声明
class List<T>{}
这是构造类型的一些例子,List<T>,List<int>和List<List<string>>。构造类型可以使用一个或多个参数,
例如List<T>被称为开放构造类型(open constructed type)。不使用类型参数的构造类型,例如List<int>被
称为封闭构造类型(closed constructed type)。
泛型类型不可以被“重载”;也就是说,和普通类型一样在一个作用域内,泛型类型必须被唯一地命
名。
class C{}
class C<V>{}//错误,C定义了两次
class C<U,V>{}//错误,C定义了两次
file:///E|/mybook/C#2.0/C#2.0新特性/2.htm(第 1/20 页)2005-9-21 10:24:15
第二章 泛型
然而在非限定类型名字查找(§20.9.3)中使用的类型查找规则和成员访问(§20.9.4),确实考虑到了类
型参数的个数。
20.1.1类型参数
类型参数可以在一个类声明上提供。每个类型参数是一个简单的标识符,它指示了用来创建一个构造类
型的类型参数的占位符。类型参数是在后面将要被提供的类型的形式占位符。相反,类型参数§20.5.1)
只是在构造类型被引用时,实际类型的一个替代。
type-parameter-list:(类型参数列表:)
<type-parameters> (<类型参数>)
type-parameters:(类型参数:)
type-parameter(类型参数)
type-parameters type-parameter(类型参数,类型参数)
type-parameter:(类型参数:)
attributesopt identifier(特性可选 标识符)
在类声明中的每个类型参数在类的声明空间(§3.3)定义了一个名字。由此,它不能和另一个类型参数
或在类中声明的成员有同样的名字。类型参数不能和类型自身有同样的名字。
在一个类中的类型参数的作用域(§3.7),包括基类 、 类型参数约束语句和类体。不像类的成员,它没
有扩展到派生类。在其作用域之内,类型参数可以被用作一个类型。
type(类型):
value-type(值类型)
reference-type(引用类型)
type-parameter(类型参数)
由于类型参数可以被许多不同的实际类型实参所实例化,类型参数与其他类型相比将略微有一些不同的
操作和限制。包括如下内容。
file:///E|/mybook/C#2.0/C#2.0新特性/2.htm(第 2/20 页)2005-9-21 10:24:15
第二章 泛型
类型参数不能用于直接声明一个基类型或者接口
对于在类型参数上的成员查找规则,如果约束存在,则依赖于应用到该类型参数的约束。更详细地说明
参看§20.7.4。
类型参数可行的转换依赖于应用到该类型参数上的约束(如果有的话)。详细地说明参看§20.7.4。
字面null不能被转换到由类型参数所给定的类型,除非类型参数是由一个类约束(§20.7.4)所约束。然
而可以使用一个默认值表达式(§20.8.1)代替。此外,由一个类型参数给定的类型的值可以使
用“==”和“!=”(§20.8.4)与null进行比较。
如果类型参数通过一个构造函数约束(constructor-constraint)(§20.7)而约束,new表达式只能用过一
个类型参数而被使用。
类型参数不能用于特性内的任何地方。
类型参数不能用于成员访问,或者表示一个静态成员或者嵌套类型的类型名字(§20.9.1、§20.9.4)。
在不安全代码中,类型参数不能被用作托管类型(§18.2)。
作为一种类型,类型参数纯粹只是一个编译时构件。在运行时,每个类型参数被绑定到运行时类型,它
是通过泛型类型声明所提供的类型实参所指定的。为此,在运行时,使用类型参数声明的变量类型是一
个封闭类型(closed type)(§20.5.2)。所有语句和表达式在运行时执行所使用的类型参数,都是由那个
参数作为类型实参而提供的实际类型。
20.1.2实例类型
每个类声明都有与之关联的构造类型,即实例类型(instance type)。对于一个泛型类声明,实例类型通过创
建一个来自于类型声明的构造类型(§20.4)而形成,它使用对应于类型参数的每一个类型实参。由于实
例化类型使用类型参数,在类型参数作用域内(类声明之内),它是唯一有效的。实例类型在类声明中
是this的类型。对于非泛型类,实例类型只是一个声明类型。下面展示了几个声明类,以及它们的实例类
型。
class A<T> //实例类型:A<T>
{
class B{} //实例类型:A<T>.B
class C<U>{} //实例类型:A<T>.C<U>
}
class D{} //实例类型:D
20.1.3基类规范
在类声明中指定的基类可以是一个构造类型(§20.5)。一个基类其自身不能是一个类型参数,但在其作
file:///E|/mybook/C#2.0/C#2.0新特性/2.htm(第 3/20 页)2005-9-21 10:24:15
第二章 泛型
用域内可以包含类型参数。
class Extend<V>: V{}//错误,类型参数被用作基类
泛型类声明不能使用System.Attribute作为直接或间接基类。
在一个类声明中指定的基接口可以是构造接口类型(§20.5)。基接口自身不能是类型参数,但在其作用
域内可以包含类型参数,下面的代码演示了如何实现和扩展构造类型。
class C<U,V>{}
Interface I1<V>{}
class D:C<string , int>,I1<string>{}
class E<T>:C<int,T> ,I1<T>{}
泛型类型声明的基接口必须满足§20.3.1中所描述的唯一性规则。
从基类或接口重写或实现方法的类的方法,必须提供特定类型的合适方法。下面的代码演示了方法如何
被重写和实现。这将会在§20.1.10中进一步解释。
class C<U,V>
{
public virtual void M1(U x , List<V> y){⋯}
}
interface I1<V>
{
V M2(V x);
}
file:///E|/mybook/C#2.0/C#2.0新特性/2.htm(第 4/20 页)2005-9-21 10:24:15
第二章 泛型
class D:C<string , int>,I1<string>
{
public override void M1(string x , List<int> y){⋯}
public string M2(string x){⋯}
}
20.1.4泛型类的成员
泛型类的所有成员都可以直接地或者作为构造类型的一部分,从任何封闭类(enclosing class)中使用类型
参数。当特定的封闭构造类型在运行时被使用时,类型参数的每次使用都由构造类型所提供的实际类型
实参所代替。例如
class C<V>
{
public V f1;
public C<V> f2=null;
public C(V x){
this.f1 = x;
this.f2 = this;
}
}
class Application
file:///E|/mybook/C#2.0/C#2.0新特性/2.htm(第 5/20 页)2005-9-21 10:24:15
第二章 泛型
{
static void Main(){
C<int> x1= new C<int >(1);
Console.WriteLine(x1.f1); //打印1
C<double> x2 = new C<double>(3.1415);
Console.WriteLine(x2.f1); //打印 3.1415
}
}
在实例函数成员之内,this的类型就是声明的实例类型(§20.1.2)。
除了使用类型参数作为类型和成员,在泛型类声明中也遵循和非泛型类成员相同的规则。适用于特定种
类成员的附加规则将在后面几节进行讨论。
20.1.5泛型类中的静态字段
在一个泛型类声明中的静态变量,在相同封闭构造类型(§20.5.2)所有实例中被共享,但在不同封闭构
造类型的实例中[1],是不被共享的。这些规则不管静态变量的类型包含那种类型参数都适用。
例如
class C<V>
{
static int count = 0;
public C()
{
count++;
}
public static int Count{
file:///E|/mybook/C#2.0/C#2.0新特性/2.htm(第 6/20 页)2005-9-21 10:24:15
第二章 泛型
get{return count;}
}
}
class Application
{
static void Main()
{
C<int> x1 = new C<int>();
Console.WriteLine(C<int>.Count);//打印 1
C<double> x2 = new C<double>();
Console.WriteLine(C<int>.Count);//打印 1
C<int> x3 = new C<int>();
Console.WriteLine(C<int>.Count);//打印 2
}
}
--------------------------------------------------------------------------------
[1] 这是很容易理解的,因为在运行时,不同的封闭构造类型,是属于不同的类型,比如List<int> 和
List<string> 这二者的实例是不能共享静态变量的。
20.1.6泛型类中的静态构造函数
在泛型类中的静态构造函数被用于初始化静态字段,为每个从特定泛型类声明中创建的不同的封闭构造
file:///E|/mybook/C#2.0/C#2.0新特性/2.htm(第 7/20 页)2005-9-21 10:24:15
第二章 泛型
类型,执行其他初始化。泛型类型声明的类型参数在作用域之内,可以在静态构造函数体内被使用。
如果下列情形之一发生,一个新的封闭构造类类型将被首次初始化。
一个封闭构造类型的实例被创建时
封闭构造类型的任何静态成员被引用时
为了初始化一个新的封闭的构造类类型,首先那个特定封闭类型的一组新静态字段(§20.1.5)将会被创
建。每个静态字段都被初始化为其默认值(§5.2)。接着,静态字段初始化器(§10.4.5.1)将为这些静
态字段执行。最后静态构造函数将被执行。
由于静态构造函数将为每个封闭构造类类型执行一次,那么在不能通过约束(§20.7)检查的类型参数上
实施运行时检查,将会很方便。例如,下面的类型使用一个静态构造函数检查一个类型参数是否是一个
引用类型。
class Gen<T>
{
static Gen(){
if((object)T.default != null){
throw new ArgumentException(“T must be a reference type”);
}
}
}
20.1.7 访问受保护的成员
在一个泛型类声明中,对于继承的受保护的实例成员的访问是允许的,通过从泛型类构造的任何类型的
实例就可以做到。尤其是,用于访问§3.5.3中指定的protected和protected internal实例成员的规则,对于泛
型使用如下的规则进行了扩充。
在一个泛型类G中,对于一个继承的受保护的实例成员M,使用E.M的基本表达式是允许的,前提是E的
类型是一个从G构造的类类型,或继承于一个从G构造的类类型的类类型。
file:///E|/mybook/C#2.0/C#2.0新特性/2.htm(第 8/20 页)2005-9-21 10:24:15
第二章 泛型
在例子
class C<T>
{
protected T x;
}
class D<T> :C<T>
{
static void F(){
D<T> dt = new D<T>();
D<int> di = new D<int>();
D<string> ds = new D<string>();
dt.x = T.default;
di.x = 123;
ds.x = “test”;
}
}
三个对x的赋值语句都是允许的,因为它们都通过从泛型构造的类类型的实例发生。
20.1.8在泛型类中重载
在一个泛型类声明中的方法、构造函数、索引器和运算符可以被重载。但为了避免在构造类中的歧义,
这些重载是受约束的。在同一个泛型类声明中使用相同的名字声明的两个函数成员必须具有这样的参数
类型,也就是封闭构造类型中不能出现两个成员使用相同的名字和签名。当考虑所有可能的封闭构造类
型时,这条规则包含了在当前程序中目前不存在的类型是实参,但它仍然是可能出现的[1]。在类型参数
上的类型约束由于这条规则的目的而被忽略了。
file:///E|/mybook/C#2.0/C#2.0新特性/2.htm(第 9/20 页)2005-9-21 10:24:15
第二章 泛型
下面的例子根据这条规则展示了有效和无效的重载。
nterface I1<T> {⋯}
interface I2<T>{⋯}
class G1<U>
{
long F1(U u); //无效重载,G<int>将会有使用相同签名的两个成员
int F1(int i);
void F2(U u1, U u2); //有效重载,对于U没有类型参数
void F2(int I , string s); //可能同时是int和string
void F3(I1<U>a); //有效重载
void F3(I2<U>a);
void F4(U a); //有效重载
void F4(U[] a);}
class G2<U,V>
{
void F5(U u , V v); //无效重载,G2<int , int>将会有两个签名相同的成员
void F5(V v, U u);
void F6(U u , I1<V> v);//无效重载,G2<I1<int>,int>将会有两个签名相同的成员
file:///E|/mybook/C#2.0/C#2.0新特性/2.htm(第 10/20 页)2005-9-21 10:24:15
第二章 泛型
void F6(I1<V> v , U u);
void F7(U u1,I1<V> V2);//有效的重载,U不可能同时是V和I1<V>
void F7(V v1 , U u2);
void F8(ref U u); //无效重载
void F8(out V v);
}
class C1{⋯}
class C2{⋯}
class G3<U , V> where U:C1 where V:C2
{
void F9(U u); //无效重载,当检查重载时,在U和V上的约束将被忽略
void F9(V v);
}
20.1.9参数数组方法和类型参数
类型参数可以被用在参数数组的类型中。例如,给定声明
class C<V>
{
static void F(int x, int y ,params V[] args);
}
方法的扩展形式的如下调用
C<int>.F(10, 20);
C<object>.F(10,20,30,40);
file:///E|/mybook/C#2.0/C#2.0新特性/2.htm(第 11/20 页)2005-9-21 10:24:15
第二章 泛型
C<string>.F(10,20,”hello”,”goodbye”);
对应于如下形式:
C<int>.F(10,20, new int[]{});
C<object>.F(10,20,new object[]{30,40});
C<string>.F(10,20,new string[](“hello”,”goodbye”));
20.1.10重写和泛型类
在泛型类中的函数成员可以重写基类中的函数成员。如果基类是一个非泛型类型或封闭构造类型,那么
任何重写函数成员不能有包含类型参数的组成类型。然而,如果一个基类是一个开放构造类型,那么重
写函数成员可以使用在其声明中的类型参数。当重写基类成员时,基类成员必须通过替换类型实参而被
确定,如§20.5.4中所描述的。一旦基类的成员被确定,用于重写的规则和非泛型类是一样的。
下面的例子演示了对于现有的泛型其重写规则是如何工作的。
abstract class C<T>
{
public virtual T F(){⋯}
public virtual C<T> G(){⋯}
public virtual void H(C<T> x ){⋯}
}
class D:C<string>
{
public override string F(){⋯}//OK
file:///E|/mybook/C#2.0/C#2.0新特性/2.htm(第 12/20 页)2005-9-21 10:24:15
第二章 泛型
public override C<string> G(){⋯}//OK
public override void H(C<T> x); //错误,应该是C<string>
}
class E<T,U>:C<U>
{
public override U F(){⋯}//OK
public override C<U> G(){⋯}//OK
public override void H(C<T> x){⋯}//错误,应该是C<U>
}
20.1.11泛型类中的运算符
泛型类声明可以定义运算符,它遵循和常规类相同的规则。类声明的实例类型(§20.1.2)必须以一种类
似于运算符的常规规则的方式,在运算符声明中被使用,如下
一元运算符必须接受一个实例类型的单一参数。一元运算符“++”和“—”必须返回实例类型。
至少二元运算符的参数之一必须是实例类型。
转换运算符的参数类型和返回类型都必须是实例类型。
下面展示了在泛型类中几个有效的运算符声明的例子
class X<T>
{
public static X<T> operator ++(X(T) operand){⋯}
public static int operator *(X<T> op1, int op2){⋯}
public static explicit operator X<T>(T value){⋯}
}
file:///E|/mybook/C#2.0/C#2.0新特性/2.htm(第 13/20 页)2005-9-21 10:24:15
第二章 泛型
对于一个从源类型S到目标类型T的转换运算符,当应用§10.9.3中的规则时,任何关联S或T的类型参数被
认为是唯一类型,它们与其他类型没有继承关系,并且在这些类型参数上的任何约束都将被忽略。
在例子
class C<T>{⋯}
class D<T>:C<T>
{
public static implicit operator C<int>(D<T> value){⋯}//OK
public static implicit operator C<T>(D<T> value){⋯}//错误
}
第一个运算符声明是允许的,由于§10.9.3的原因,T和int被认为是没有关系的唯一类型。然而,第二个
运算符是一个错误,因为C<T>是D<T>的基类。
给定先前的例子,为某些类型实参声明运算符,指定已经作为预定义转换而存在的转换是可能的。
struct Nullable<T>
{
public static implicit operator Nullable<T>(T value){⋯}
public static explicit operator T(Nullable<T> value){⋯}
}
当类型object作为T的类型实参被指定,第二个运算符声明了一个已经存在的转换(从任何类型到object是
一个隐式的,也可以是显式的转换)。
在两个类型之间存在预定义的转换的情形下,在这些类型上的任何用户定义的转换都将被忽略。尤其是
如果存在从类型S到类型T的预定义的隐式转换(§6.1),所有用户定义的转换(隐式的或显式的)都将
file:///E|/mybook/C#2.0/C#2.0新特性/2.htm(第 14/20 页)2005-9-21 10:24:15
第二章 泛型
被忽略。
如果存在从类型S到类型T的预定义的显式转换,那么任何用户定义的从类型S到类型T的显式转换都将被
忽略。但用户定义的从S到T的隐式转换仍会被考虑。
对于所有类型除了object,由Nullable<T>类型声明的运算符都不会与预定义的转换冲突。例如
void F(int I , Nullable<int> n){
i = n; //错误
i = (int)n; //用户定义的显式转换
n = i; //用户定义的隐式转换
n = (Nullable<int>)i; //用户定义的隐式转换
}
然而,对于类型object,预定义的转换在所有情况隐藏了用户定义转换,除了一种情况:
void F(object o , Nullable<object> n){
o = n; //预定义装箱转换
o= (object)n; //预定义装箱转换
n= o; //用户定义隐式转换
n = (Nullable<object>)o; //预定义取消装箱转换
}
20.1.12泛型类中的嵌套类型
泛型类声明可以包含嵌套类型声明。封闭类的类型参数可以在嵌套类型中使用。嵌套类型声明可以包含
附加的类型参数,它只适用于该嵌套类型。
包含在泛型类声明中的每个类型声明是隐式的泛型类型声明。当编写一个嵌套在泛型类型内的类型的引
用时,包含构造类型,包括它的类型实参,必须被命名。然而,在外部类中,内部类型可以被无限制的
使用;当构造一个内部类型时,外部类的实例类型可以被隐式地使用。下面的例子展示了三个不同的引
用从Inner创建的构造类型的方法,它们都是正确的;前两个是等价的。
file:///E|/mybook/C#2.0/C#2.0新特性/2.htm(第 15/20 页)2005-9-21 10:24:15
第二章 泛型
class Outer<T>
{
class Inner<U>
{
static void F(T t , U u){⋯}
}
static void F(T t)
{
Outer<T>.Inner<string >.F(t,”abc”);//这两个语句有同样的效果
Inner<string>.F(t,”abc”);
Outer<int>.Inner<string>.F(3,”abc”); //这个类型是不同的
Outer.Inner<string>.F(t , “abc”); //错误,Outer需要类型参数
}
}
尽管这是一种不好的编程风格,但嵌套类型中的类型参数可以隐藏一个成员,或在外部类型中声明的一
个类型参数。
class Outer<T>
{
class Inner<T> //有效,隐藏了 Ouer的 T
file:///E|/mybook/C#2.0/C#2.0新特性/2.htm(第 16/20 页)2005-9-21 10:24:15
第二章 泛型
{
public T t; //引用Inner的T
}
}
20.1.13应用程序入口点
应用程序入口点不能在一个泛型类声明中。
20.2泛型结构声明
像类声明一样,结构声明可以有可选的类型参数。
struct-declaration:(结构声明:)
attributes opt struct-modifiers opt struct identifier type-parameter-list opt struct-interfaces opt type-parameterconstraints-
clauses opt struct-body ;opt
(特性可选 结构修饰符可选 struct 标识符 类型参数列表可选 结构接口可选 类型参数约束语句可选 结构
体;可选)
除了§11.3中为结构声明而指出的差别之外,泛型类声明的规则也适用于泛型结构声明。
20.3泛型接口声明
接口也可以定义可选的类型参数
interface-declaration:(接口声明:)
attribute opt interface-modifiers opt interface indentifier type-parameter-list opt
interface-base opt type-parameter-constraints-clause opt interface-body;
(特性可选 接口修饰符可选 interface 标识符 类型参数列表可选 基接口可选 类型参数约束语句可选 接口
体;可选)
file:///E|/mybook/C#2.0/C#2.0新特性/2.htm(第 17/20 页)2005-9-21 10:24:15
第二章 泛型
使用类型参数声明的接口是一个泛型接口声明。除了所指出的那些,泛型接口声明遵循和常规结构声明
相同的规则。
在接口声明中的每个类型参数在接口的声明空间定义了一个名字。在一个接口上的类型参数的作用域包
括基接口、类型约束语句和接口体。在其作用域之内,一个类型参数可以被用作一个类型。应用到接口
上的类型参数和应用到类(§20.1.1)上的类型参数具有相同的限制。
在泛型接口中的方法与泛型类(§20.1.8)中的方法遵循相同的重载规则。
20.3.1实现接口的唯一性
由泛型类型声明实现的接口必须为所有可能的构造类型保留唯一性。没有这条规则,将不可能为特定的
构造类型确定调用正确的方法。例如,假定一个泛型类声明允许如下写法。
interface I<T>
{
void F();
}
class X<U, V>:I<U>,I<V> //错误,I<U>和I<V>冲突
{
void I<U>.F(){⋯}
void I<V>.F(){⋯}
}
如果允许这么写,那么下面的情形将无法确定执行那段代码。
I<int> x = new X<int ,int>();
x.F();
file:///E|/mybook/C#2.0/C#2.0新特性/2.htm(第 18/20 页)2005-9-21 10:24:15
第二章 泛型
为了确定一个泛型类型声明的接口列表是有效的,可以按下面的步骤进行。
让L成为在泛型类、结构或接口声明 C中指定的接口的列表。
将任何已经在L中的接口的基接口添加到L
从L中删除任何重复的接口
在类型实参被替换到L后,如果任何从C创建的可能构造类型,导致在L中的两个接口是同一的,那么C的
声明是无效的。当确定所有可能的构造类型时,约束声明不予考虑。
在类声明X之上,接口列表L由I<U>和I<V>组成。该声明是无效的,因为任何使用相同类型U和V的构造
类型,将导致这两个接口是同一的。
20.3.2显式接口成员实现
使用构造接口类型的显式接口成员实现本质上与简单接口类型方式上是相同的。和以往一样,显式接口
成员实现必须由一个指明哪个接口被实现的接口类型而限定。该类型可能是一个简单接口或构造接口,
如下例子所示。
interface IList<T>
{
T[] GetElement();
}
interface IDictionary<K,V>
{
V this[K key];
Void Add(K key , V value);
}
class List<T>:IList<T>,IDictionary<int , T>
file:///E|/mybook/C#2.0/C#2.0新特性/2.htm(第 19/20 页)2005-9-21 10:24:15
第二章 泛型
{
T[] IList<T>.GetElement(){⋯}
T IDictionary<int , T>.this[int index]{⋯}
void IDictionary<int , T>.Add(int index , T value){⋯}
}
--------------------------------------------------------------------------------
[1] 也就是在类型参数被替换成类型实参时,有可能替换后的实参导致出现两个成员使用相同的名字和签
名。
file:///E|/mybook/C#2.0/C#2.0新特性/2.htm(第 20/20 页)2005-9-21 10:24:15
第三章 匿名方法
第三章 匿名方法
原著:Microsoft Corporation
原文:http://msdn.microsoft.com/vcsharp/team/language/default.aspx (SpecificationVer2.doc)
翻译:lover_P
出处:
--------------------------------------------------------------------------------
[内容]
3.1 匿名方法表达式
3.2 匿名方法签名
3.3 匿名方法转换
3.3.1 委托建立表达式
3.4 匿名方法块
3.5 外部变量
3.5.1 捕获外部变量
3.5.2 局部变量的实例化
3.6 匿名方法求值
3.7 委托实例相等性
3.8 明确赋值
3.9 方法组转换
3.10 实现实例
3.1 匿名方法表达式
匿名方法表达(anonymous-method-expression)式定义了匿名方法(anonymous method),并求得一个
引用了该方法的特殊的值。
primary-no-array-creation-expression:

anonymous-method-expression
anonymous-method-expression:
delegate anonymous-method-signatureopt block
anonymous-method-signature:
( anonymous-method-parameter-listopt )
anonymous-method-parameter-list:
anonymous-method-parameter
file:///E|/mybook/C#2.0/C#2.0新特性/3.htm(第 1/15 页)2005-9-21 10:24:18
第三章 匿名方法
anonymous-method-parameter-list , anonymous-method-parameter
anonymous-method-parameter:
parameter-modifieropt type identifier
初等非数组表达式:
...
匿名方法表达式
匿名方法表达式:
delegate 匿名方法签名可选 块
匿名方法签名:
( 匿名方法参数列表可选 )
匿名方法参数列表:
匿名方法参数
匿名方法参数 , 匿名方法参数
匿名方法参数:
参数修饰符可选 类型 标识符
匿名方法表达(anonymous-method-expression)是一个遵从特殊转换规则(见3.3)的值。这个值没有类
型,但可以隐式地转换为一个兼容的委托类型。
匿名方法表达式(anonymous-method-expression)为参数、局部变量和常量以及标签定义了一个新的声
明空间。
3.2 匿名方法签名
可选的匿名方法签名(anonymous-method-signature)为匿名方法的形式参数定义了名字和类型。这些参
数的作用于是匿名方法的块(block)。如果一个局部变量的作用域包含了匿名方法表达式(anonymousmethod-
expression),且该匿名方法的参数和该局部变量相同,则会产生编译错误。
如果一个匿名方法表达式(anonymous-method-expression)具有匿名方法签名(anonymous-methodsignature),
则与之兼容的委托类型被强制具有相同的参数类型和修饰符,且具有相同顺序(见3.3)。如
果一个匿名方法表达式(anonymous-method-expression)没有匿名方法签名(anonymous-methodsignature),
则与之相兼容的委托类型被强制要求没有out参数。
注意匿名方法签名(anonymous-method-signature)不能包含特性或参数数组(译注:用于实现变长参数
列表)。然而,一个匿名方法签名(anonymous-method-signature)可以和一个包含参数数组的委托类型
相兼容。
3.3 匿名方法转换
file:///E|/mybook/C#2.0/C#2.0新特性/3.htm(第 2/15 页)2005-9-21 10:24:18
第三章 匿名方法
匿名方法表达式(anonymous-method-expression)是一个没有类型的特殊值。一个匿名方法表达式
(anonymous-method-expression)可以用于委托建立表达式(delegate-creation-expression)(见3.3.1)。对
于匿名方法表达式(anonymous-method-expression)的其他有效的应用取决于定义于其上的隐式转换。
匿名方法表达式(anonymous-method-expressio)与任何兼容的委托类型之间均存在隐式转换。如果D是
一个委托类型,而A是一个匿名方法表达式(anonymous-method-expression),当且仅当以下两个条件成
立的时候D和A是兼容的。
首先,D的参数类型必须与A兼容:
如果A不含匿名方法签名(anonymous-method-signature),则D可以具有任意类型的零或多个参数,但
这些参数不能带有out修饰符。
如果具有匿名方法签名(anonymous-method-signature),则D必须具有和A形同数量的参数,A中的每
个参数必须和D中相应的参数具有相同的类型,并且A中每个参数上的ref或out修饰符的出现与否必须与D
中的相应参数相同。如果D中的最后一个参数是参数数组,则没有相互兼容的A和D。
其次,D的返回值类型必须与A兼容。由于这一规则,A中不能包含其他匿名方法的块(block)。
如果D的返回值类型被声明为void,则A中包含的所有return语句不能指定表达式。
如果D的返回值类型被声明为R,则A中包含的所有return语句不许指定一个能够隐式转换为R的表达
式。A中的块(block)的终点必须可达。
除了和相兼容的委托类型之间的隐式转换,匿名方法表达式(anonymous-method-expression)与任何类
型之间不存在任何转换,包括object类型。
下面的例子详细地解释了这些规则:
delegate void D(int x);
D d1 = delegate { }; // 正确
D d2 = delegate() { }; // 错误,签名不匹配
D d3 = delegate(long x) { }; // 错误,签名不匹配
D d4 = delegate(int x) { }; // 正确
D d5 = delegate(int x) { return; }; // 正确
D d6 = delegate(int x) { return x; }; // 错误,返回值不匹配
delegate void E(out int x);
E e1 = delegate { }; // 错误,E带有一个输出参数
E e2 = delegate(out int x) { x = 1; }; // 正确
E e3 = delegate(ref int x) { x = 1; }; // 错误,签名不匹配
delegate int P(params int[] a);
file:///E|/mybook/C#2.0/C#2.0新特性/3.htm(第 3/15 页)2005-9-21 10:24:18
第三章 匿名方法
P p1 = delegate { }; // 错误,块的结尾不可达
P p2 = delegate { return; }; // 错误,返回值类型不匹配
P p3 = delegate { return 1; }; // 正确
P p4 = delegate { return "Hello"; }; // 错误,返回值类型不匹配
P p5 = delegate(int[] a) { // 正确
return a[0];
};
P p6 = delegate(params int[] a) { // 错误,(译注:多余的)params修饰符
return a[0];
};
P p7 = delegate(int[] a) { // 错误,返回值类型不匹配
if(a.Length > 0) return a[0];
return "Hello";
};
delegate object Q(params int[] a);
Q q1 = delegate(int[] a) { // 正确
if(a.Length > 0) return a[0];
return "Hello";
};
3.3.1 委托建立表达式
委托建立表达式(delegate-creation-expression)可以用于匿名方法和委托类型之间的转换语法的替代
品。如果一个委托建立表达式(delegate-creation-expression)的参数表达式(expression)是一个匿名方法
表达式(anonymous-method-expression),则匿名方法依照上面定义的隐式转换规则转换为给定的委托类
型。例如,如果D是一个委托类型,则表达式
new D(delegate { Console.WriteLine("hello"); })
等价于表达式
(D) delegate { Console.WriteLine("hello"); }
3.4 匿名方法块
匿名方法表达式(anonymous-method-expression)的块(block)遵从下列规则:
如果匿名方法包含一个签名,则签名中指定的参数在块

鲜花

握手

雷人

路过

鸡蛋
该文章已有0人参与评论

请发表评论

全部评论

专题导读
热门推荐
热门话题
阅读排行榜

扫描微信二维码

查看手机版网站

随时了解更新最新资讯

139-2527-9053

在线客服(服务时间 9:00~18:00)

在线QQ客服
地址:深圳市南山区西丽大学城创智工业园
电邮:jeky_zhao#qq.com
移动电话:139-2527-9053

Powered by 互联科技 X3.4© 2001-2213 极客世界.|Sitemap