返回

泛型

泛型 支持另一种形式的代码重用,即 算法重用.

定义算法的开发人员不设定该算法要操作的数据类型, 该算法可广泛的应用于不同类型的对象.

泛型有两种表现形式:泛型类型和泛型方法。

CLR允许创建:

  • 泛型引用类型
  • 泛型值类型
  • 泛型接口
  • 泛型委托

不允许创建泛型枚举类型.

CLR也允许在引用类型,值类型和接口中定义泛型方法.

泛型的写法

例如泛型类List类, 在类名后添加一个<T>, 表名它操作的是一个未指定的数据类型.

定义泛型类或方法时, 为类型指定的任何变量(比如T),都称为类型参数. T是变量名,源代码能使用数据类型的任何地方都能使用T.

命名规则: 泛型参数变量要么称为T, 要么以大写T开头(TKey,TValue) , 类似I代表接口一样.T代表类型Type.

使用泛型

private static void SomeMethod()
{
  // 构造一个List来操作DateTime对象
  List<DateTime> dtList = new List<DateTime>();

  dtList.Add(DateTime.Now); //不进行装箱, 值类型

  dtList.Add("1/1/2004");   // 编译时错误,检查类型

  DateTime dt = dtList[0];  // 不需要转型
}

使用泛型的优势

  1. 源代码保护

    不需要使用泛型算法的开发人员访问算法的源代码

  2. 类型安全

    将泛型算法应用于具体的类型时,编译器和CLR能保证只有与指定类型兼容的对象才能用于算法.否则编译时报错.

  3. 更清晰的代码

    由于编译器强制类型安全, 所以减少了类型强制转换次数.DateTime dt = dtList[0];

  4. 更佳的性能

    值类型能以传值的形式传递,不需要执行任何装箱操作.CLR无需验证这种转型是否类型安全,提高了代码的运行速度.

比较泛型和非泛型算法的性能

public static class Generics
{
    public static void Main()
    {
        Performance.ValueTypePerfTest();
        Performance.ReferenceTypePerfTest();
    }
}
internal static class Performance
{
    // 值类型的泛型类和非泛型类性能测试
    public static void ValueTypePerfTest()
    {
        const Int32 count = 100000000;

        // 泛型类List<Int32>性能测试
        // 运行性能计时器, 在using代码块结束后会DisPose停止计时
        using (new OperationTimer("List<Int32>"))
        {
            List<Int32> l = new List<Int32>();
            for (Int32 n = 0; n < count; n++)
            {
                l.Add(n);       // 不发生装箱
                Int32 x = l[n]; // 不发生拆箱
            }
            l = null; // 使引用为null,确保进行垃圾回收
        }
        // 非泛型类ArrayList<Int32>性能测试
        // 运行性能计时器, 在using代码块结束后会DisPose停止计时
        using (new OperationTimer("ArrayList of Int32"))
        {
            ArrayList a = new ArrayList();
            for (Int32 n = 0; n < count; n++)
            {
                a.Add(n);               // 装箱
                Int32 x = (Int32) a[n]; // 拆箱
            }
            a = null; // 使引用为null,确保进行垃圾回收
        }
    }
    // 引用类型的非泛型和泛型性能测试
    public static void ReferenceTypePerfTest()
    {
        const Int32 count = 100000000;

        using (new OperationTimer("List<String>"))
        {
            List<String> l = new List<String>();
            for (Int32 n = 0; n < count; n++)
            {
                // 字符串
                l.Add("X");      // Reference copy
                String x = l[n]; // Reference copy
            }
            l = null; // Make sure this gets GC'd
        }

        using (new OperationTimer("ArrayList of String"))
        {
            ArrayList a = new ArrayList();
            for (Int32 n = 0; n < count; n++)
            {
                // 字符串
                a.Add("X");               // Reference copy
                String x = (String) a[n]; // Cast check & reference copy
            }
            a = null; // Make sure this gets GC'd
        }
    }

    // 用于运行时性能计时
    private sealed class OperationTimer : IDisposable
    {
        private Stopwatch m_stopwatch;
        private String    m_text;
        private Int32     m_collectionCount;

