C# 泛型


1. 泛型的意义

类型安全(更好的编译时检查),性能增强(避免装箱和拆箱),代码复用(减少了功能相似的代码)

2. 种类:

  • 泛型类型(包括类,结构,接口,委托,数组)
  • 泛型方法。

    枚举、属性、构造函数、字段 这些没有对应的泛型

3. 约束 :

  • where T: class (必须是引用类型)
  • where T:struct (必须是值类型,nullable 类型的值类型除外)
  • where T: 基类名 (限制类型参数必须是继承自指定的基类,或是基类本身)
  • new() (必须有无参构造函数,只能用于引用类型(如果类型是结构类型也不会报错),并且必须放在约束的最后如 class Sample where T : class, IDisposable, new() )
  • where T:接口名 (限制类型参数必须是实现指定接口)
  • where T:U (为 T 提供的类型参数必须是为 U 提供的参数或派生自为 U 提供的参数。这称为裸类型约束.)
  • 组合约束

    上面的几种约束可以形成组合约束。若一个类型参数有多个约束,约束之间用 , 分开; 如果包含多个类型参数 ,多个类型参数用 where 分开。如

    public class KeyValue where TKey : new() where TValue: Base,new()

4. 相关术语:

  • 形参(一般用 paramter 描述)
  • 实参(一般用 argument 描述)
  • 类型参数:是一种形参,如 List 中的 T
  • 类型实参 ,如 List 中的 int
  • 泛型的 开放类型
    指的是带有 类型参数(且未分配类型实参) 的类型,如 Type myType= typeof(List) ;中的变量 myType;
  • 泛型的 封闭类型; 已经分配具体 类型实参 的类型,如 Type myType2= typeof(List) ; 中的变量 myType2;

5. 泛型方法

  • 带有类型参数的方法不一定都是泛型方法

    public class Animal
        {
            public void Move1(T speed) { }
    
            public void Move2(S speedT) { }
        }
    

    Move2 是泛型方法 , Move1 则不是. 判断一个方法是否是泛型方法,可以通过反射获取到 MethodInfo 对象,然后调用 对象的 IsGenericMethod 方法.

  • 方法重载: 调用方法时,如果有多个方法签名相匹配,则优先调用类型参数较少的方法

  • 类型参数的省略:调用泛型方法时 ,如果是下面的2种情况则可以省略类型参数

    • 使用类型明确的值,传给被调用的方法时 如
        Move(100); // 可以简写成  Move(100);
    
    • 使用泛型方法调用另一个具有相同方法签名的泛型方法时
        public void Move(T speed){}
    
        public void MoveWrapper(T s)
        {
            Move(s);// 可以简写成  Move(s);
        }
    

6. 协变和逆变:

  • 协变和逆变指的是泛型的类型实参之间的类型转换 。且协变和逆变都是类型安全的.

    C# 只对 接口委托 提供可变性支持(包括协变和逆变),另外支持 数组 的协变(但是仅仅是编译时不报错,允许时访问到内部的具体元素还是会出现运行时异常,这样设计的原因据说是为了兼容 Java)。 类型参数前加 inout ,这种写法是 C# 4.0 才引人的, 这也表明之前的 C#版本不支持协变和逆变

  • 协变(Covariance 允许使用比原始指定的类型派生程度更大的类型)

    通用的表示是 Ixxx<父类或父接口> iBase = new xxx<子类或子接口>(); ,

    其中 Ixxx接口中必然有 类型参数 前面带有 out 关键字修饰
    如: IEnumerable animals= new List();
    当泛型的类型参数只描述返回类型参数(输出)的操作时,协变是类型安全的;
    若类型参数前加 out ,表明该类型只能作为函数的返回类型使用,不能作为输入

  • 逆变(Contravariance 允许使用比原始指定的类型派生程度更小的类型)

    通用的表示是 Ixxx<子类或子接口> iBase = new xxx<父类或父接口>();
    其中 Ixxx接口中必然有 类型参数 前面带有 in 关键字修饰。
    如:

        Action b = (target) => { Console.WriteLine(target.GetType().Name); };
        Action d = b;
        d(new Derived());
    

    当类型参数只描述接受类型参数(输入)的操作时,逆变就是安全的 。
    若类型参数前加 in ,表明该类型只能作为函数的输入类型使用,不能作为输出类型。

  • 不变体(Invariance 只能使用原始指定的类型;泛型类型参数既不是协变类型,也不是逆变类型) 如: List cats= new List();

7. 进阶内容

  • 泛型类型内的静态字段:

    不同的封闭类型具有不同的静态字段

  • JIT 编译器如何处理泛型
    1. JIT 为每个以值类型作为类型实参的 封闭类型 都创建不同的代码
    2. 所有使用引用类型作为类型实参的 封闭类型 都共享相同的本机代码
      这是因为所有引用类型的变量实际上只是指向堆上对象的指针.而所有的指针都可以使用相同的方式操作.