1泛型的定义:
所谓泛型,即通过参数化类型来实现在同一份代码上操作多种数据类型.泛型编程是一种编程范式,它利用"参数化类型"将类型抽象化,从而实现更为灵活的复用.
2优点:
(1)代码更强的类型安全;类型之间的转换,在老大的视频中已经举了例子,这里就不多说.
(2)理好的复用;类似于C++模板,如比对不同数据类型的同种操作,则可以省掉很多的代码,共用一段代码.
(3)更高的效率;主要是针对装箱与拆箱,尤其是对隐式的装箱,像for语句中可能有大量的boxing unboxing.
关于详细的介绍在知识库"C# 引用类型与数值类型的认识(一)".
(4)更清晰的约束. 在泛型(3)中总结
3.C#泛型机制简介.
(1)C#泛型能力由CLR在运行时支持,区别于c++的编辑时模板机制.这使得泛型能力可以在各个支持CLR的语言之间进行无缝
的互操作.
(2)C#泛型代码在被编译为IL代码和元数据时,采用特殊的占位符来表示泛型类型(说明此时泛型并没有发生实例化),并用专有的IL指令支持泛型操作.而真正的泛
型实例化工作以"on-demand"的方式,发生在JIT编译时(说明:IL,JIT与元数据等将在知识库中".NET加密与解密(一)"中简单说明)
A. 第一轮的编译时,编译器只为 类型产生"泛型版"的IL代码与元数据---并不进行泛型类型的实例化,T在中间只充当占位符
B. JIT编译时,当JIT编译器第一次遇到 时,将用int替换"泛型版"IL代码与元数据中的T---进行泛型类型的实例化.
( 如果第二次实例时,而且也是int,就运用内存中已有类型,而不会重新生成新的;类型不同会重新生成)
C. CLR为所有类型参数为"引用类型"的泛型类型产生同一份代码;但如果类型为"值类型",对每一个不同的"值类型",CLR将为其产生一份独立代码.
D:如果实例化类型参数的类型相同,则JIT编译器会重复使用该类型.因此C#动态泛型能力避免了C++静态模板可能导致的代码膨涨问题.(这也是C#泛型与C++模板区别之一)
(3)泛型可以理解为更高层次的抽象形式.
=================================================================================================
.Net泛型类的成员
泛型类的所有成员都可以直接或作为构造类型的一部分使用任何包容类 (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
- {
- static void Main() {
- C﹤int﹥ x1 = new C﹤int﹥(1);
- Console.WriteLine(x1.f1); // Prints 1
- C﹤double﹥ x2 = new C﹤double﹥(3.1415);
- Console.WriteLine(x2.f1); // Prints 3.1415
- }
- }
在实例函数成员中,类型 this 是包含这些成员的声明的实例类型。
除了使用类型形参作为类型以外,泛型类声明中的成员与非泛型类的成员遵循相同的规则。下面几小节将讨论适用于特定种类的成员的附加规则。
.Net泛型类中的静态字段
泛型类声明中的静态变量在相同封闭构造类型的所有实例之间共享,但是不会在不同封闭构造类型的实例之间共享。不管静态变量的类型是否涉及任何类型形参,这些规则都适用。
例如:
- class C﹤V﹥
- {
- static int count = 0;
- public C() {
- count++;
- }
- public static int Count {
- get { return count; }
- }
- }
- class Application
- {
- static void Main() {
- C﹤int﹥ x1 = new C﹤int﹥();
- Console.WriteLine(C﹤int﹥.Count); // Prints 1
- C﹤double﹥ x2 = new C﹤double﹥();
- Console.WriteLine(C﹤int﹥.Count); // Prints 1
- C﹤int﹥ x3 = new C﹤int﹥();
- Console.WriteLine(C﹤int﹥.Count); // Prints 2
- }
- }
.Net泛型类中的静态构造函数
泛型类中的静态构造函数用于初始化静态字段,并为从该泛型类声明创建的每个不同封闭构造类型执行其他初始化操作。泛型类型声明的类型形参处于作用域中,并且可在静态构造函数的函数体中使用。
新的封闭构造类类型在第一次发生下列任一情况时进行初始化:
·创建该封闭构造类型的实例。
·引用该封闭构造类型的任何静态成员。
为了初始化新的封闭构造类类型,需要先为该特定的封闭构造类型创建一组新的静态字段。将其中的每个静态字段初始化为默认值。下一步,为这些静态字段执行静态字段初始值设定项。最后,执行静态构造函数。
由于静态构造函数只为每个封闭构造类类型执行一次,因此对于无法通过约束在编译时进行检查的类型形参来说,此处是进行运行时检查的方便位置。例如,下面的类型使用静态构造函数检查类型实参是否为一个枚举:
- class Gen﹤T﹥ where T: struct
- {
- static Gen() {
- if (!typeof(T).IsEnum) {
- throw new ArgumentException("T must be an enum");
- }
- }
- }
.Net泛型类之访问受保护成员
在泛型类声明中,通过从该泛型类构造的任何类类型的实例,可以对继承的受保护实例成员进行访问。具体而言,指定的用于访问 protected 和 protected internal 实例成员的规则通过下面针对泛型的规则进行了扩充:
在泛型类 G 中,如果 E 的类型是从 G 构造的类类型或从 G 构造的类类型继承的类类型,则使用 E.M 形式的 primary-expression 访问继承的受保护实例成员 M 是允许的。
在下面的示例中
- 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 = default(T);
- di.x = 123;
- ds.x = "test";
- }
- }
对 x 的三个赋值是允许的,因为它们全都通过从该泛型类型构造的类类型的实例进行。
.Net泛型类中的重载
泛型类声明中的方法、构造函数、索引器和运算符可以被重载。虽然声明的签名必须唯一,但是在替换类型实参时可能会导致出现完全相同的签名。在这样的情况下,重载解析的附加规则将挑选最明确的
成员。
下面的示例根据此规则演示有效和无效的重载:
- interface I1﹤T﹥ {}
- interface I2﹤T﹥ {}
- class G1﹤U﹥
- {
- int F1(U u); // Overload resulotion for G﹤int﹥.F1
- int F1(int i); // will pick non-generic
- void F2(I1﹤U﹥ a); // Valid overload
- void F2(I2﹤U﹥ a);
- }
- class G2﹤U,V﹥
- {
- void F3(U u, V v);// Valid, but overload resolution for
- void F3(V v, U u);// G2﹤int,int﹥.F3 will fail
- void F4(U u, I1﹤V﹥ v); // Valid, but overload resolution for
- void F4(I1﹤V﹥ v, U u);// G2﹤I1﹤int﹥,int﹥.F4 will fail
- void F5(U u1, I1﹤V﹥ v2); // Valid overload
- void F5(V v1, U u2);
- void F6(ref U u); // valid overload
- void F6(out V v);
- }
形参数组方法和类型形参
可以在形参数组的类型中使用类型形参。例如,给定下面的声明
- 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);
- 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"} );
.Net泛型类之重写和泛型类
和往常一样,泛型类中的函数成员可以重写基类中的函数成员。在确定被重写的基成员时,必须通过替换类型实参来确定基类的成员。一旦确定了基类的成员,重写规则就与非泛型类
相同。
下面的示例演示重写规则如何在存在泛型的情况下起作用:
- 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
- public override C﹤string﹥ G() {} // Ok
- public override void H(C﹤T﹥ x) {} // Error, should be 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) {} // Error, should be C﹤U﹥
- }
.Net泛型类中的运算符
泛型类声明可以定义运算符,所遵循的规则与非泛型类声明相同。运算符声明中使用类声明的实例类型的方式必须与运算符的正常使用规则类似,具体如下:
·一元运算符必须以该实例类型的单个参数为操作对象。一元的 ++ 和 -- 运算符必须返回该实例类型或从该实例类型派生的类型。
·二元运算符的参数中必须至少有一个属于该实例类型。
·转换运算符的形参类型或返回类型必须属于该实例类型。
下面演示泛型类中的有效运算符声明的一些示例:
- 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) {}
- }
对于从源类型 S 转换到目标类型 T 的转换运算符,在应用指定的规则时,与 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﹤string﹥(D﹤T﹥ value) {} // Ok
- public static implicit operator C﹤T﹥(D﹤T﹥ value) {} // Error
- }
前两个运算符声明是允许的,T 和 int 以及 string 分别被视为没有关系的唯一类型。但是,第三个运算符是错误的,因为 C﹤T﹥ 是 D﹤T﹥ 的基类。
对于某些类型实参,可以声明这样的运算符,即这些运算符指定了已经作为预定义转换而存在的转换。在下面的示例中
- struct Convertible﹤T﹥
- {
- public static implicit operator Convertible﹤T﹥(T value) {}
- public static explicit operator T(Convertible﹤T﹥ value) {}
- }
当把类型 object 指定为 T 的类型实参时,第二个运算符将声明一个已经存在的转换(存在从任何类型到类型 object 的隐式转换,因此也存在显式转换)。
在两个类型之间存在预定义转换的情况下,这些类型之间的任何用户定义的转换将被忽略。具体而言:
·如果存在从类型 S 到类型T 的预定义隐式转换,则从S 到T 的所有用户定义的转换(隐式或显式)将被忽略。
·如果存在从类型S 到类型T 的预定义显式转换,则从 S 到T 的所有用户定义的显式转换将被忽略。但是,仍然会考虑从 S 到 T 的用户定义的隐式转换。
对于除 object 以外的所有类型,上面的 Convertible﹤T﹥ 类型声明的运算符都不会与预定义的转换发生冲突。例如:
- void F(int i, Convertible﹤int﹥ n) {
- i = n; // Error
- i = (int)n; // User-defined explicit conversion
- n = i; // User-defined implicit conversion
- n = (Convertible﹤int﹥)i; // User-defined implicit conversion
- }
但是对于类型 object,除了下面这个特例之外,预定义的转换将在其他所有情况下隐藏用户定义的
转换:
- void F(object o, Convertible﹤object﹥ n) {
- o = n; // Pre-defined boxing conversion
- o = (object)n; // Pre-defined boxing conversion
- n = o; // User-defined implicit conversion
- n = (Convertible﹤object﹥)o; // Pre-defined unboxing conversion
- }
.Net泛型类中的嵌套类型
泛型类声明可以包含嵌套的类型声明。包容类的类型形参可以在嵌套类型中使用。嵌套类型声明可以包含仅适用于该嵌套类型的附加类型形参。
泛型类声明中包含的每个类型声明都隐式地是泛型类型声明。在编写对嵌套在泛型类型中的类型的引用时,必须指定其包容构造类型(包括其类型实参)。但是可在外层类中不加限定地使用嵌套类型;在构造嵌套类型时可以隐式地使用外层类的实例类型。下面的示例演示三种不同的引用从 Inner 创建的构造类型的正确方法;前两种方法是等效的:
- class Outer﹤T﹥
- {
- class Inner﹤U﹥
- {
- public static void F(T t, U u) {}
- }
- static void F(T t) {
- Outer﹤T﹥.Inner﹤string﹥.F(t, "abc"); // These two statements have
- Inner﹤string﹥.F(t, "abc"); // the same effect
- Outer﹤int﹥.Inner﹤string﹥.F(3, "abc"); // This type is different
- Outer.Inner﹤string﹥.F(t, "abc");// Error, Outer needs type arg
- }
- }
嵌套类型中的类型形参可以隐藏外层类型中声明的成员或类型形参,但这是一种不好的编程风格:
- class Outer﹤T﹥
- {
- class Inner﹤T﹥ // Valid, hides Outer’s T
- {
- public T t; // Refers to Inner’s T
- }
- }
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 分享 3 个 .NET 开源的文件压缩处理库,助力快速实现文件压缩解压功能!
· Ollama——大语言模型本地部署的极速利器
· [AI/GPT/综述] AI Agent的设计模式综述