        public OperationTimer(String text)
        {
            PrepareForOperation();

            m_text            = text;
            m_collectionCount = GC.CollectionCount(0);

            // This should be the last statement in this
            // method to keep timing as accurate as possible
            m_stopwatch = Stopwatch.StartNew();
        }

        // 在using代码块结束后会Dispose会执行
        public void Dispose()
        {
            Console.WriteLine("{0} (GCs={1,3}) {2}",
                (m_stopwatch.Elapsed),
                GC.CollectionCount(0) - m_collectionCount, m_text);
        }

        // 强制垃圾回收器执行
        private static void PrepareForOperation()
        {
            // 首先 GC.Collect(); 并不会立即去回收 只是告诉回收器 去回收
            // 垃圾收集器在一次垃圾收集过程中,垃圾收集器的逻辑不能保证所有未引用的对象都从堆中删除

            // 我们可以显式调用 GC.Collect();GC.WaitForPendingFinalizers();
            // 这两行代码进行强制回收的执行
            GC.Collect();
            // 挂起当前线程,直到处理终结器队列的线程清空该队列为止。
            GC.WaitForPendingFinalizers();
            GC.Collect();
        }
    }
}
// 00:00:00.6625861 (GCs=  6) List<Int32>
// 00:00:05.5150908 (GCs=388) ArrayList of Int32
// 00:00:01.1244509 (GCs=  1) List<String>
// 00:00:01.2407716 (GCs=  0) ArrayList of String

结果表明:

  1. 泛型List算法比非泛型ArrayList算法快得多.
  2. ArrayList会造成大量装箱,要进行频繁的垃圾回收
  3. 引用类型则区别不大

首次为特定的数据类型调用方法时,CLR都会为这个方法生成本机代码.

FCL中的泛型

泛型最明显的应用是集合类. Microsoft建议使用泛型集合类,不建议使用非泛型集合类.常用的接口包含在Sysytem.Collections.Generic命名空间中。

System.Array类(即所有数组的基类)提供了大量静态泛型方法,比如,AsReadonly、FindAll、Find、FindIndex等。

集合类实现了许多接口,放入集合中的对象可实现接口来执行排序和搜索等操作.

泛型基础结构

开放类型和封闭类型

CLR会为应用程序的各种类型创建称为类型对象的内部数据结构. 泛型类型参数仍然是类型,CLR同样会创建内部数据结构.(包括 引用类型(类),值类型(结构), 接口类型和委托类型).

具有泛型参数的类型称为开放类型. CLR禁止构造开放类型的任何实例. 类似于禁止构造接口类型实例.

代码引用泛型类型时,为所有类型参数传递了实际的数据类型,类型就成为封闭类型, CLR允许创建封闭类型的实例.如果在引用泛型类型时,留下一些泛型类型 实参未指定,CLR就会创建开放类型对象,而且不能创建该类型的实例.

// 定义一个部分指定的开放类型
internal sealed class DictonaryStringKey<TValue> : Dictionary<String, TValue>
{
}

static void Main(string[] args)
{
    Object o = null;

    // Dictionary<,>有2个泛型参数,是开放类型,不允许创建实例
    Type t = typeof(Dictionary<,>);
    // 在运行时抛出异常, 创建失败
    o = CreateInstance(t);

    // DictonaryStringKey<>,有一个泛型参数没指定,所以是开放类型
    t = typeof(DictonaryStringKey<>);
    // 在运行时抛出异常, 创建失败
    o = CreateInstance(t);

    // 传入了确定的类型,就是封闭类型
    t = typeof(DictonaryStringKey<Guid>);
    // 创建成功
    o = CreateInstance(t);

    Console.WriteLine(o.GetType());
}

private static Object CreateInstance(Type t)
{
    Object o = null;
    try
    {
        o = Activator.CreateInstance(t);
        Console.WriteLine($"已创建{t.ToString()}的实例.");
    }
    catch (ArgumentException e)
    {
        Console.WriteLine(e.Message);
    }
    return o;
}
// 输出:
// Cannot create an instance of System.Collections.Generic.Dictionary`2[TKey,TValue] because Type.ContainsGenericParameters is true.
// Cannot create an instance of DictonaryStringKey`1[TValue] because Type.ContainsGenericParameters is true.
// 已创建DictonaryStringKey`1[System.Guid]的实例.
// DictonaryStringKey`1[System.Guid]

