C#学习教程十一
2014-01-12 19:08
295 查看
在2005年底微软公司正式发布了C# 2.0,与C# 1.x相比,新版本增加了很多新特性,其中最重要的是对泛型的支持。通过泛型,我们可以定义类型安全的数据结构,而无需使用实际的数据类型。这能显著提高性能并得到更高质量的代码。泛型并不是什么新鲜的东西,他在功能上类似于C++的模板,模板多年前就已存在C++上了,并且在C++上有大量成熟应用。
本文讨论泛型使用的一般问题,比如为什么要使用泛型、泛型的编写方法、泛型中数据类型的约束、泛型中静态成员使用要注意的问题、泛型中方法重载的问、泛型方法等,通过这些使我们可以大致了解泛型并掌握泛型的一般应用,编写出更简单、通用、高效的应用系统。
什么是泛型
我们在编写程序时,经常遇到两个模块的功能非常相似,只是一个是处理int数据,另一个是处理string数据,或者其他自定义的数据类型,但我们没有办法,只能分别写多个方法处理每个数据类型,因为方法的参数类型不同。有没有一种办法,在方法中传入通用的数据类型,这样不就可以合并代码了吗?泛型的出现就是专门解决这个问题的。读完本篇文章,你会对泛型有更深的了解。
为什么要使用泛型
泛型是 2.0 版 C# 语言和公共语言运行库 (CLR) 中的一个新功能。泛型将类型参数的概念引入 .NET Framework,类型参数使得设计如下类和方法成为可能:这些类和方法将一个或多个类型的指定推迟到客户端代码声明并实例化该类或方法的时候。例如,通过使用泛型类型参数 T,您可以编写其他客户端代码能够使用的单个类,而不致引入运行时强制转换或装箱操作的成本或风险
为了了解这个问题,我们先看下面的代码,代码省略了一些内容,但功能是实现一个栈,这个栈只能处理int数据类型:
public class Stack
{
private int[] items;
private int count;
public Stack(int size)
{
items = new int[size];
count = 0;
}
public void Push(int x)
{
//为了代码清晰及只简单熟悉泛型因此不考虑堆栈时超出数组大小的情况
items[count++]=x;
}
public int Pop()
{
//为了代码清晰及只简单熟悉泛型因此不考虑出栈时超出数组已为空的情况
return items[--count];
}
}
class Test
{
static void Main()
{
Stack s = new Stack(10);
s.Push(111);
s.Push(222);
Console.WriteLine(s.Pop()+s.Pop())
}
}
上面代码运行的很好,但是,当我们需要一个栈来保存string类型时,该怎么办呢?很多人都会想到把上面的代码复制一份,把int改成string不就行了。当然,这样做本身是没有任何问题的,但一个优秀的程序是不会这样做的,因为他想到若以后再需要long、Node类型的栈该怎样做呢?还要再复制吗?优秀的程序员会想到用一个通用的数据类型object来实现这个栈:
public class Stack
{
private object[] items;
private int count;
public Stack(int size)
{
items = new object[size];
count = 0;
}
public void Push(object x)
{
items[count++]=x;
}
public object Pop()
{
return items[--count];
}
}
class Test
{
static void Main()
{
Stack s = new Stack(10);
s.Push("111");
s.Push("222");
Console.WriteLine((string)s.Pop()+(string)s.Pop())
}
}
这个栈写的不错,他非常灵活,可以接收任何数据类型,可以说是一劳永逸。但全面地讲,也不是没有缺陷的,主要表现在:
当Stack处理值类型时,会出现装箱、折箱操作,这将在托管堆上分配和回收大量的变量,若数据量大,则性能损失非常严重。在处理引用类型时,虽然没有装箱和折箱操作,但将用到数据类型的强制转换操作,增加处理器的负担。
在数据类型的强制转换上还有更严重的问题(假设stack是Stack的一个实例):
Node1 n1 = new Node1();
stack.Push(n1);
Node2 n2 = (Node2)stack.Pop();
上面的代码在编译时是完全没问题的,但由于Push了一个Node1类型的数据,但在Pop时却要求转换为Node2类型,这将出现程序运行时的类型转换异常,但却逃离了编译器的检查。
针对object类型栈的问题,我们引入泛型,他可以优雅地解决这些问题。泛型用用一个通过的数据类型T来代替object,在类实例化时指定T的类型,运行时(Runtime)自动编译为本地代码,运行效率和代码质量都有很大提高,并且保证数据类型安全。
使用泛型
下面是用泛型来重写上面的栈,用一个通用的数据类型T来作为一个占位符,等待在实例化时用一个实际的类型来代替。让我们来看看泛型的威力:
public class Stack<T>
{
private T[] items;
private int count;
public Stack(int size)
{
items = new T[size];
count = 0;
}
public void Push(T x)
{
items[count++]=x;
}
public T Pop()
{
return items[--count];
}
}
类的写法不变,只是引入了通用数据类型T就可以适用于任何数据类型,并且类型安全的。这个类的调用方法:
//实例化只能保存int类型的类
class Test
{
static void Main()
{
Stack<int> s = new Stack<int>(10);
s.Push(111);
s.Push(222);
Console.WriteLine(s.Pop()+s.Pop())
}
}
//实例化只能保存string类型的类
class Test
{
static void Main()
{
Stack<string> s = new Stack<string>(10);
s.Push("111");
s.Push("222");
Console.WriteLine(s.Pop()+s.Pop())
}
}
这个类和object实现的类有截然不同的区别:
1. 他是类型安全的。实例化了int类型的栈,就不能处理string类型的数据,其他数据类型也一样。
2. 无需装箱和折箱。这个类在实例化时,按照所传入的数据类型生成本地代码,本地代码数据类型已确定,所以无需装箱和折箱。
3. 无需类型转换。
泛型概述
1.使用泛型类型可以最大限度地重用代码、保护类型的安全以及提高性能。
2.泛型最常见的用途是创建集合类。
3..NET Framework 类库在 System.Collections.Generic 命名空间中包含几个新的泛型集合类。应尽可能地使用这些类来代替普通的类,如 System.Collections 命名空间中的 ArrayList。
4.您可以创建自己的泛型接口、泛型类、泛型方法、泛型事件和泛型委托。
5.可以对泛型类进行约束以访问特定数据类型的方法。
6.关于泛型数据类型中使用的类型的信息可在运行时通过反射获取。
//上面的示例已简单说明了泛型的特性如欲想深入了解可以看下面的理论及扩展知识
泛型类实例化的理论
C#泛型类在编译时,先生成中间代码IL,通用类型T只是一个占位符。在实例化类时,根据用户指定的数据类型代替T并由即时编译器(JIT)生成本地代码,这个本地代码中已经使用了实际的数据类型,等同于用实际类型写的类,所以不同的封闭类的本地代码是不一样的。按照这个原理,我们可以这样认为:
泛型类的不同的封闭类是分别不同的数据类型。
例:Stack<int>和Stack<string>是两个完全没有任何关系的类,你可以把他看成类A和类B,这个解释对泛型类的静态成员的理解有很大帮助。
泛型类中数据类型的约束
程序员在编写泛型类时,总是会对通用数据类型T进行有意或无意地有假想,也就是说这个T一般来说是不能适应所有类型,但怎样限制调用者传入的数据类型呢?这就需要对传入的数据类型进行约束,约束的方式是指定T的祖先,即继承的接口或类。因为C#的单根继承性,所以约束可以有多个接口,但最多只能有一个类,并且类必须在接口之前。这时就用到了C#2.0的新增关键字:
public class Node<T, V> where T : Stack, IComparable
where V: Stack
{...}
以上的泛型类的约束表明,T必须是从Stack和IComparable继承,V必须是Stack或从Stack继承,否则将无法通过编译器的类型检查,编译失败。
通用类型T没有特指,但因为C#中所有的类都是从object继承来,所以他在类Node的编写中只能调用object类的方法,这给程序的编写造成了困难。比如你的类设计只需要支持两种数据类型int和string,并且在类中需要对T类型的变量比较大小,但这些却无法实现,因为object是没有比较大小的方法的。了解决这个问题,只需对T进行IComparable约束,这时在类Node里就可以对T的实例执行CompareTo方法了。这个问题可以扩展到其他用户自定义的数据类型。
如果在类Node里需要对T重新进行实例化该怎么办呢?因为类Node中不知道类T到底有哪些构造函数。为了解决这个问题,需要用到new约束:
public class Node<T, V> where T : Stack, new()
where V: IComparable
需要注意的是,new约束只能是无参数的,所以也要求相应的类Stack必须有一个无参构造函数,否则编译失败。
C#中数据类型有两大类:引用类型和值类型。引用类型如所有的类,值类型一般是语言的最基本类型,如int, long, struct等,在泛型的约束中,我们也可以大范围地限制类型T必须是引用类型或必须是值类型,分别对应的关键字是class和struct:
public class Node<T, V> where T : class
where V: struct
泛型方法
泛型不仅能作用在类上,也可单独用在类的方法上,他可根据方法参数的类型自动适应各种参数,这样的方法叫泛型方法。看下面的类:
public class Stack2
{
public void Push<T>(Stack<T> s, params T[] p)
{
foreach (T t in p)
{
s.Push(t);
}
}
}
原来的类Stack一次只能Push一个数据,这个类Stack2扩展了Stack的功能(当然也可以直接写在Stack中),他可以一次把多个数据压入Stack中。其中Push是一个泛型方法,这个方法的调用示例如下:
Stack<int> x = new Stack<int>(100);
Stack2 x2 = new Stack2();
x2.Push(x, 1, 2, 3, 4, 6);
string s = "";
for (int i = 0; i < 5; i++)
{
s += x.Pop().ToString();
} //至此,s的值为64321
泛型中的静态成员变量
在C#1.x中,我们知道类的静态成员变量在不同的类实例间是共享的,并且他是通过类名访问的。C#2.0中由于引进了泛型,导致静态成员变量的机制出现了一些变化:静态成员变量在相同封闭类间共享,不同的封闭类间不共享。
这也非常容易理解,因为不同的封闭类虽然有相同的类名称,但由于分别传入了不同的数据类型,他们是完全不同的类,比如:
Stack<int> a = new Stack<int>();
Stack<int> b = new Stack<int>();
Stack<long> c = new Stack<long>();
类实例a和b是同一类型,他们之间共享静态成员变量,但类实例c却是和a、b完全不同的类型,所以不能和a、b共享静态成员变量。
泛型中的静态构造函数
静态构造函数的规则:只能有一个,且不能有参数,他只能被.NET运行时自动调用,而不能人工调用。
泛型中的静态构造函数的原理和非泛型类是一样的,只需把泛型中的不同的封闭类理解为不同的类即可。以下两种情况可激发静态的构造函数:
1. 特定的封闭类第一次被实例化。
2. 特定封闭类中任一静态成员变量被调用。
泛型类中的方法重载
方法的重载在.Net Framework中被大量应用,他要求重载具有不同的签名。在泛型类中,由于通用类型T在类编写时并不确定,所以在重载时有些注意事项,这些事项我们通过以下的例子说明:
public class Node<T, V>
{
public T add(T a, V b) //第一个add
{
return a;
}
public T add(V a, T b) //第二个add
{
return b;
}
public int add(int a, int b) //第三个add
{
return a + b;
}
}
上面的类很明显,如果T和V都传入int的话,三个add方法将具有同样的签名,但这个类仍然能通过编译,是否会引起调用混淆将在这个类实例化和调用add方法时判断。请看下面调用代码:
Node<int, int> node = new Node<int, int>();
object x = node.add(2, 11);
这个Node的实例化引起了三个add具有同样的签名,但却能调用成功,因为他优先匹配了第三个add。但如果删除了第三个add,上面的调用代码则无法编译通过,提示方法产生的混淆,因为运行时无法在第一个add和第二个add之间选择。
Node<string, int> node = new Node<string, int>();
object x = node.add(2, "11");
这两行调用代码可正确编译,因为传入的string和int,使三个add具有不同的签名,当然能找到唯一匹配的add方法。
由以上示例可知,C#的泛型是在实例的方法被调用时检查重载是否产生混淆,而不是在泛型类本身编译时检查。同时还得出一个重要原则:
当一般方法与泛型方法具有相同的签名时,会覆盖泛型方法。
泛型类的方法重写
方法重写(override)的主要问题是方法签名的识别规则,在这一点上他与方法重载一样,请参考泛型类的方法重载。
泛型的使用范围
本文主要是在类中讲述泛型,实际上,泛型还可以用在类方法、接口、结构(struct)、委托等上面使用,使用方法大致相同,就不再讲述。
如果子类是一般的而非具体的类型实参,则可以使用子类一般类型参数作为一般基类的指定类型:
在使用子类一般类型参数时,必须在子类级别重复在基类级别规定的任何约束。例如,派生约束:
或构造函数约束:
基类可以定义其签名使用一般类型参数的虚拟方法。在重写它们时,子类必须在方法签名中提供相应的类型:
如果该子类是一般类型,则它还可以在重写时使用它自己的一般类型参数:
您可以定义一般接口、一般抽象类,甚至一般抽象方法。这些类型的行为像其他任何一般基类型一样:
一般抽象方法和一般接口有一种有趣的用法。在 C# 2.0 中,不能对一般类型参数使用诸如 + 或 += 之类的运算符。例如,以下代码无法编译,因为 C# 2.0 不具有运算符约束:
但是,您可以通过定义一般操作,使用抽象方法(最好使用接口)进行补偿。由于抽象方法的内部不能具有任何代码,因此可以在基类级别指定一般操作,并且在子类级别提供具体的类型和实现:
一般接口还可以产生更加干净一些的解决方案:
返回页首
这是一种重要的功能,因为它使您可以每次用不同的类型调用该方法,而这对于实用工具类非常方便。
即使包含类根本不使用泛型,您也可以定义方法特定的一般类型参数:
该功能仅适用于方法。属性或索引器只能使用在类的作用范围中定义的一般类型参数。
在调用定义了一般类型参数的方法时,您可以提供要在调用场所使用的类型:
因此,当调用该方法时,C# 编译器将足够聪明,从而基于传入的参数的类型推断出正确的类型,并且它允许完全省略类型规范:
该功能称为一般类型推理。请注意,编译器无法只根据返回值的类型推断出类型:
当方法定义它自己的一般类型参数时,它还可以定义这些类型的约束:
但是,您无法为类级别一般类型参数提供方法级别约束。类级别一般类型参数的所有约束都必须在类作用范围中定义。
在重写定义了一般类型参数的虚拟方法时,子类方法必须重新定义该方法特定的一般类型参数:
子类实现必须重复在基础方法级别出现的所有约束:
请注意,方法重写不能定义没有在基础方法中出现的新约束。
此外,如果子类方法调用虚拟方法的基类实现,则它必须指定要代替一般基础方法类型参数使用的类型实参。您可以自己显式指定它,或者依靠类型推理(如果可用):
一般静态方法
C# 允许定义使用一般类型参数的静态方法。但是,在调用这样的静态方法时,您需要在调用场所为包含类提供具体的类型,如下面的示例所示:
静态方法可以定义方法特定的一般类型参数和约束,就像实例方法一样。在调用这样的方法时,您需要在调用场所提供方法特定的类型 — 可以按如下方式显式提供:
或者依靠类型推理(如果可能):
一般静态方法遵守施加于它们在类级别使用的一般类型参数的所有约束。就像实例方法一样,您可以为由静态方法定义的一般类型参数提供约束:
C# 中的运算符只是静态方法而已,并且 C# 允许您为自己的一般类型重载运算符。假设代码块 3 的一般 LinkedList 提供了用于串联链表的 + 运算符。+ 运算符使您能够编写下面这段优美的代码:
代码块 7 显示 LinkedList 类上的一般 + 运算符的实现。请注意,运算符不能定义新的一般类型参数。
代码块 7. 实现一般运算符
返回页首
在为包含类指定类型时,也会影响到委托:
C# 2.0 使您可以将方法引用的直接分配转变为委托变量:
我将把该功能称为委托推理。编译器能够推断出您分配到其中的委托的类型,查明目标对象是否具有采用您指定的名称的方法,并且验证该方法的签名匹配。然后,编译器创建所推断出的参数类型(包括正确的类型而不是一般类型参数)的新委托,并且将新委托分配到推断出的委托中。
像类、结构和方法一样,委托也可以定义一般类型参数:
在类的作用范围外部定义的委托可以使用一般类型参数。在该情况下,在声明和实例化委托时,必须为其提供类型实参:
另外,还可以在分配委托时使用委托推理:
当然,委托可以定义约束以伴随它的一般类型参数:
委托级别约束只在使用端实施(在声明委托变量和实例化委托对象时),类似于在类型或方法的作用范围中实施的其他任何约束。
一般委托对于事件尤其有用。您可以精确地定义一组有限的一般委托(只按照它们需要的一般类型参数的数量进行区分),并且使用这些委托来满足所有事件处理需要。代码块 8 演示了一般委托和一般事件处理方法的用法。
代码块 8. 一般事件处理
代码块 8 使用名为 GenericEventHandler 的一般委托,它接受一般发送者类型和一般类型参数。显然,如果您需要更多的参数,则可以简单地添加更多的一般类型参数,但是我希望模仿按如下方式定义的 .NET
EventHandler 来设计 GenericEventHandler:
与 EventHandler 不同,GenericEventHandler 是类型安全的(如代码块 8 所示),因为它只接受
MyPublisher 类型的对象(而不是纯粹的 Object)作为发送者。实际上,.NET 已经在 System 命名空间中定义了一般样式的
EventHandler:
返回页首
typeof 运算符或者通过调用每个类型支持的 GetType() 方法来获得任何类型的 Type。不管您选择哪种方式,都会产生相同的 Type。例如,在以下代码示例中,type1 与
type2 完全相同。
typeof 和 GetType() 都可以对一般类型参数进行操作:
此外,typeof 运算符还可以对未绑定的一般类型进行操作。例如:
所追踪的数字 1 是所使用的一般类型的一般类型参数的数量。请注意空 <> 的用法。要对带有多个类型参数的未绑定一般类型进行操作,请在
<> 中使用“,”:
Type 具有新的方法和属性,用于提供有关该类型的一般方面的反射信息。代码块 9 显示了新方法。
代码块 9. Type 的一般反射成员
上述新成员中最有用的是 HasGenericArguments 属性,以及 GetGenericArguments() 和
GetGenericTypeDefinition() 方法。Type 的其余新成员用于高级的且有点深奥的方案,这些方案超出了本文的范围。
正如它的名称所指示的那样,如果由 Type 对象表示的类型使用一般类型参数,则 HasGenericArguments 被设置为 true。GetGenericArguments() 返回与所使用的类型参数相对应的 Type 数组。GetGenericTypeDefinition() 返回一个表示基础类型的一般形式的 Type。代码块 10 演示如何使用上述一般处理 Type
成员获得有关代码块 3 中的 LinkedList 的一般反射信息。
代码块 10. 使用 Type 进行一般反射
与 Type 类似,MethodInfo 和它的基类 MethodBase 具有反射一般方法信息的新成员。
与 C# 1.1 中一样,您可以使用 MethodInfo(以及很多其他选项)进行晚期绑定调用。但是,您为晚期绑定传递的参数的类型,必须与取代一般类型参数而使用的绑定类型(如果有)相匹配:
属性和泛型
在定义属性时,可以使用枚举 AttributeTargets 的新 GenericParameter 值,通知编译器属性应当以一般类型参数为目标:
请注意,C# 2.0 不允许定义一般属性。
然而,属性类可以通过使用一般类型或者定义 Helper 一般方法(像其他任何类型一样)在内部利用泛型:
返回页首
System.Array 和泛型
System.Array 类型通过很多一般静态方法进行了扩展。这些一般静态方法专门用于自动执行和简化处理数组的常见任务,例如,遍历数组并且对每个元素执行操作、扫描数组,以查找匹配某个条件(谓词)的值、对数组进行变换和排序等等。代码块 11 是这些静态方法的部分清单。
代码块 11. System.Array 的一般方法
System.Array 的静态一般方法都使用 System 命名空间中定义的下列四个一般委托:
代码块 12 演示如何使用这些一般方法和委托。它用从 1 到 20 的所有整数初始化一个数组。然后,代码通过一个匿名方法和
Action 委托,使用 Array.ForEach() 方法来跟踪这些数字。使用第二个匿名方法和
Predicate 委托,代码通过调用 Array.FindAll() 方法(它返回另一个相同的一般类型的数组),来查找该数组中的所有质数。最后,使用相同的
Action 委托和匿名方法来跟踪这些质数。请注意代码块 12 中类型参数推理的用法。您在使用静态方法时无须指定类型参数。
代码块 12. 使用 System.Array 的一般方法
在 System.Collections.Generic 命名空间中定义的类 List 中,也可以得到类似的一般方法。这些方法使用四个相同的一般委托。实际上,您还可以在您的代码中利用这些委托,如以下部分所示。
静态集合类
尽管 System.Array 和 List 都提供了能够大大简化自身使用方式的、方便的实用工具方法,但 .NET 没有为其他集合提供这样的支持。为了对此进行补偿,本文随附的源代码包含了静态 Helper 类
Collection,其定义如下所示:
Collection 的实现简单易懂。例如,以下为 ForEach() 方法:
Collection 静态类的用法非常类似于 Array 和 List,它们都利用相同的一般委托。您可以将
Collection 用于任何集合,只要该集合支持 IEnumerable 或
IEnumerator:
一般集合
System.Collections 中的数据结构全部都是基于 Object 的,因而继承了本文开头描述的两个问题,即性能较差和缺少类型安全。.NET 2.0 在 System.Collections.Generic 命名空间中引入了一组一般集合。例如,有一般的
Stack 类和一般的 Queue 类。Dictionary 数据结构等效于非一般的
HashTable,并且还有一个有点像 SortedList 的 SortedDictionary 类。类
List 类似于非一般的 ArrayList。表 1 将 System.Collections.Generic 的主要类型映射到 System.Collections 中的那些主要类型。
[thead]
System.Collections.Generic 中的所有一般集合还实现了一般的 IEnumerable 接口,该接口的定义如下所示:
简单说来,IEnumerable 提供了对 IEnumerator 迭代器接口的访问,该接口用于对集合进行抽象迭代。所有集合都在嵌套结构上实现了
IEnumerable,其中,一般类型参数 T 是集合存储的类型。
特别有趣的是,词典集合定义它们的迭代器的方式。词典实际上是两个类型(而非一个类型)的一般参数(键和值)集合。System.Collection.Generic 提供了一个名为 KeyValuePair 的一般结构,其定义如下所示:
KeyValuePair 简单地存储一般键和一般值组成的对。该结构就是词典作为集合进行管理的类型,并且是它用于实现它的 IEnumerable 的类型。Dictionary 类将一般 KeyValuePair 结构指定为
IEnumerable 和 ICollection 的项参数:
KeyValuePair 中使用的键和值类型参数当然是词典自己的一般键和值类型参数。您无疑可以在您自己的使用键和值对的一般数据结构中完成同样的工作。例如:
序列化和泛型
.NET 允许您具有可序列化的一般类型:
在序列化类型时,除了持久保持对象成员的状态以外,.NET 还持久保持有关该对象及其类型的元数据。如果可序列化的类型是一般类型并且它包含绑定类型,则有关该一般类型的元数据还包含有关该绑定类型的类型信息。因此,一般类型的每个带有特定参数类型的变形都被视为唯一的类型。例如,您不能将对象类型
MyClass 序列化(而只能将其反序列化)为 MyClass 类型的对象。序列化一般类型的实例与序列化非一般类型没有什么不同。但是,在反序列化该类型时,您需要通过匹配的特定类型声明变量,并且在向下强制转换从 Deserialize 返回的 Object 时再次指定这些类型。代码块 13 显示了一般类型的序列化和反序列化。
代码块 13. 一般类型的客户端序列化
请注意,IFormatter 是基于对象的。您可以通过定义 IFormatter 的一般版本进行补偿:
您可以通过包含一个基于对象的格式化程序来实现 IGenericFormatter:
请注意一般类型参数 F 上的两个约束的用法。尽管可以原样使用 GenericFormatter<F>:
但是,您还可以将该格式化程序强类型化以使用:
强类型化定义的优点是可以跨文件和程序集共享它,这与 using 别名相反。
代码块 14 与代码块 13 相同,唯一的不同之处在于它使用一般格式化程序:
代码块 14. 使用 IGenericFormatter
泛型和远程处理
可以定义和部署利用泛型的远程类,并且可以使用编程或管理配置。请考虑使用泛型并且派生自 MarshalByRefObject 的类
MyServer。
只有当类型参数 T 是可封送的对象时,您才能通过远程处理访问该类。这意味着 T 是可序列化的类型或者派生自
MarshalByRefObject。您可以通过将 T 约束为派生自 MarshalByRefObject 来实施这一要求。
在使用管理类型注册时,您需要指定要取代一般类型参数而使用的确切类型实参。您必须以与语言无关的方式命名这些类型,并且提供完全限定命名空间。例如,假设类
MyServer 在命名空间 RemoteServer 中的程序集 ServerAssembly 中定义,并且您希望在客户端激活模式下将其与整型而不是一般类型参数 T 一起使用。在该情况下,配置文件中必需的客户端类型注册条目应该是:
配置文件中的匹配主机端类型注册条目是:
双方括号用来指定多个类型。例如:
在使用编程配置时,您可以用类似于 C# 1.1 的方式配置激活模式和类型注册,不同之处在于,当定义远程对象的类型时,您必须提供类型实参而不是一般类型参数。例如,对于主机端激活模式和类型注册,您可以编写如下代码:
对于客户端类型激活模式和位置注册,具有以下代码:
当实例化远程服务器时,只须提供类型参数,就好像您在使用本地一般类型一样:
除了使用 new 以外,客户端还可以选择使用 Activator 类的方法来连接到远程对象。在使用 Activator.GetObject() 时,您需要提供要使用的类型实参,并且在显式强制转换返回的 Object 时提供实参类型:
您还可以将 Activator.CreateInstance() 与一般类型一起使用:
实际上,Activator 还提供 CreateInstance() 的一般版本,定义如下:
CreateInstance() 的使用方式类似于非一般方法,只是添加了类型安全的好处:
泛型无法完成的工作
在 .NET 2.0 下,您不能定义一般 Web 服务,即使用一般类型参数的 Web 方法。原因是没有哪个 Web 服务标准支持一般服务。
您还不能在服务组件上使用一般类型。原因是泛型不能满足 COM 可见性要求,而该要求对于服务组件而言是必需的(就像您无法在 COM 或 COM+ 中使用 C++ 模板一样)。
返回页首
在2005年底微软公司正式发布了C# 2.0,与C# 1.x相比,新版本增加了很多新特性,其中最重要的是对泛型的支持。通过泛型,我们可以定义类型安全的数据结构,而无需使用实际的数据类型。这能显著提高性能并得到更高质量的代码。泛型并不是什么新鲜的东西,他在功能上类似于C++的模板,模板多年前就已存在C++上了,并且在C++上有大量成熟应用。
本文讨论泛型使用的一般问题,比如为什么要使用泛型、泛型的编写方法、泛型中数据类型的约束、泛型中静态成员使用要注意的问题、泛型中方法重载的问、泛型方法等,通过这些使我们可以大致了解泛型并掌握泛型的一般应用,编写出更简单、通用、高效的应用系统。
什么是泛型
我们在编写程序时,经常遇到两个模块的功能非常相似,只是一个是处理int数据,另一个是处理string数据,或者其他自定义的数据类型,但我们没有办法,只能分别写多个方法处理每个数据类型,因为方法的参数类型不同。有没有一种办法,在方法中传入通用的数据类型,这样不就可以合并代码了吗?泛型的出现就是专门解决这个问题的。读完本篇文章,你会对泛型有更深的了解。
为什么要使用泛型
泛型是 2.0 版 C# 语言和公共语言运行库 (CLR) 中的一个新功能。泛型将类型参数的概念引入 .NET Framework,类型参数使得设计如下类和方法成为可能:这些类和方法将一个或多个类型的指定推迟到客户端代码声明并实例化该类或方法的时候。例如,通过使用泛型类型参数 T,您可以编写其他客户端代码能够使用的单个类,而不致引入运行时强制转换或装箱操作的成本或风险
为了了解这个问题,我们先看下面的代码,代码省略了一些内容,但功能是实现一个栈,这个栈只能处理int数据类型:
public class Stack
{
private int[] items;
private int count;
public Stack(int size)
{
items = new int[size];
count = 0;
}
public void Push(int x)
{
//为了代码清晰及只简单熟悉泛型因此不考虑堆栈时超出数组大小的情况
items[count++]=x;
}
public int Pop()
{
//为了代码清晰及只简单熟悉泛型因此不考虑出栈时超出数组已为空的情况
return items[--count];
}
}
class Test
{
static void Main()
{
Stack s = new Stack(10);
s.Push(111);
s.Push(222);
Console.WriteLine(s.Pop()+s.Pop())
}
}
上面代码运行的很好,但是,当我们需要一个栈来保存string类型时,该怎么办呢?很多人都会想到把上面的代码复制一份,把int改成string不就行了。当然,这样做本身是没有任何问题的,但一个优秀的程序是不会这样做的,因为他想到若以后再需要long、Node类型的栈该怎样做呢?还要再复制吗?优秀的程序员会想到用一个通用的数据类型object来实现这个栈:
public class Stack
{
private object[] items;
private int count;
public Stack(int size)
{
items = new object[size];
count = 0;
}
public void Push(object x)
{
items[count++]=x;
}
public object Pop()
{
return items[--count];
}
}
class Test
{
static void Main()
{
Stack s = new Stack(10);
s.Push("111");
s.Push("222");
Console.WriteLine((string)s.Pop()+(string)s.Pop())
}
}
这个栈写的不错,他非常灵活,可以接收任何数据类型,可以说是一劳永逸。但全面地讲,也不是没有缺陷的,主要表现在:
当Stack处理值类型时,会出现装箱、折箱操作,这将在托管堆上分配和回收大量的变量,若数据量大,则性能损失非常严重。在处理引用类型时,虽然没有装箱和折箱操作,但将用到数据类型的强制转换操作,增加处理器的负担。
在数据类型的强制转换上还有更严重的问题(假设stack是Stack的一个实例):
Node1 n1 = new Node1();
stack.Push(n1);
Node2 n2 = (Node2)stack.Pop();
上面的代码在编译时是完全没问题的,但由于Push了一个Node1类型的数据,但在Pop时却要求转换为Node2类型,这将出现程序运行时的类型转换异常,但却逃离了编译器的检查。
针对object类型栈的问题,我们引入泛型,他可以优雅地解决这些问题。泛型用用一个通过的数据类型T来代替object,在类实例化时指定T的类型,运行时(Runtime)自动编译为本地代码,运行效率和代码质量都有很大提高,并且保证数据类型安全。
使用泛型
下面是用泛型来重写上面的栈,用一个通用的数据类型T来作为一个占位符,等待在实例化时用一个实际的类型来代替。让我们来看看泛型的威力:
public class Stack<T>
{
private T[] items;
private int count;
public Stack(int size)
{
items = new T[size];
count = 0;
}
public void Push(T x)
{
items[count++]=x;
}
public T Pop()
{
return items[--count];
}
}
类的写法不变,只是引入了通用数据类型T就可以适用于任何数据类型,并且类型安全的。这个类的调用方法:
//实例化只能保存int类型的类
class Test
{
static void Main()
{
Stack<int> s = new Stack<int>(10);
s.Push(111);
s.Push(222);
Console.WriteLine(s.Pop()+s.Pop())
}
}
//实例化只能保存string类型的类
class Test
{
static void Main()
{
Stack<string> s = new Stack<string>(10);
s.Push("111");
s.Push("222");
Console.WriteLine(s.Pop()+s.Pop())
}
}
这个类和object实现的类有截然不同的区别:
1. 他是类型安全的。实例化了int类型的栈,就不能处理string类型的数据,其他数据类型也一样。
2. 无需装箱和折箱。这个类在实例化时,按照所传入的数据类型生成本地代码,本地代码数据类型已确定,所以无需装箱和折箱。
3. 无需类型转换。
泛型概述
1.使用泛型类型可以最大限度地重用代码、保护类型的安全以及提高性能。
2.泛型最常见的用途是创建集合类。
3..NET Framework 类库在 System.Collections.Generic 命名空间中包含几个新的泛型集合类。应尽可能地使用这些类来代替普通的类,如 System.Collections 命名空间中的 ArrayList。
4.您可以创建自己的泛型接口、泛型类、泛型方法、泛型事件和泛型委托。
5.可以对泛型类进行约束以访问特定数据类型的方法。
6.关于泛型数据类型中使用的类型的信息可在运行时通过反射获取。
//上面的示例已简单说明了泛型的特性如欲想深入了解可以看下面的理论及扩展知识
泛型类实例化的理论
C#泛型类在编译时,先生成中间代码IL,通用类型T只是一个占位符。在实例化类时,根据用户指定的数据类型代替T并由即时编译器(JIT)生成本地代码,这个本地代码中已经使用了实际的数据类型,等同于用实际类型写的类,所以不同的封闭类的本地代码是不一样的。按照这个原理,我们可以这样认为:
泛型类的不同的封闭类是分别不同的数据类型。
例:Stack<int>和Stack<string>是两个完全没有任何关系的类,你可以把他看成类A和类B,这个解释对泛型类的静态成员的理解有很大帮助。
泛型类中数据类型的约束
程序员在编写泛型类时,总是会对通用数据类型T进行有意或无意地有假想,也就是说这个T一般来说是不能适应所有类型,但怎样限制调用者传入的数据类型呢?这就需要对传入的数据类型进行约束,约束的方式是指定T的祖先,即继承的接口或类。因为C#的单根继承性,所以约束可以有多个接口,但最多只能有一个类,并且类必须在接口之前。这时就用到了C#2.0的新增关键字:
public class Node<T, V> where T : Stack, IComparable
where V: Stack
{...}
以上的泛型类的约束表明,T必须是从Stack和IComparable继承,V必须是Stack或从Stack继承,否则将无法通过编译器的类型检查,编译失败。
通用类型T没有特指,但因为C#中所有的类都是从object继承来,所以他在类Node的编写中只能调用object类的方法,这给程序的编写造成了困难。比如你的类设计只需要支持两种数据类型int和string,并且在类中需要对T类型的变量比较大小,但这些却无法实现,因为object是没有比较大小的方法的。了解决这个问题,只需对T进行IComparable约束,这时在类Node里就可以对T的实例执行CompareTo方法了。这个问题可以扩展到其他用户自定义的数据类型。
如果在类Node里需要对T重新进行实例化该怎么办呢?因为类Node中不知道类T到底有哪些构造函数。为了解决这个问题,需要用到new约束:
public class Node<T, V> where T : Stack, new()
where V: IComparable
需要注意的是,new约束只能是无参数的,所以也要求相应的类Stack必须有一个无参构造函数,否则编译失败。
C#中数据类型有两大类:引用类型和值类型。引用类型如所有的类,值类型一般是语言的最基本类型,如int, long, struct等,在泛型的约束中,我们也可以大范围地限制类型T必须是引用类型或必须是值类型,分别对应的关键字是class和struct:
public class Node<T, V> where T : class
where V: struct
泛型方法
泛型不仅能作用在类上,也可单独用在类的方法上,他可根据方法参数的类型自动适应各种参数,这样的方法叫泛型方法。看下面的类:
public class Stack2
{
public void Push<T>(Stack<T> s, params T[] p)
{
foreach (T t in p)
{
s.Push(t);
}
}
}
原来的类Stack一次只能Push一个数据,这个类Stack2扩展了Stack的功能(当然也可以直接写在Stack中),他可以一次把多个数据压入Stack中。其中Push是一个泛型方法,这个方法的调用示例如下:
Stack<int> x = new Stack<int>(100);
Stack2 x2 = new Stack2();
x2.Push(x, 1, 2, 3, 4, 6);
string s = "";
for (int i = 0; i < 5; i++)
{
s += x.Pop().ToString();
} //至此,s的值为64321
泛型中的静态成员变量
在C#1.x中,我们知道类的静态成员变量在不同的类实例间是共享的,并且他是通过类名访问的。C#2.0中由于引进了泛型,导致静态成员变量的机制出现了一些变化:静态成员变量在相同封闭类间共享,不同的封闭类间不共享。
这也非常容易理解,因为不同的封闭类虽然有相同的类名称,但由于分别传入了不同的数据类型,他们是完全不同的类,比如:
Stack<int> a = new Stack<int>();
Stack<int> b = new Stack<int>();
Stack<long> c = new Stack<long>();
类实例a和b是同一类型,他们之间共享静态成员变量,但类实例c却是和a、b完全不同的类型,所以不能和a、b共享静态成员变量。
泛型中的静态构造函数
静态构造函数的规则:只能有一个,且不能有参数,他只能被.NET运行时自动调用,而不能人工调用。
泛型中的静态构造函数的原理和非泛型类是一样的,只需把泛型中的不同的封闭类理解为不同的类即可。以下两种情况可激发静态的构造函数:
1. 特定的封闭类第一次被实例化。
2. 特定封闭类中任一静态成员变量被调用。
泛型类中的方法重载
方法的重载在.Net Framework中被大量应用,他要求重载具有不同的签名。在泛型类中,由于通用类型T在类编写时并不确定,所以在重载时有些注意事项,这些事项我们通过以下的例子说明:
public class Node<T, V>
{
public T add(T a, V b) //第一个add
{
return a;
}
public T add(V a, T b) //第二个add
{
return b;
}
public int add(int a, int b) //第三个add
{
return a + b;
}
}
上面的类很明显,如果T和V都传入int的话,三个add方法将具有同样的签名,但这个类仍然能通过编译,是否会引起调用混淆将在这个类实例化和调用add方法时判断。请看下面调用代码:
Node<int, int> node = new Node<int, int>();
object x = node.add(2, 11);
这个Node的实例化引起了三个add具有同样的签名,但却能调用成功,因为他优先匹配了第三个add。但如果删除了第三个add,上面的调用代码则无法编译通过,提示方法产生的混淆,因为运行时无法在第一个add和第二个add之间选择。
Node<string, int> node = new Node<string, int>();
object x = node.add(2, "11");
这两行调用代码可正确编译,因为传入的string和int,使三个add具有不同的签名,当然能找到唯一匹配的add方法。
由以上示例可知,C#的泛型是在实例的方法被调用时检查重载是否产生混淆,而不是在泛型类本身编译时检查。同时还得出一个重要原则:
当一般方法与泛型方法具有相同的签名时,会覆盖泛型方法。
泛型类的方法重写
方法重写(override)的主要问题是方法签名的识别规则,在这一点上他与方法重载一样,请参考泛型类的方法重载。
泛型的使用范围
本文主要是在类中讲述泛型,实际上,泛型还可以用在类方法、接口、结构(struct)、委托等上面使用,使用方法大致相同,就不再讲述。
继承和泛型
在从一般基类派生时,必须提供类型实参,而不是该基类的一般类型参数:public class BaseClass {...} public class SubClass : BaseClass {...}
如果子类是一般的而非具体的类型实参,则可以使用子类一般类型参数作为一般基类的指定类型:
public class SubClass : BaseClass {...}
在使用子类一般类型参数时,必须在子类级别重复在基类级别规定的任何约束。例如,派生约束:
public class BaseClass where T : ISomeInterface {...} public class SubClass : BaseClass where T : ISomeInterface {...}
或构造函数约束:
public class BaseClass where T : new() { public T SomeMethod() { return new T(); } } public class SubClass : BaseClass where T : new() {...}
基类可以定义其签名使用一般类型参数的虚拟方法。在重写它们时,子类必须在方法签名中提供相应的类型:
public class BaseClass { public virtual T SomeMethod() {...} } public class SubClass: BaseClass<int> { public override int SomeMethod() {...} }
如果该子类是一般类型,则它还可以在重写时使用它自己的一般类型参数:
public class SubClass: BaseClass { public override T SomeMethod() {...} }
您可以定义一般接口、一般抽象类,甚至一般抽象方法。这些类型的行为像其他任何一般基类型一样:
public interface ISomeInterface { T SomeMethod(T t); } public abstract class BaseClass { public abstract T SomeMethod(T t); } public class SubClass : BaseClass { public override T SomeMethod(T t) {...) }
一般抽象方法和一般接口有一种有趣的用法。在 C# 2.0 中,不能对一般类型参数使用诸如 + 或 += 之类的运算符。例如,以下代码无法编译,因为 C# 2.0 不具有运算符约束:
public class Calculator { public T Add(T arg1,T arg2) { return arg1 + arg2;//Does not compile } //Rest of the methods }
但是,您可以通过定义一般操作,使用抽象方法(最好使用接口)进行补偿。由于抽象方法的内部不能具有任何代码,因此可以在基类级别指定一般操作,并且在子类级别提供具体的类型和实现:
public abstract class BaseCalculator { public abstract T Add(T arg1,T arg2); public abstract T Subtract(T arg1,T arg2); public abstract T Divide(T arg1,T arg2); public abstract T Multiply(T arg1,T arg2); } public class MyCalculator : BaseCalculator { public override int Add(int arg1, int arg2) { return arg1 + arg2; } //Rest of the methods }
一般接口还可以产生更加干净一些的解决方案:
public interface ICalculator { T Add(T arg1,T arg2); //Rest of the methods } public class MyCalculator : ICalculator { public int Add(int arg1, int arg2) { return arg1 + arg2; } //Rest of the methods }
返回页首
一般方法
在 C# 2.0 中,方法可以定义特定于其执行范围的一般类型参数:public class MyClass { public void MyMethod(X x) {...} }
这是一种重要的功能,因为它使您可以每次用不同的类型调用该方法,而这对于实用工具类非常方便。
即使包含类根本不使用泛型,您也可以定义方法特定的一般类型参数:
public class MyClass { public void MyMethod(T t) {...} }
该功能仅适用于方法。属性或索引器只能使用在类的作用范围中定义的一般类型参数。
在调用定义了一般类型参数的方法时,您可以提供要在调用场所使用的类型:
MyClass obj = new MyClass(); obj.MyMethod(3);
因此,当调用该方法时,C# 编译器将足够聪明,从而基于传入的参数的类型推断出正确的类型,并且它允许完全省略类型规范:
MyClass obj = new MyClass(); obj.MyMethod(3);
该功能称为一般类型推理。请注意,编译器无法只根据返回值的类型推断出类型:
public class MyClass { public T MyMethod() {} } MyClass obj = new MyClass(); int number = obj.MyMethod();//Does not compile
当方法定义它自己的一般类型参数时,它还可以定义这些类型的约束:
public class MyClass { public void SomeMethod(T t) where T : IComparable {...} }
但是,您无法为类级别一般类型参数提供方法级别约束。类级别一般类型参数的所有约束都必须在类作用范围中定义。
在重写定义了一般类型参数的虚拟方法时,子类方法必须重新定义该方法特定的一般类型参数:
public class BaseClass { public virtual void SomeMethod(T t) {...} } public class SubClass : BaseClass { public override void SomeMethod(T t) {...} }
子类实现必须重复在基础方法级别出现的所有约束:
public class BaseClass { public virtual void SomeMethod(T t) where T : new() {...} } public class SubClass : BaseClass { public override void SomeMethod(T t) where T : new() {...} }
请注意,方法重写不能定义没有在基础方法中出现的新约束。
此外,如果子类方法调用虚拟方法的基类实现,则它必须指定要代替一般基础方法类型参数使用的类型实参。您可以自己显式指定它,或者依靠类型推理(如果可用):
public class BaseClass { public virtual void SomeMethod(T t) {...} } public class SubClass : BaseClass { public override void SomeMethod(T t) { base.SomeMethod(t); base.SomeMethod(t); } }
一般静态方法
C# 允许定义使用一般类型参数的静态方法。但是,在调用这样的静态方法时,您需要在调用场所为包含类提供具体的类型,如下面的示例所示:
public class MyClass { public static T SomeMethod(T t) {...} } int number = MyClass.SomeMethod(3);
静态方法可以定义方法特定的一般类型参数和约束,就像实例方法一样。在调用这样的方法时,您需要在调用场所提供方法特定的类型 — 可以按如下方式显式提供:
public class MyClass { public static T SomeMethod(T t,X x) {..} } int number = MyClass.SomeMethod(3,"AAA");
或者依靠类型推理(如果可能):
int number = MyClass.SomeMethod(3,"AAA");
一般静态方法遵守施加于它们在类级别使用的一般类型参数的所有约束。就像实例方法一样,您可以为由静态方法定义的一般类型参数提供约束:
public class MyClass { public static T SomeMethod(T t) where T : IComparable {...} }
C# 中的运算符只是静态方法而已,并且 C# 允许您为自己的一般类型重载运算符。假设代码块 3 的一般 LinkedList 提供了用于串联链表的 + 运算符。+ 运算符使您能够编写下面这段优美的代码:
LinkedList list1 = new LinkedList(); LinkedList list2 = new LinkedList(); ... LinkedList list3 = list1+list2;
代码块 7 显示 LinkedList 类上的一般 + 运算符的实现。请注意,运算符不能定义新的一般类型参数。
代码块 7. 实现一般运算符
public class LinkedList { public static LinkedList operator+(LinkedList lhs, LinkedList rhs) { return concatenate(lhs,rhs); } static LinkedList concatenate(LinkedList list1, LinkedList list2) { LinkedList newList = new LinkedList(); Node current; current = list1.m_Head; while(current != null) { newList.AddHead(current.Key,current.Item); current = current.NextNode; } current = list2.m_Head; while(current != null) { newList.AddHead(current.Key,current.Item); current = current.NextNode; } return newList; } //Rest of LinkedList }
返回页首
一般委托
在某个类中定义的委托可以利用该类的一般类型参数。例如:public class MyClass { public delegate void GenericDelegate(T t); public void SomeMethod(T t) {...} }
在为包含类指定类型时,也会影响到委托:
MyClass obj = new MyClass(); MyClass.GenericDelegate del; del = new MyClass.GenericDelegate(obj.SomeMethod); del(3);
C# 2.0 使您可以将方法引用的直接分配转变为委托变量:
MyClass obj = new MyClass(); MyClass.GenericDelegate del; del = obj.SomeMethod;
我将把该功能称为委托推理。编译器能够推断出您分配到其中的委托的类型,查明目标对象是否具有采用您指定的名称的方法,并且验证该方法的签名匹配。然后,编译器创建所推断出的参数类型(包括正确的类型而不是一般类型参数)的新委托,并且将新委托分配到推断出的委托中。
像类、结构和方法一样,委托也可以定义一般类型参数:
public class MyClass { public delegate void GenericDelegate(T t,X x); }
在类的作用范围外部定义的委托可以使用一般类型参数。在该情况下,在声明和实例化委托时,必须为其提供类型实参:
public delegate void GenericDelegate(T t); public class MyClass { public void SomeMethod(int number) {...} } MyClass obj = new MyClass(); GenericDelegate del; del = new GenericDelegate(obj.SomeMethod); del(3);
另外,还可以在分配委托时使用委托推理:
MyClass obj = new MyClass(); GenericDelegate del; del = obj.SomeMethod;
当然,委托可以定义约束以伴随它的一般类型参数:
public delegate void MyDelegate(T t) where T : IComparable;
委托级别约束只在使用端实施(在声明委托变量和实例化委托对象时),类似于在类型或方法的作用范围中实施的其他任何约束。
一般委托对于事件尤其有用。您可以精确地定义一组有限的一般委托(只按照它们需要的一般类型参数的数量进行区分),并且使用这些委托来满足所有事件处理需要。代码块 8 演示了一般委托和一般事件处理方法的用法。
代码块 8. 一般事件处理
public delegate void GenericEventHandler (S sender,A args); public class MyPublisher { public event GenericEventHandler MyEvent; public void FireEvent() { MyEvent(this,EventArgs.Empty); } } public class MySubscriber //Optional: can be a specific type { public void SomeMethod(MyPublisher sender,A args) {...} } MyPublisher publisher = new MyPublisher(); MySubscriber subscriber = new MySubscriber(); publisher.MyEvent += subscriber.SomeMethod;
代码块 8 使用名为 GenericEventHandler 的一般委托,它接受一般发送者类型和一般类型参数。显然,如果您需要更多的参数,则可以简单地添加更多的一般类型参数,但是我希望模仿按如下方式定义的 .NET
EventHandler 来设计 GenericEventHandler:
public void delegate EventHandler(object sender,EventArgs args);
与 EventHandler 不同,GenericEventHandler 是类型安全的(如代码块 8 所示),因为它只接受
MyPublisher 类型的对象(而不是纯粹的 Object)作为发送者。实际上,.NET 已经在 System 命名空间中定义了一般样式的
EventHandler:
public void delegate EventHandler(object sender,A args) where A : EventArgs;
返回页首
泛型和反射
在 .NET 2.0 中,扩展了反射以支持一般类型参数。类型 Type 现在可以表示带有特定类型实参(称为绑定类型)或未指定(未绑定)类型的一般类型。像 C# 1.1 中一样,您可以通过使用typeof 运算符或者通过调用每个类型支持的 GetType() 方法来获得任何类型的 Type。不管您选择哪种方式,都会产生相同的 Type。例如,在以下代码示例中,type1 与
type2 完全相同。
LinkedList list = new LinkedList(); Type type1 = typeof(LinkedList); Type type2 = list.GetType(); Debug.Assert(type1 == type2);
typeof 和 GetType() 都可以对一般类型参数进行操作:
public class MyClass { public void SomeMethod(T t) { Type type = typeof(T); Debug.Assert(type == t.GetType()); } }
此外,typeof 运算符还可以对未绑定的一般类型进行操作。例如:
public class MyClass {} Type unboundedType = typeof(MyClass<>); Trace.WriteLine(unboundedType.ToString()); //Writes: MyClass`1[T]
所追踪的数字 1 是所使用的一般类型的一般类型参数的数量。请注意空 <> 的用法。要对带有多个类型参数的未绑定一般类型进行操作,请在
<> 中使用“,”:
public class LinkedList {...} Type unboundedList = typeof(LinkedList<,>); Trace.WriteLine(unboundedList.ToString()); //Writes: LinkedList`2[K,T]
Type 具有新的方法和属性,用于提供有关该类型的一般方面的反射信息。代码块 9 显示了新方法。
代码块 9. Type 的一般反射成员
public abstract class Type : //Base types { public virtual bool ContainsGenericParameters{get;} public virtual int GenericParameterPosition{get;} public virtual bool HasGenericArguments{get;} public virtual bool IsGenericParameter{get;} public virtual bool IsGenericTypeDefinition{get;} public virtual Type BindGenericParameters(Type[] typeArgs); public virtual Type[] GetGenericArguments(); public virtual Type GetGenericTypeDefinition(); //Rest of the members }
上述新成员中最有用的是 HasGenericArguments 属性,以及 GetGenericArguments() 和
GetGenericTypeDefinition() 方法。Type 的其余新成员用于高级的且有点深奥的方案,这些方案超出了本文的范围。
正如它的名称所指示的那样,如果由 Type 对象表示的类型使用一般类型参数,则 HasGenericArguments 被设置为 true。GetGenericArguments() 返回与所使用的类型参数相对应的 Type 数组。GetGenericTypeDefinition() 返回一个表示基础类型的一般形式的 Type。代码块 10 演示如何使用上述一般处理 Type
成员获得有关代码块 3 中的 LinkedList 的一般反射信息。
代码块 10. 使用 Type 进行一般反射
LinkedList list = new LinkedList(); Type boundedType = list.GetType(); Trace.WriteLine(boundedType.ToString()); //Writes: LinkedList`2[System.Int32,System.String] Debug.Assert(boundedType.HasGenericArguments); Type[] parameters = boundedType.GetGenericArguments(); Debug.Assert(parameters.Length == 2); Debug.Assert(parameters[0] == typeof(int)); Debug.Assert(parameters[1] == typeof(string)); Type unboundedType = boundedType.GetGenericTypeDefinition(); Debug.Assert(unboundedType == typeof(LinkedList<,>)); Trace.WriteLine(unboundedType.ToString()); //Writes: LinkedList`2[K,T]
与 Type 类似,MethodInfo 和它的基类 MethodBase 具有反射一般方法信息的新成员。
与 C# 1.1 中一样,您可以使用 MethodInfo(以及很多其他选项)进行晚期绑定调用。但是,您为晚期绑定传递的参数的类型,必须与取代一般类型参数而使用的绑定类型(如果有)相匹配:
LinkedList list = new LinkedList(); Type type = list.GetType(); MethodInfo methodInfo = type.GetMethod("AddHead"); object[] args = {1,"AAA"}; methodInfo.Invoke(list,args);
属性和泛型
在定义属性时,可以使用枚举 AttributeTargets 的新 GenericParameter 值,通知编译器属性应当以一般类型参数为目标:
[AttributeUsage(AttributeTargets.GenericParameter)] public class SomeAttribute : Attribute {...}
请注意,C# 2.0 不允许定义一般属性。
//Does not compile: public class SomeAttribute : Attribute {...}
然而,属性类可以通过使用一般类型或者定义 Helper 一般方法(像其他任何类型一样)在内部利用泛型:
public class SomeAttribute : Attribute { void SomeMethod(T t) {...} LinkedList m_List = new LinkedList(); }
返回页首
泛型和 .NET Framework
为了对本文做一下小结,下面介绍 .NET 中除 C# 本身以外的其他一些领域如何利用泛型或者与泛型交互。System.Array 和泛型
System.Array 类型通过很多一般静态方法进行了扩展。这些一般静态方法专门用于自动执行和简化处理数组的常见任务,例如,遍历数组并且对每个元素执行操作、扫描数组,以查找匹配某个条件(谓词)的值、对数组进行变换和排序等等。代码块 11 是这些静态方法的部分清单。
代码块 11. System.Array 的一般方法
public abstract class Array { //Partial listing of the static methods: public static IList AsReadOnly(T[] array); public static int BinarySearch(T[] array, T value); public static int BinarySearch(T[] array, T value, IComparer comparer); public static U[] ConvertAll(T[] array, Converter converter); public static bool Exists(T[] array,Predicate match); public static T Find(T[] array,Predicate match); public static T[] FindAll(T[] array, Predicate match); public static int FindIndex(T[] array, Predicate match); public static void ForEach(T[] array, Action action); public static int IndexOf(T[] array, T value); public static void Sort(K[] keys, V[] items, IComparer comparer); public static void Sort(T[] array,Comparison comparison) }
System.Array 的静态一般方法都使用 System 命名空间中定义的下列四个一般委托:
public delegate void Action(T t); public delegate int Comparison(T x, T y); public delegate U Converter(T from); public delegate bool Predicate(T t);
代码块 12 演示如何使用这些一般方法和委托。它用从 1 到 20 的所有整数初始化一个数组。然后,代码通过一个匿名方法和
Action 委托,使用 Array.ForEach() 方法来跟踪这些数字。使用第二个匿名方法和
Predicate 委托,代码通过调用 Array.FindAll() 方法(它返回另一个相同的一般类型的数组),来查找该数组中的所有质数。最后,使用相同的
Action 委托和匿名方法来跟踪这些质数。请注意代码块 12 中类型参数推理的用法。您在使用静态方法时无须指定类型参数。
代码块 12. 使用 System.Array 的一般方法
int[] numbers = {1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20}; Action trace = delegate(int number) { Trace.WriteLine(number); }; Predicate isPrime = delegate(int number) { switch(number) { case 1:case 2:case 3:case 5:case 7: case 11:case 13:case 17:case 19: return true; default: return false; } }; Array.ForEach(numbers,trace); int[] primes = Array.FindAll(numbers,isPrime); Array.ForEach(primes,trace);
在 System.Collections.Generic 命名空间中定义的类 List 中,也可以得到类似的一般方法。这些方法使用四个相同的一般委托。实际上,您还可以在您的代码中利用这些委托,如以下部分所示。
静态集合类
尽管 System.Array 和 List 都提供了能够大大简化自身使用方式的、方便的实用工具方法,但 .NET 没有为其他集合提供这样的支持。为了对此进行补偿,本文随附的源代码包含了静态 Helper 类
Collection,其定义如下所示:
public static class Collection { public static IList AsReadOnly(IEnumerable collection); public static U[] ConvertAll(IEnumerable collection, Converter converter); public static bool Contains(IEnumerable collection,T item) where T : IComparable; public static bool Exists(IEnumerable collection,Predicate); public static T Find(IEnumerable collection,Predicate match); public static T[] FindAll(IEnumerable collection, Predicate match); public static int FindIndex(IEnumerable collection,T value) where T : IComparable; public static T FindLast(IEnumerable collection, Predicate match); public static int FindLastIndex(IEnumerable collection,T value) where T : IComparable; public static void ForEach(IEnumerable collection,Action action); public static T[] Reverse(IEnumerable collection); public static T[] Sort(IEnumerable collection); public static T[] ToArray(IEnumerable collection); public static bool TrueForAll(IEnumerable collection, Predicate match); //Overloaded versions for IEnumerator }
Collection 的实现简单易懂。例如,以下为 ForEach() 方法:
public static void ForEach(IEnumerator iterator,Action action) { /* Some parameter checking here, then: */ while(iterator.MoveNext()) { action(iterator.Current); } }
Collection 静态类的用法非常类似于 Array 和 List,它们都利用相同的一般委托。您可以将
Collection 用于任何集合,只要该集合支持 IEnumerable 或
IEnumerator:
Queue queue = new Queue(); //Some code to initialize queue Action trace = delegate(int number) { Trace.WriteLine(number); }; Collection.ForEach(queue,trace);
一般集合
System.Collections 中的数据结构全部都是基于 Object 的,因而继承了本文开头描述的两个问题,即性能较差和缺少类型安全。.NET 2.0 在 System.Collections.Generic 命名空间中引入了一组一般集合。例如,有一般的
Stack 类和一般的 Queue 类。Dictionary 数据结构等效于非一般的
HashTable,并且还有一个有点像 SortedList 的 SortedDictionary 类。类
List 类似于非一般的 ArrayList。表 1 将 System.Collections.Generic 的主要类型映射到 System.Collections 中的那些主要类型。
表 1. 将 System.Collections.Generic 映射到 System.Collections | |
System.Collections.Generic | System.Collections |
Comparer | Comparer |
Dictionary | HashTable |
LinkedList | - |
List | ArrayList |
Queue | Queue |
SortedDictionary | SortedList |
Stack | Stack |
ICollection | ICollection |
IComparable | System.IComparable |
IDictionary | IDictionary |
IEnumerable | IEnumerable |
IEnumerator | IEnumerator |
IList | IList |
public interface IEnumerable { IEnumerator GetEnumerator(); } public interface IEnumerator : IDisposable { T Current{get;} bool MoveNext(); }
简单说来,IEnumerable 提供了对 IEnumerator 迭代器接口的访问,该接口用于对集合进行抽象迭代。所有集合都在嵌套结构上实现了
IEnumerable,其中,一般类型参数 T 是集合存储的类型。
特别有趣的是,词典集合定义它们的迭代器的方式。词典实际上是两个类型(而非一个类型)的一般参数(键和值)集合。System.Collection.Generic 提供了一个名为 KeyValuePair 的一般结构,其定义如下所示:
struct KeyValuePair { public KeyValuePair(K key,V value); public K Key(get;set;) public V Value(get;set;) }
KeyValuePair 简单地存储一般键和一般值组成的对。该结构就是词典作为集合进行管理的类型,并且是它用于实现它的 IEnumerable 的类型。Dictionary 类将一般 KeyValuePair 结构指定为
IEnumerable 和 ICollection 的项参数:
public class Dictionary : IEnumerable<KEYVALUEPAIR>, ICollection<KEYVALUEPAIR>, //More interfaces {...}
KeyValuePair 中使用的键和值类型参数当然是词典自己的一般键和值类型参数。您无疑可以在您自己的使用键和值对的一般数据结构中完成同样的工作。例如:
public class LinkedList : IEnumerable<KEYVALUEPAIR> where K : IComparable {...}
序列化和泛型
.NET 允许您具有可序列化的一般类型:
[Serializable] public class MyClass {...}
在序列化类型时,除了持久保持对象成员的状态以外,.NET 还持久保持有关该对象及其类型的元数据。如果可序列化的类型是一般类型并且它包含绑定类型,则有关该一般类型的元数据还包含有关该绑定类型的类型信息。因此,一般类型的每个带有特定参数类型的变形都被视为唯一的类型。例如,您不能将对象类型
MyClass 序列化(而只能将其反序列化)为 MyClass 类型的对象。序列化一般类型的实例与序列化非一般类型没有什么不同。但是,在反序列化该类型时,您需要通过匹配的特定类型声明变量,并且在向下强制转换从 Deserialize 返回的 Object 时再次指定这些类型。代码块 13 显示了一般类型的序列化和反序列化。
代码块 13. 一般类型的客户端序列化
[Serializable] public class MyClass {...}
MyClass obj1 = new MyClass();
IFormatter formatter = new BinaryFormatter();?
Stream stream = new FileStream("obj.bin",FileMode.Create,FileAccess.ReadWrite);
using(stream)
{
formatter.Serialize(stream,obj1);
stream.Seek(0,SeekOrigin.Begin);
MyClass obj2;
obj2 = (MyClass)formatter.Deserialize(stream);
}
请注意,IFormatter 是基于对象的。您可以通过定义 IFormatter 的一般版本进行补偿:
public interface IGenericFormatter { T Deserialize(Stream serializationStream); void Serialize(Stream serializationStream,T graph); }
您可以通过包含一个基于对象的格式化程序来实现 IGenericFormatter:
public class GenericFormatter : IGenericFormatter where F : IFormatter,new() { IFormatter m_Formatter = new F(); public T Deserialize(Stream serializationStream) { return (T)m_Formatter.Deserialize(serializationStream); } public void Serialize(Stream serializationStream,T graph) { m_Formatter.Serialize(serializationStream,graph); } }
请注意一般类型参数 F 上的两个约束的用法。尽管可以原样使用 GenericFormatter<F>:
using GenericBinaryFormatter = GenericFormatter; using GenericSoapFormatter2 = GenericFormatter;
但是,您还可以将该格式化程序强类型化以使用:
public sealed class GenericBinaryFormatter : GenericFormatter {} public sealed class GenericSoapFormatter : GenericFormatter {}
强类型化定义的优点是可以跨文件和程序集共享它,这与 using 别名相反。
代码块 14 与代码块 13 相同,唯一的不同之处在于它使用一般格式化程序:
代码块 14. 使用 IGenericFormatter
[Serializable] public class MyClass {...}
MyClass obj1 = new MyClass();
IGenericFormatter formatter = new GenericBinaryFormatter();?
Stream stream = new FileStream("obj.bin",FileMode.Create,FileAccess.ReadWrite);
using(stream)
{
formatter.Serialize(stream,obj1);
stream.Seek(0,SeekOrigin.Begin);
MyClass obj2;
obj2 = formatter.Deserialize(stream);
}
泛型和远程处理
可以定义和部署利用泛型的远程类,并且可以使用编程或管理配置。请考虑使用泛型并且派生自 MarshalByRefObject 的类
MyServer。
public class MyServer : MarshalByRefObject {...}
只有当类型参数 T 是可封送的对象时,您才能通过远程处理访问该类。这意味着 T 是可序列化的类型或者派生自
MarshalByRefObject。您可以通过将 T 约束为派生自 MarshalByRefObject 来实施这一要求。
public class MyServer : MarshalByRefObject where T : MarshalByRefObject {...}
在使用管理类型注册时,您需要指定要取代一般类型参数而使用的确切类型实参。您必须以与语言无关的方式命名这些类型,并且提供完全限定命名空间。例如,假设类
MyServer 在命名空间 RemoteServer 中的程序集 ServerAssembly 中定义,并且您希望在客户端激活模式下将其与整型而不是一般类型参数 T 一起使用。在该情况下,配置文件中必需的客户端类型注册条目应该是:
<client url="...some url goes here..."> <activated type="RemoteServer.MyServer<b>[[System.Int32]]</b>,ServerAssembly"/> </client>
配置文件中的匹配主机端类型注册条目是:
<service> <activated type="RemoteServer.MyServer<b>[[System.Int32]]</b>,ServerAssembly"/> </service>
双方括号用来指定多个类型。例如:
LinkedList[[System.Int32],[System.String]]
在使用编程配置时,您可以用类似于 C# 1.1 的方式配置激活模式和类型注册,不同之处在于,当定义远程对象的类型时,您必须提供类型实参而不是一般类型参数。例如,对于主机端激活模式和类型注册,您可以编写如下代码:
Type serverType = typeof(MyServer); RemotingConfiguration.RegisterActivatedServiceType(serverType);
对于客户端类型激活模式和位置注册,具有以下代码:
Type serverType = typeof(MyServer); string url = ...; //some url initialization RemotingConfiguration.RegisterWellKnownClientType(serverType,url);
当实例化远程服务器时,只须提供类型参数,就好像您在使用本地一般类型一样:
MyServer obj; obj = new MyServer(); //Use obj
除了使用 new 以外,客户端还可以选择使用 Activator 类的方法来连接到远程对象。在使用 Activator.GetObject() 时,您需要提供要使用的类型实参,并且在显式强制转换返回的 Object 时提供实参类型:
string url = ...; //some url initialization Type serverType = typeof(MyServer); MyServer obj; obj = (MyServer)Activator.GetObject(serverType,url); //Use obj
您还可以将 Activator.CreateInstance() 与一般类型一起使用:
Type serverType = typeof(MyServer); MyServer obj; obj = (MyServer)Activator.CreateInstance(serverType); //Use obj
实际上,Activator 还提供 CreateInstance() 的一般版本,定义如下:
T CreateInstance<T>();
CreateInstance() 的使用方式类似于非一般方法,只是添加了类型安全的好处:
Type serverType = typeof(MyServer); MyServer obj; obj = Activator.CreateInstance(serverType); //Use obj
泛型无法完成的工作
在 .NET 2.0 下,您不能定义一般 Web 服务,即使用一般类型参数的 Web 方法。原因是没有哪个 Web 服务标准支持一般服务。
您还不能在服务组件上使用一般类型。原因是泛型不能满足 COM 可见性要求,而该要求对于服务组件而言是必需的(就像您无法在 COM 或 COM+ 中使用 C++ 模板一样)。
返回页首
小结
C# 泛型是开发工具库中的一个无价之宝。它们可以提高性能、类型安全和质量,减少重复性的编程任务,简化总体编程模型,而这一切都是通过优雅的、可读性强的语法完成的。尽管 C# 泛型的根基是 C++ 模板,但 C# 通过提供编译时安全和支持将泛型提高到了一个新水平。C# 利用了两阶段编译、元数据以及诸如约束和一般方法之类的创新性的概念。毫无疑问,C# 的将来版本将继续发展泛型,以便添加新的功能,并且将泛型扩展到诸如数据访问或本地化之类的其他 .NET Framework 领域。相关文章推荐
- C#学习笔记(十一)-实现Windows程序的数据绑定
- VS2010轻松学习C#-从零到深入-天轰穿.NET4趣味编程视频教程
- C#学习笔记(十一):动态类型
- DotNet(C#)学习-你学到什么程度!_C#教程
- c#学习笔记之十一 直播 interface 的学习
- jQuery学习教程十一: jQuery - 获得内容和属性
- C#进阶教程(十一)
- C# 学习黑马.Net视频教程,大文件拷贝
- 挑战C#学习的最快速度_C#教程
- DotNet(C#)学习-你学到什么程度!_C#教程
- C# 学习教程之一
- 学习【OpenCV入门教程之十一】 形态学图像处理(二)开运算,闭运算,梯度运算,顶帽,黑帽---思维导图笔记
- C#温故而知新学习系列之面向对象编程—自动属性(十一)
- C#学习-图解教程(2):访问修饰符(其中两种)
- mybatis学习教程中级(十一)mybatis和spring框架整合(前奏篇2)
- 【C#】菜鸟教程学习笔记(一)
- C#学习笔记(4)_C#教程
- C#学习教程之三
- 天轰穿C#教程之C#基础的学习路线
- Visual Studio 2017中使用正则修改部分内容 如何使用ILAsm与ILDasm修改.Net exe(dll)文件 C#学习-图解教程(1):格式化数字字符串 小程序开发之图片转Base64(C#、.Net) jquery遍历table为每一个单元格取值及赋值 。net加密解密相关方法 .net关于坐标之间一些简单操作