C#泛型全面讲解

发布时间:2020-02-05编辑:脚本学堂
本文是有关C#泛型的非常全面的内容讲解,系统的介绍了C#泛型的相关知识与实例,正在学习C#泛型的朋友,建议参考下本文的介绍。

这也非常容易理解,因为不同的封闭类虽然有相同的类名称,但由于分别传入了不同的数据类型,他们是完全不同的类,比如:
 

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# 泛型是开发工具库中的一个无价之宝。它们可以提高性能、类型安全和质量,减少重复性的编程任务,简化总体编程模型,而这一切都是通过优雅的、可读性强的语 法完成的。尽管 C# 泛型的根基是 C++ 模板,但 C# 通过提供编译时安全和支持将泛型提高到了一个新水平。C# 利用了两阶段编译、元数据以及诸如约束和一般方法之类的创新性的概念。毫无疑问,C# 的将来版本将继续发展泛型,以便添加新的功能,并且将泛型扩展到诸如数据访问或本地化之类的其他 .NET Framework 领域。

C#泛型编程
泛型:通过参数化类型来实现在同一份代码上操作多种数据类型。利用“参数化类型”将类型抽象化,从而实现灵活的复用。
例子:
 

class Program
{
static void Main(string[] args)
{
int obj = 2;
Test<int> test = new Test<int>(obj);
Console.WriteLine("int:" + test.obj);
string obj2 = "hello world";
Test<string> test1 = new Test<string>(obj2);
Console.WriteLine("String:" + test1.obj);
Console.Read();
}
}

class Test<T>
{
public T obj;
public Test(T obj)
{
this.obj = obj;
}
}

输出结果是:
int:2
String:hello world

程序分析:
1、 Test是一个泛型类。T是要实例化的范型类型。如果T被实例化为int型,那么成员变量obj就是int型的,如果T被实例化为string型,那么obj就是string类型的。
2、 根据不同的类型,上面的程序显示出不同的值。

C#泛型机制:
C#泛型能力有CLR在运行时支持:C#泛型代码在编译为IL代码和元数据时,采用特殊的占位符来表示范型类型,并用专有的IL指令支持泛型操作。而真正的泛型实例化工作以“on-demand”的方式,发生在JIT编译时。

看看刚才的代码中Main函数的元数据
 

.method private hidebysig static void Main(string[] args) cil managed
{
.entrypoint
// Code size 79 (0x4f)
.maxstack 2
.locals init ([0] int32 obj,
[1] class CSharpStudy1.Test`1<int32> test,
[2] string obj2,
[3] class CSharpStudy1.Test`1<string> test1)
IL_0000: nop
IL_0001: ldc.i4.2
IL_0002: stloc.0
IL_0003: ldloc.0
IL_0004: newobj instance void class CSharpStudy1.Test`1<int32>::.ctor(!0)
IL_0009: stloc.1
IL_000a: ldstr "int:"
IL_000f: ldloc.1
IL_0010: ldfld !0 class CSharpStudy1.Test`1<int32>::obj
IL_0015: box [mscorlib]System.Int32
IL_001a: call string [mscorlib]System.String::Concat(object,
object)
IL_001f: call void [mscorlib]System.Console::WriteLine(string)
IL_0024: nop
IL_0025: ldstr "hello world"
IL_002a: stloc.2
IL_002b: ldloc.2
IL_002c: newobj instance void class CSharpStudy1.Test`1<string>::.ctor(!0)
IL_0031: stloc.3
IL_0032: ldstr "String:"
IL_0037: ldloc.3
IL_0038: ldfld !0 class CSharpStudy1.Test`1<string>::obj
IL_003d: call string [mscorlib]System.String::Concat(string,
string)
IL_0042: call void [mscorlib]System.Console::WriteLine(string)
IL_0047: nop
IL_0048: call int32 [mscorlib]System.Console::Read()
IL_004d: pop
IL_004e: ret
} // end of method Program::Main

再来看看Test类中构造函数的元数据
 

.method public hidebysig specialname rtspecialname
instance void .ctor(!T obj) cil managed
{
// Code size 17 (0x11)
.maxstack 8
IL_0000: ldarg.0
IL_0001: call instance void [mscorlib]System.Object::.ctor()
IL_0006: nop
IL_0007: nop
IL_0008: ldarg.0
IL_0009: ldarg.1
IL_000a: stfld !0 class ConsoleCSharpTest1.Test`1<!T>::obj
IL_000f: nop
IL_0010: ret
} // end of method Test`1::.ctor

1、第一轮编译时,编译器只为Test<T>类型产生“泛型版”的IL代码与元数据——并不进行泛型的实例化,T在中间只充当占位符。例如:Test类型元数据中显示的<!T>
2、JIT编译时,当JIT编译器第一次遇到Test<int>时,将用int替换“范型版”IL代码与元数据中的T——进行泛型类型的实例化。例如:Main函数中显示的<int>
3、CLR为所有类型参数为“引用类型”的泛型类型产生同一份代码;但是如果类型参数为“值类型”,对每一个不同的“值类型”,CLR将为其产生一份独立的代码。因为实例化一个引用类型的泛型,它在内存中分配的大小是一样的,但是当实例化一个值类型的时候,在内存中分配的大小是不一样的。

C#泛型特点:
1、如果实例化泛型类型的参数相同,那么JIT编辑器会重复使用该类型,因此C#的动态泛型能力避免了C++静态模板可能导致的代码膨胀的问题。
2、C#泛型类型携带有丰富的元数据,因此C#的泛型类型可以应用于强大的反射技术。
3、C#的泛型采用“基类、接口、构造器,值类型/引用类型”的约束方式来实现对类型参数的“显示约束”,提高了类型安全的同时,也丧失了C++模板基于“签名”的隐式约束所具有的高灵活性

C#泛型继承:
C#除了可以单独声明泛型类型(包括类与结构)外,也可以在基类中包含泛型类型的声明。但基类如果是泛型类,它的类型要么以实例化,要么来源于子类(同样是泛型类型)声明的类型参数,看如下类型
 

class C<U,V>
class D:C<string,int>
class E<U,V>:C<U,V>
class F<U,V>:C<string,int>
class G:C<U,V> //非法
 

E类型为C类型提供了U、V,也就是上面说的来源于子类
F类型继承于C<string,int>,个人认为可以看成F继承一个非泛型的类
G类型为非法的,因为G类型不是泛型,C是泛型,G无法给C提供泛型的实例化

泛型类型的成员:
泛型类型的成员可以使用泛型类型声明中的类型参数。但类型参数如果没有任何约束,则只能在该类型上使用从System.Object继承的公有成员。如下图:

泛型接口:
泛型接口的类型参数要么已实例化,要么来源于实现类声明的类型参数