如上代码, Activator.CreateInstance在运行时试图构造开放类型的实例时,会抛出ArgumentException异常,并指明泛型参数.

类型名以 单引号+数字结尾, 数字代表类型的元数,也就是要求的参数个数,Dictionary类的元数是2, 要求[TKey,TValue] . DictonaryStringKey要求一个[TValue], 指定元数的具体类型.

  • CLR会在类型对象内部分配类型的静态字段( 非静态的字段是实例的,这里是类型字段,回顾第四章 )

  • 每个封闭类型都有自己的静态字段

    • 换言之:List中定义了任何静态字段,不会在ListList之间共享.
  • 如果定义了泛型类的静态构造器,那针对每个封闭类型类,构造器都会执行一次.

    • 泛型静态构造器目的是保证传递的类型参数满足特定条件.
internal sealed class MyClass<T>
{
    // 静态类型构造器
    static MyClass()
    {
        // 这样定义只能处理枚举类型的泛型类型
        // CLR提供了约束的功能, 可以更好的指定有效的类型实参,
        // 但是约束无法将类型实参限制为仅枚举类型. 由于这个原因,
        // 所以需要用静态构构造器来保证类型是一个枚举类型
        if (!typeof(T).IsEnum)
        {
            throw new ArgumentException("T must be an Enum ");
        }
    }
    // 实例构造器
    public MyClass()
    {
    }
}

泛型类型和继承

使用泛型类型并指定类型的实参时,实际是在CLR中定义一个新的类型对象, 新的类型对象从泛型类型派生自的那个类型派生.

  • List<T>Object派生,List<String>List<Guid>也从Objcet派生.
  • DictonaryStringKe<TValue>yDictionary<String,TValue>派生, 那么DictonaryStringKey<Guid>也是从Dictonary<String,Guid>派生.

指定类型实参不影响继承层次结构. 需要判断强制类型转换是否是允许的.

定义一个如下链表节点类:

static void Main(string[] args)
{
    // 传入构造函数需要的参数
    Node<Char> head = new Node<Char>('C');
    head = new Node<char>('B',head);
    head = new Node<char>('A',head);
    Console.WriteLine(head.ToString());// 输出ABC
}
// 链表节点类
internal sealed class Node<T>
{
    public T m_data;
    public Node<T>  m_next;

    public Node(T mData, Node<T> mNext)
    {
        m_data = mData;
        m_next = mNext;
    }
    // 单参数构造函数,会通过this指针调用Node(T mData, Node<T> mNext)构造函数
    public Node(T mData) : this(mData,null)
    {
    }
    public override string ToString()
    {
        return m_data.ToString() + ((m_next != null) ? m_next.ToString() : String.Empty);
    }
}

在这个Node类中,对于m_next字段引用的另一个节点来说, m_data字段必须包含相同的数据类型, 例如不能一个包含Char,一个包含String,一个包含DataTime. 如果全部用Node,会失去编译时的类型安全性,值类型会被装箱.

很好的办法是定义非泛型Node基类. 再定义泛型TypedNode类(用Node类作为基类). 这样就可以创建一个链表,每个节点都可以是一种具体的数据类型(不能是Object),并防止了值类型装箱.

static void Main(string[] args)
{
    // 传入构造函数需要的参数
    Node head = new TypeNode<Char>('.');
    head = new TypeNode<DateTime>(DateTime.Now,head);
    head = new TypeNode<String>("Today is ",head);
    Console.WriteLine(head.ToString());// Today is 2019/8/5 18:06:43.
}

// 非泛型Node基类
internal class Node
{
    protected Node m_next;

    public Node(Node mNext)
    {
        m_next = mNext;
    }
}

// 链表节点类
internal sealed class TypeNode<T> : Node
{
    public T m_data;

