C#与CLR学习笔记(7)—— 泛型与泛型约束

文章目录

  • 1 泛型概述
    • 1.1 含义
    • 1.2 泛型的继承
      • 1.2.1 泛型类型的继承
      • 1.2.2 泛型的类型参数的继承
  • 2 泛型约束
    • 2.1 编译器对泛型参数的验证
    • 2.2 泛型约束的类型
    • 2.3 其他验证问题
  • 参考文献

1 泛型概述

1.1 含义

使用泛型的主要目的有三个:(1)实现代码复用;(2)避免使用Object类,在实例化一个泛型类时,我们需要指定T的实际类型(类型实参),这样保证了类型安全;(3)减少了 Object 造成的装箱拆箱,提高性能(原理见下文)。
对于编译器而言,泛型 T 本质上就是一个 类型参数(Type parameter),所谓参数其实就是一个特殊的占位符。泛型被定义在程序集中,因此当代码被编译成 IL 放到程序集中时,T 仍然存在,例如以下案例:

Console.WriteLine(typeof(List<>));
Console.WriteLine(typeof(Dictionary<,>));
//以下是输出
System.Collections.Generic.List`1[T]
System.Collections.Generic.Dictionary`2[TKey,TValue]

其中,后单引号(`)后面的数字代表类型参数的个数。
在代码运行时,即在 JIT 阶段,IL 被翻译成 本机语言时,泛型类型参数 T 会被替换成具体的类型。例如,如果你使用了 List 类,那么代码中的 T 是在 JIT 翻译之后才变成了 int 类型。因此,不存在类型转换、装箱拆箱,这就是性能更好的原因。

需要注意,未指定类型实参的泛型类型是不能实例化的,因为它是一种 开放类型,这与你不能实例化一个接口一样。传递一个 类型实参 (Type argument)后,变成 封闭类型,才可以实例化。案例:

Object o;
o = Activator.CreateInstance(typeof(List<int>));//OK
o = Activator.CreateInstance(typeof(List<>));//抛出ArgumentException,因为含有泛型参数

泛型最常用的地方是集合类。微软建议使用泛型集合,不建议使用非泛型集合,除了上文中提到的类型安全、性能高之外,泛型集合类中的虚方法更少,从而进一步提高执行性能;另外,泛型集合一般拥有更多的扩展方法,使用更方便。

1.2 泛型的继承

1.2.1 泛型类型的继承

泛型类可以派生自一个泛型基类,但是,泛型子类必须重复泛型基类的泛型类型,或者必须指定基类的泛型类型。
例如,考察以下代码:

public class ChildGeneric : Generic<T>{} //编译失败
public class ChildGeneric<T> : Generic<T>{} //编译通过
public class ChildGeneric : Generic<int>{} //编译通过

1.2.2 泛型的类型参数的继承

考察两个泛型类型 ListList,其中 MyChildClass 派生自 MyBaseClass,那么 ListList 之间有什么关系吗?答案是没有关系。

因为 类型参数 的继承关系 不改变 泛型类型的 继承关系,或者说,泛型类型 破坏了 泛型类型参数的继承关系

更具体一点,指定类型实参并不影响层次结构。List 派生自 Object,那么 ListList 都是从 Object 派生,二者是 “平辈” 的。指定类型实参只是在 JIT 时拿指定的类型替换 T,这两个 List 是两个不同的类。

由此引出另一个话题,即 逆变协变,详见我的另一篇文章《C#与CLR学习笔记(6)—— 轻松理解协变与逆变》

2 泛型约束

2.1 编译器对泛型参数的验证

由于泛型参数 T 在定义时并未指定,因此,为了安全性,编译器会在编译时进行分析,确保代码适用于未来可能指定的任何泛型类型实参。
例如,考察如下代码:

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

上述代码编译失败,因为并非所有类型都实现了 IComparable 接口,因此 CompareTo()方法有无法执行的风险。

所以从表面上看,使用泛型似乎做不了太多事情,只能使用 Object 中定义的方法。显然,实际情况并不是这样,因为有 泛型约束 机制,它使得泛型变得有用。

上面的案例改进一下,就可以顺利编译:

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

约束 可应用于 泛型类型泛型方法 。如果 基类 或者 被重写/实现的方法 拥有泛型约束,那么子类或其方法必须应用同样的泛型约束。
例如:

public class Generic<T> where T : struct
{}
public class ChildGeneric<T> : Generic<T> //编译失败
{}
public class ChildGeneric<T> : Generic<T> where T : struct //编译通过
{}


public interface IGeneric2
{
     string GetTInfo<T>() where T : struct;
}
class ChildGeneric : IGeneric2
{
     public string GetTInfo<T>() where T : class //编译失败
     {
          return "";
     }
}
class ChildGeneric : IGeneric2
{
     public string GetTInfo<T>() where T : struct//编译通过
     {
          return "";
     }
}

2.2 泛型约束的类型

分类 形式 含义
主要约束
(最多指定一个)
where T : struct T 必须是值类型
Nullable除外,详见参考文献)
where T : class T 必须是引用类型
where T : Foo T 必须派生自 Foo 类
次要约束
(可以指定多个)
where T : IFoo T 必须实现 IFoo 接口
where T1 : T2 T1 派生自 泛型类型 T2
构造器约束 where T : new() T 是拥有公共无参构造函数的非抽象类

2.3 其他验证问题

(1)类型转换问题

泛型参数进行类型转换,要保证符合约束。尽量使用 as 进行转换。

public void DoSomething<T>(T obj)
{
    int x = (int)obj; //编译错误
    int x = (int)(Object)obj; //编译通过,但运行时可能抛 InvalidCastException
    stirng x = obj as string; //推荐
}

(2)将泛型类型转为默认值

推荐使用 default 关键字。

public void DoSomething<T>()
{
    T temp = null; //编译错误,除非指定 T : class
    T temp = default(T); //推荐
}

(3)两个泛型参数对比

需指定泛型约束。

public void DoSomething<T>(T o1, T o2)
{
    if (o1 == o2){} //编译失败,因为若 T 是值类型,那么可能没有重载 == 运算符。
}

(4)不能将泛型约束为具体的值类型,一是因为值类型是隐式密封的,无法被继承;二是因为这种情况使用泛型就没有意义。

参考文献

[1] 《CLR via C#》 第四版

你可能感兴趣的:(.NET,C#,泛型)