.NET via C#笔记12——泛型

12 泛型

使用值类型作为参数的泛型容器,传入值类型的参数时,不需要进行装箱

12.1 FCL中的泛型

System.Array中提供了很多泛型方法

  1. AsReadOnly
  2. BinarySearch
  3. ConvertAll
  4. Exists
  5. Find
  6. FindAll
  7. FindIndex
  8. FindLast
  9. FindLastIndex
  10. ForEach
  11. IndexOf
  12. LastIndexOf
  13. Resize
  14. Sort
  15. TrueForAll

12.2 泛型基础结构

12.2.1 开放类型和封闭类型

  1. 具有泛型类型参数的类型成为开放类型
    1. CLR禁止构建开放类型的实例
  2. 为所有类型参数传递了具体类型,则成为封闭类型
    1. 使用不同参数创建的封闭类型,静态成员不同享

12.2.3 泛型类型的同一性

使用using关键字来创建一个类的别名

using DataList = List<Data>;
// 返回true
typeof(DataList) == typeof(List<Data>);

12.2.4 代码爆炸

  1. 使用引用类型的泛型类共享一份代码
  2. 使用值类型的泛型类由编译器生成独一份的代码

12.5 委托和接口的逆变和协变泛型类型实参

泛型委托

泛型委托的泛型类型可以标记为你变量和协变量,默认为不变量

  1. 不变量(invariant):默认,参数类型不可变
  2. 逆变量(contravariant):可以变为子类,只能作为输入
  3. 协变量(covariant):可以变为基类,只能作为输出
delegate TResult Func<in T, out TResult>(T arg);

Func<object, ArgumentException> fn1 = null;
// 可以赋值给另一种使用不同类型参数的泛型委托
Func<string, Exception> fn2 = fn1;

// 这是错的
fn1 = fn2;

这也很好理解,Func<string, Exception>的参数和输出是兼容Func<object, ArgumentException>的,例如下面的程序展示的那样:

ArgumentException Func1(object arg) {
    ...
}
Exception Func2(string arg) {
    return Func1(arg);
}

在Func2中调用Func1是OK的,参数可以隐式转化为其基类类型,而反过来则是不对的,不能隐式转为其子类类型。

泛型接口

泛型接口的类型参数也可以声明为逆变量或协变量

interface IWhatTheFuck<in T, out TResult> {
    TResult Func(T arg);
}

IWhatTheFuck<object, ArgumentException> if1 = null;
IWhatTheFuck<string, Exception> if2 = null;

if2 = if1; // 通过编译
if1 = if2; // 编译失败

值类型

值类型作为模板参数时不能逆变或协变(涉及装箱和拆箱)。

12.6 泛型方法

如果普通方法和泛型方法同时匹配一个调用,编译器优先适配普通方法

12.7 泛型和其他成员

属性,索引器,事件,操作符方法,构造器,终结器本身不能有类型参数,但可以使用类的泛型参数

12.8 可验证性和约束

这样的函数无法通过编译

static T Min<T>(T o1, T o2) {
    if (o1.CompareTo(o2) < 0) return o1;
    return o2;
}

通过添加约束

static T Min<T>(T o1, T o2) where T : IComparable<T> {
    if (o1.CompareTo(o2) < 0) return o1;
    return o2;
}
  1. 通过泛型约束无法重载
  2. 通过参数个数可以重载
  3. 重写虚泛型方法时,所有的约束会继承,不能添加新的约束

主要约束

  1. 主要约束可以是除了这些类型之外的引用类型:
    1. Object
    2. Array
    3. Delegate
    4. MultiCastDelegate
    5. ValueType
    6. Enum
    7. Void
  2. 指定了主要类型之后,类型参数只能是主要类型或者其子类。
  3. 有两种特殊的主要约束:
    1. class:包括了class,interface,delegate,array
    2. struct:所有的值类型,包括了一个默认的无参数构造函数

次要约束

  1. 指明需要实现的多个接口
  2. 指明多个类型参数之间的关系
    csharp static List<TBase> ConvertIList<T, TBase>(IList<T> list) where T : TBase { List<TBase> baseList = new List<TBase>(list.Count); for (int i = 0; i < list.Count; i++) { baseList.Add(list[i]); } }

    构造函数约束

  3. 约束中可以包含一个构造器约束
  4. 指定类型可以有一个public,无参数的构造函数
  5. 主要约束为值类型,无需指定构造函数约束

其他情况

  1. 隐式转型模板函数中的未定类型变量是非法的,除非在约束中指定过了
  2. 使用defaut(T)来初始化一个未定类型的变量
    1. 为引用类型初始化为null
    2. 为值类型初始化内存为0
  3. 未定类型变量与null进行比较是否相等,编译器不会报错,如果是值类型作为参数,编译器判断为不等
    void func<T>(T o) { if (o == null) { // 如果T是引用类型,判断成立 // 如果T是值类型,判断不成立 // 但是编译器不会报错 } }
  4. 相同类型的泛型类型变量之间不能直接比较是否相等
    csharp void func<T>(T a, T b) { if (a == b) { ... } // 编译不通过 if (EqualityComparer<T>.Default.Equals(a, b)) { ... } // 编译通过 }
  5. 泛型类型变量无法作为操作数使用
    csharp void func<T>(T a, T b) { var c = a + b; // 编译不通过 }

总结

  1. C#的泛型比C++模板是更好的存在
    1. 通过各种约束,可以直接编译泛型函数\类
    2. 模板需要实例化,导致代码爆炸,各模块\编译单元的实例化模板类还不通用,需要
      1. template class std::vector<int>;显式实例化
      2. template class __declspec(dllexport) std::vector<int>;实例化导出实例化的模板类
      3. extern template class std::vector<int>;引用别处的实例化模板类
  2. C#函数是如何调用的?
    1. 如果像C++那样,直接绑定函数地址,那么对于不同参数的模板类,类型成员函数的参数地址是不同的,如何做到不同参数的模板共享一份代码的呢?
    2. 如果模板类没有源码,怎样用dll中的开放类(已经编译成IL),生成一个参数为值类型的封闭类呢?
上一篇:SIMD via C#


下一篇:Linkage Based Face Clustering via Graph Convolution Network