    // 基类没有无参构造函数,派生类要显式调用基类的
    public TypeNode(T mData, Node mNext) : base(mNext)
    {
        m_data = mData;
    }
    // 使用单个参数构造函数用this调用TypeNode(T mData, Node mNext),
    public TypeNode(T mData) : this(mData,null)
    {
    }
    public override string ToString()
    {
        return m_data.ToString() + ((m_next != null) ? m_next.ToString() : String.Empty);
    }
}

泛型类型的同一性

有些开发人员为了简化如下代码:

// 因为源代码中大量<>符号有损可读性
List<DateTime> dt1 = new ListList<DateTime>();

// 定义个新的非泛型类,从泛型类型派生
// 这样就只是为了简化代码没有了<>符号
internal sealed class DateTimeList : List<DateTime>
{
  //这里不用写任何代码
}

// false
Boolean sameType = (typeof(List<DateTime>) == typeof(DateTimeList));

上述代码会失去类型的同一性相等性. 如果方法原型需要的是DateTimeList类型参数,那么List类型就不能传递给它

所以C#允许使用简化语法来引用泛型封闭类型,同时不影响类型的相等性. 就是在源文件顶部使用传统的using指令.

// 用using指令定义DateTimeList符号.
// 代码编译时, 所有DateTimeList替换成System.Collections.Generic.List<System.DateTime>
using DateTimeList = System.Collections.Generic.List<System.DateTime>;

// true
Boolean sameType = (typeof(List<DateTime>) == typeof(DateTimeList));

// 还可以用var
var dt1 =new List<DateTime>();

代码爆炸

使用泛型类型参数的方法进行JIT编译时,CLR获取方法的IL, 用指定的类型实参替换, 然后创建恰当的本机代码(这些代码是为指定数据类型量身定制). 这样CLR要为每种不同的方法/类型组合生成本机代码. 称为 代码爆炸.

CLR的优化措施:

  1. 假如特性的类型实参调用了一个方法, 以后再用相同的类型实参调用这个方法,CLR只会为这个方法/类型组合编译一次代码. 例如:一个程序集中使用List,另一个程序集加载到同一个AppDomain也使用List,CLR只编译一次List.
  2. CLR认为所有引用类型实参都完全相同,所以代码能够共享. 因为任何引用类型的实参或变量实际上只是指向堆上对象的指针(32位系统是32位指针,64位系统是64位指针). 所有的对象指针都以相同方式操作. 例如 List编译的代码可直接用于List的方法.
  3. 值类型就不能,CLR必须专门为那个值类型生成本机代码. 因为值类型的大小不定.即使值类型大小一样,仍然无法共享(Int32和UInt32都是32位). 要用不同的本机CPU指令来操纵这些值.

泛型接口

泛型的主要作用是定义泛型的引用类型和值类型.

但是,对泛型接口的支持对CLR也很重要. 没有泛型接口,每次用非泛型接口(如IComparable)来操纵值类型都会发生装箱.

// 这个泛型接口定义是FCL的一部分
public interface IEnumerator<T> : IDisposable, IEnumerator
{
  // 实现接口的需要有此属性
  T Current {get {...}}
}
// 实现了泛型接口, 保持类型实参的未指定状态
internal sealed class ArrayEnumrator<T> : IEnumerator<T>
{
  private T[] m_array;
  // IEnumerator<T>的Current是T类型.
  public T Current { get { ... }  }
}

泛型委托

CLR支持泛型委托, 目的是保证任何类型的对象都能以类型安全的方式传给回调方法.

泛型委托运行值类型实例在传给回调方法时不进行任何装箱.

委托实际只是提供了4个方法的一个类定义.

  1. 构造器
  2. Invoke方法
  3. BeginInvoke方法
  4. EndInvoke方法

尽量使用FCL预定义的泛型Action和Func委托.

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

协变性: 指定返回类型的兼容性.

逆变性: 指定参数的兼容性.

委托的每个泛型类型参数都可标记为协变量和逆变量.

泛型类型参数形式:

  1. 不变量: 泛型类型参数不能更改.

  2. 逆变量: 泛型类型参数可以从一个类更改为它的某个派生类. 在C#中用in关键字标记.

    逆变量泛型类型参数只出现在输入位置,比如方法的参数.

  3. 协变量: 泛型类参数可以从一个类更改为它的某个基类. 在C#中用out关键字标记.

    协变量泛型类型参数只能出现在输出位置,比如作为方法的返回类型.

方便记忆: in(里面,内部,子类) 参数类型允许它的子类; out(外面,外部,基类) 返回类型允许它的基类.

// 委托定义
// in T 逆变量, out TResult协变量
public delegate TResult Func<in T, out TResult>(T arg);

// 如果像这样声明一个变量
Func<Object, ArgumentException> fn1 = null;
//    子类       基类
//     ↓          ↓
// 可以转型为另一个泛型类型参数不同的Func类型
Func<String, Exception> fn2 = fn1; // 不需要显式转型
Exception e = fn2("");

fn1 变量引用了一个方法, 获取一个Object, 返回一个ArgumentException.

fn2 变量引用另一个方法, 获取String, 返回Exception

因为in逆变量,String是从Object派生, 并且 out协变量, ExceptionArgumentException的基类,上述代码能正确编译, 而且编译时能维持类型的安全性.

由于需要装箱,所以值类型不具有这种可变性,

// 不能再调用它时传递List<Datetime>
// 虽然Datetime派生自Object
// 但是Datetime值类型和Object之间不存在引用转换
// 此外, 此方法最大的好处,JIT编译得到的代码只有一个版本
void Test(IEnumerable<Object> collection) { ... }

// 为了解决上面问题,可以这样声明
// 这样写,只有在T类型是引用类型的前提下,才能共享同一个版本的JIT编译代码
// 每个值类型都有一份不同的JIT编译代码
// 起码能传递值类型
void Test<T>(IEnumerable<T> collection) { ... }

// 编译不通过, 无效的可变性,T 必须是不变量, 当前T为逆变.
// delegate void SomeDelegate<in T>(ref T t);

注意: 不能将可变性(in/out)泛型类型参数传给使用了out/ref关键字的方法. 必须是不变量

使用 要 获取泛型参数和返回值委托时, 尽量为逆变性参数和协变性返回值指定in和out关键字,这样做不会有不良反应,使委托能在更多的情形中使用.

要使用 具有 泛型类型参数接口也尽量为逆变性参数和协变性返回值指定in和out关键字.

// T可接受
public interface IEnumerator<in T> : IEnumerator
{
  Boolean MoveNext();
  T Current{ get; }
}

// 定义一个方法,接受任意引用类型的一个IEnumerator
Int32 Count(IEnumerator<Object> col) { ... }

// 以下调用Count,传递IEnumerator<String>
// 因为T是逆变量,String是Object的子类,所以编译没问题,可以顺利运行
Int32 c = Count(new[] {"Grant"});

在声明泛型类型参数时,必须由你显式使用in/out来标记可变性. 这样防止以后修改类型参数时,用法与声明不符的地方编译器就会报错,提醒你违反了自己订立的协定.

泛型方法

定义泛型类,结构和接口时, 类型中定义的任何方法都可引用类型指定的类型参数. 类型参数可作为方法参数,方法返回值或方法内部定义的局部变量的类型使用.

CLR还允许方法指定它自己的类型参数.

static void Main(string[] args)
{

    GenericType<String> gt = new GenericType<string>("123");
    // 123 : System.Int32
    Console.WriteLine($"{gt.Converter<Int32>()} : {gt.Converter<Int32>().GetType()}");

}

// 定义了类型参数T
internal sealed class GenericType<T>
{
    private T m_value;

    public GenericType(T mValue)
    {
        m_value = mValue;
    }
    // 定义了自己的类型参数TOutput
    // Converter方法能将m_value字段引用的对象转换成任意类型(TOutput).取决于调用时传递的TOutput参数
    public TOutput Converter<TOutput>()
    {
        TOutput result = (TOutput) Convert.ChangeType(m_value, typeof(TOutput));
        // 返回类型转换之后的结果
        return result;
    }
}

Converter方法能将m_value字段引用的对象转换成任意类型(TOutput).取决于调用时传递的TOutput参数.

// ref关键字标记参数,o1,o2必须先初始化,方法内能读写
// out关键字标记 则不必需初始化,不能读取,在返回前必须写入
private static void Swap<T>(ref T o1, ref T o2)
{
    T temp = o1;
    o1 = o2;
    o2 = o1;
}

// 这样调用Swap
Int32 n1 = 1, n2 = 2l;
Swap<Int32>(ref n1,ref n2);

有ref/out参数的泛型方法, 实参传递的变量必须和方法参数相同的类型 , 不允许用可变性来标识参数,以防损害类型安全性.

泛型方法和类型推断

C#编译器支持在调用泛型方法时进行类型推断.(就是省略<>).

private static vod Test()
{
  Int32 n1 = 1, n2 = 2;
  // 编译器会推断n1,n2的类型,最后调用Swap<Int32>
  Swap(ref n1, ref n2);

  // 重要说明: C#使用的是变量的数据类型, 而不是变量引用对象的实际类型
  String s1 = "A";
  Object s2 = "B";
  // 编译出错, 因为编译器无法推断用哪个传递的类型
  // 编译器发现s1是String , s2是Object(不是String )
  Swap(ref s1, ref s2);
}

类型可以定义多个方法,一个接受具体类型,另一个接受泛型类型.

private static void Display(String s)
{
  Console.WriteLine(s);
}
private static void Display<T>(T t)
{
  // 调用Display(String)
  Display(t.ToString());
}

// 调用的方式
// 2个方法都可以被调用,
// 但是C#编译器优先考虑明确的匹配, 再考虑泛型匹配
Display("Jeff");    // 调用Display(String)
Display(123);       // 调用Display<T>(T)
// 明确指定了泛型类型实参,告诉编译器不要尝试推断类型实参
// 所以编译器会毫不犹豫的代用泛型方法
Display<String>("AAA"); // 调用Display<T>(T)

C#编译器优先考虑参数明确的匹配, 再考虑泛型匹配, 如果指定了``,就调用泛型方法.

泛型和其他成员

C#中, 属性,索引器(有参属性),事件,操作符方法,构造器,终结器本身不能有类型参数. 但他们能在泛型类型中定义,这些成员中的代码能使用类型的类型参数.

可验证性和约束

CLR支持称为约束的机制.

private static T Min<T>(T o1,T o2)
{
  // 编译错误, 因为不是所有类型都能有ComparableTo方法
  //if (o1.ComparableTo(o2))
  {
    return o1;
  }
  return o2;
}

// 通过限制类型, 可以对那些类型进行更多的操作
// where关键字告诉编译器,为T指定的任何类型都必须实现IComparable<T>接口
private static T Min<T>(T o1,T o2) where T : IComparable<T>
{
  if (o1.ComparableTo(o2))
  {
    return o1;
  }
  return o2;
}

约束可以应用于泛型类型的类型参数,也可以用于泛型方法的类型参数(如上面的Min方法).

CLR不允许基于类型参数名称和约束来进行重载.

internal sealed class Test<T>{}

// 错误与Test<T>{}类定义冲突
//internal sealed class Test<T> where T : IComparable<T>{}

private static void M<T>();

// 错误与M<T>方法定义冲突
// private static void M<T> where T : IComparable<T>();

重写虚泛型方法时, 重写的方法必须指定相同数量的类型参数,这些类型参数会 继承在基类方法上指定的约束.

事实上根本不允许为重写方法的类型指定任何约束. 但是类型名称可以更改(T可以改为T1,T2之类),不能指定约束.

主要约束

T : Class类型参数可以指定 0个或1个 主要约束 , 主要约束可是代表非密封类的一个引用类型.

不能指定以下特殊类型:

  1. System.Object
  2. System.Array
  3. System.Delegate
  4. System.MulticastDelegate
  5. System.ValueType
  6. System.Enum
  7. System.Void

指定引用类型约束时, 相当于向编译器承诺: 一个指定的类型实参要么是与约束类型相同,要么是从约束类型派生的类型. 如果类型参数没有指定主要约束,就默认为System.Object ,并且不能显式指定.

两个特殊的主要约束 class 和struct

T : class
T : struct

class约束是承诺类型实参是引用类型.

  • 任何类,接口类型,委托类型或者数组类型都满足这个约束. struct约束是承诺类型实参是值类型.
  • 包括枚举在内的任何值类型都满足这个约束.
  • CLR和编译器将任何System.Nullable值视为特殊类型, 不满足这个struct约束.
  • 原因是Nullable类型将它的类型参数约束为struct,而CLR希望禁止这样的递归类型.
internal sealed class Test<T> where T : class
{
  public void M()
  {
    // 引用类型的变量都能设为null
    // 值类型的变量不能设置为null
    T temp = null; //允许,因为T约束为引用类型
  }
}

internal sealed class Test<T> where T : struct
{
  public static T Factory()
  {
    // 允许,因为所有值类型都隐式有一个公共无参构造器
    // 如果约束为class, 无法通过编译,因为有的引用类型没有公共的无参构造器.
    return new T();
  }
}

值类型都有公共的无参构造器. 不允许设置为null 引用类型不一定都有无参构造器,不允许设置为null

次要约束

T : interface类型参数可以指定 0个或多个 次要约束, 次要约束代表接口类型. 向编译器承诺类型实参实现了接口. 由于能指定多个接口约束,所以类型实参必须实现了所有接口约束.

在第13章详细讲 接口约束.

T : TBase还有一种次要约束称为 类型参数约束, 也称为 裸类型约束. 它允许一个泛型类型或方法规定: 指定的类型实参要么就是约束类型,要么是约束的类型的派生类. 一个类型参数可指定 0个或多个 裸类型约束.

意思就是: T 由 TBase 约束, 由类型参数决定约束. List where T:TBase

// 指定了两个类型参数
// T参数 由TBase类型参数约束, T必须兼容于TBase指定的类实参
private static List<TBase> ConvertIList<T, TBase>(IList<T> list) where T : TBase
{
    List<TBase> baseList = new List<TBase>(list.Count);

    for (int index = 0; index < list.Count; index++)
    {
        baseList.Add(list[index]);
    }
    return baseList;
}
static void Main(string[] args)
{
    // 初始化一个List<String> , 它实现了IList<String>
    List<String> ls = new List<string>();
    ls.Add("A String");

    // 将IList<String>转成IList<Object>
    // 编译器检查String是否兼容于Object,由于是派生关系,所以满足约束 T(string) : TBase(Object)
    IList<Object> lo = ConvertIList<String, Object>(ls);

    // 将IList<String> 转成IList<IComparable>
    // 编译器检查Strings是否实现了IComparable接口,由于String实现了,所以也满足约束.
    IList<IComparable> lc = ConvertIList<String, IComparable>(ls);

    // 将IList<String> 转成IList<IComparable<String>>
    // 由于String实现了接口,所以也满足约束.
    IList<IComparable<String>> lcs = ConvertIList<String, IComparable<String>>(ls);

    //  IList<String>转成 IList<String>
    IList<String> ls2 = ConvertIList<String, String>(ls);

    // 错误,不能将IList<String>转 IList<Exception>
    // String没有隐式引用转换到Exception
    // IList<Exception> le = ConvertIList<String, Exception>(ls);
}

构造器约束

T : new()类型参数可以指定 0个或1个 构造器约束. 它向编译器承诺类型实参是 实现了公共无参构造器的非抽象类型. 如果同时和struct约束一起使用,C#编译器会认为这是一个错误,因为是多余的; 所有值类型都隐式提供了公共无参构造器.

internal sealed class Test<T> where T : new()
{
  public static T Factory()
  {
    // 允许,因为所有值类型都隐式有一个公共无参构造器
    // 如果约束为class, 约束也要求它提供公共无参构造器
    return new T();
  }
}

其他可验证性问题

几个特殊的代码构造和泛型共同使用时,可能产生不可预期的行为.

  1. 泛型类型变量的转型

    不允许将泛型类型转型为其他类型, 除非转型为与约束兼容的类型.

    private static void CastGenericType<T>(T obj)
    {
      // T是任意类型无法保证成功转型
      // Int32 x = (Int32) obj;   // 错误
      // String s = (String) obj; // 错误
    
      // 虽然能通过编译, 但是CLR仍有可能在运行时抛出InvalidCastException异常
      Int32 x = (Int32) (Object) obj;    // 可能报异常
      String s1 = (String) (Object) obj; // 可能报异常
    
      // 转型为引用类型时还可以使用C# as操作符
      // 使用了as就不会报异常
      // 值类型不能用as
      String s2 = obj as String; // 无错误
    }
    
  2. 将泛型类型变量设为默认值

    default(T)不允许将泛型类型变量设为null, 除非将泛型类型约束成引用类型.

    由于未对T进行约束,所以可能是值类型.

    private static void SettingDefault<T>()
    {
      // 编译错误,因为可能是不可以为null的值类型, 考虑改用default(T)
      // T temp = null;
    
      // default告诉编译器和CLR的JIT编译器,如果T是引用类型,就将temp设为null
      // 如果temp是值类型,就将temp的所有位设为0
      T temp = default(T);
    }
    
  3. 将泛型类型变量与null进行比较

    不论泛型类型是否被约束,使用==!=操作符将泛型类型与null进行比较都是可以的.

    // 由于T类型未进行约束, 所以可能是值类型或引用类型
    // 如果是值类型,obj永远不会为null
    // 如果被约束为struct, C# 编译器会报错,
    // 因为值类型的变量不能与null比较,因为结果始终一样
    private static void Compare<T>(T obj)
    {
      if (obj == null)
      {
        // 如果obj是值类型, 这里的代码永远不会执行
        // JIT编译器不会为此处的代码生成本机代码
        // 如果 换成!=操作符
        // 则不会为if (obj != null) 生成本机代码,因为永远为true
        // 但是大括号内还是会生成
      }
    }
    

    如果是值类型,obj永远不会为null. 如果被约束为struct, C# 编译器会报错,因为值类型的变量不能与null比较,因为结果始终一样.

  4. 两个泛型类型变量比较

    如果泛型参数不限定为引用类型,对两个变量进行比较就是非法的.

    因为两个值类型的变量互相比较是非法的, 除非值类型重载了==操作符.

    private static void Compare<T>(T o1, T o2)
    {
      if(o1 == o2) {} // 编译错误
    }
    
    // 对于非`基元值类型的值类型`,C#不知道如何比较,所以编译器会报错.
    //private static void Compare<T>(T o1, T o2) where T : struct
    
    private static void Compare<T>(T o1, T o2) where T : class
    {
      if(o1 == o2) {} // 编译成功
    }
    

    上述代码如果T约束成class,就能编译通过.

    1. 如果引用相同的对象, ==操作符就返回true.
    2. 如果引用类型重载了==, 编译器会生成对operator==方法的调用代码

    写代码来比较基元值类型,C#编译器能知道生成正确的代码.如果约束为struct, 对于非基元值类型的值类型,C#不知道如何比较,所以编译器会报错.

    不允许将类型参数约束成具体的值类型, 因为值类型隐式密封,不可能存在从值类型派生的类型.如果支持,那就好比只支持该具体类型, 泛型参数的意义就不存在了.

    Only class or interface could be specified as constraint 只有类或接口可以指定为约束.

    private static void Test(T t) where T : Int32 // 编译错误.

泛型类型变量作为操作数使用

操作符用于泛型类型的操作数会出现大量问题. C# 知道如何解释应用于基元类型的操作符(加减乘除), 但是不能将这些操作符应用于泛型类型的变量.

编译器在编译时确定不了类型, 所以不能向泛型类型的变量应用任何操作符.

因此不可能写出一个能处理任何数值数据类型的算法.

// 尝试写一个能处理任何数据类型的算法
private static T Sum<T>(T num) where T : struct
{
    T sum = default(T);
    // 报错. 运算符< ++ += 无法应用于"T"和"T"类型的操作数
    for (T n = default(T); n < num; n++)
    {
        // 报错. 运算符< ++ += 无法应用于"T"和"T"类型的操作数
        sum += n;
    }
    return sum;
}

这是CLR的泛型支持体系的一个严重限制, 许多开发人员(科学,金融,数学领域)对这个限制失望. 通过别的技术来避开这个以限制, 反射,dynamic基元类型操作符重载等. 但是这些技术会严重损害性能和代码的可读性.

Licensed under CC BY-NC-SA 4.0
0