泛型

程序= 算法 + 数据。编程人员设计好一种算法,例如排序、比较、交换等等。这些算法应该应用于不同的数据类型,而不是为每个数据类型都写一个专有的算法。

CLR 允许创建:泛型引用类型,泛型值类型,泛型接口,泛型委托。

CLR 不允许创建:泛型枚举类型。

在FCL 中泛型最明显的应用就是集合类,FLC 在 System.Collections.Generic 和 System.Collections.ObjectModel 命名空间中提供了多个泛型集合类。System.Collections.Concurrent 命名空间则提供了线程安全的泛型集合类。

 

开放类型和封闭类型

具有泛型类型参数的类型任然是类型,CLR 同样会为它创建内部的类型对象。这一点适合引用类型(类)、值类型(结构)、接口类型和委托类型。具有泛型类型的参数类型称为开放类型,CLR 禁止构造开放类型的任何实例。这类似于CLR 禁止构造接口类型的实例。

代码引用泛型类型时可指定一组泛型类型实参。为所有类型参数都传递了实际的数据类型,类型就成了封闭类型。CLR允许构造封闭类型的实例。

using System;
using System.Collections.Generic;
// A partially specified open type
internal sealed class DictionaryStringKey<TValue> :
    Dictionary<String, TValue> {
}
public static class Program {
    public static void Main() {
            Object o = null;
            // Dictionary<,> is an open type having 2 type parameters
            Type t = typeof(Dictionary<,>);
            // Try to create an instance of this type (fails)
            o = CreateInstance(t);
            Console.WriteLine();
            // DictionaryStringKey<> is an open type having 1 type parameter
            t = typeof(DictionaryStringKey<>);
            // Try to create an instance of this type (fails)
            o = CreateInstance(t);
            Console.WriteLine();
            // DictionaryStringKey<Guid> is a closed type
            t = typeof(DictionaryStringKey<Guid>);
            // Try to create an instance of this type (succeeds)
            o = CreateInstance(t);
            // Prove it actually worked
            Console.WriteLine("Object type=" + o.GetType());
        }
        private static Object CreateInstance(Type t) {CHAPTER 12 Generics 273
        Object o = null;
        try {
            o = Activator.CreateInstance(t);
            Console.Write("Created instance of {0}", 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 DictionaryStringKey`1[TValue] because
Type.ContainsGenericParameters is true.
Created instance of DictionaryStringKey`1[System.Guid]
Object type=DictionaryStringKey`1[System.Guid]

 

泛型类型和继承

泛型类型任然是类型,所以能从其他任何类型派生。使用泛型类型并指定类型参数时,实际是在CLR 中定义了一个新的类型对象,这个新的类型对象从泛型类型派生自的哪个类型派生。例如: List<T> 从 Object 派生,所以List<String> 和 List<Giud> 也从Object 派生。指定类型实参不影响继承层次结构,理解这一点,有助于你判断哪些强制类型转换是允许的,哪些不允许。

 

泛型类型同一性

同一性就是为了方便使用泛型类型,可以像C++中的宏定义那样将一个泛型类型用其他的符号代表。C# 允许使用简化的语法来引用泛型封闭类型,同时不会影响类型的相等性。这个语法要求在源文件顶部使用传统的using 指令,例如:

using DateTimeList = System.Collections.Generic.List<System.DateTime>;

执行下面的代码验证一下:

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

 

代码爆炸

使用泛型类型参数的方法在进行JIT 编译时,CLR 获取方法的IL,用指定的类型实参替换,然后创建恰当的本机代码(这些代码是为操作指定数据类型“量身定制的”),这正是泛型的重要特点。但这样做也有一个缺点: CLR 要为每种不同的方法/类型 组合生成本机代码。我们将这个现象称为代码爆炸。它可能会造成应用程序的工作集显著增大,从而损害性能。

CLR 内建了一些措施能缓解代码爆炸。

1、一次编译,重复使用。为特定的类型实参调用了一个方法后,以后再调用相同的类型实参的方法时,CLR只会在第一次编译代码。

2、CLR 认为所有引用类型实参都完全相同,所以代码能够共享。对于任何引用类型的实参,都会调相同的代码。

但是假如某个类型实参是值类型,CLR 就必须专门为哪个值类型生成本机代码。

 

泛型接口

如果没有泛型接口,每次用非泛型接口(如 IComparable)来操纵值类型都会发生装箱,而且会失去编译时的类型安全性。因此,CLR 提供了对泛型接口的支持。引用类型或值类型可指定类型实参实现泛型接口。也可保持类型实参的未指定状态来实现泛型接口。

 

  

泛型委托

CLR 支持泛型委托,目的是保证任何类型的对象都能以类型安全的方式传给回调方法。泛型委托允许值类型实例在传给回调方法时不进行任何装箱。

 

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

委托的每个泛型类类型参数都可以标记为协变量或逆变量。利用这个功能,可将泛型委托类型的变量转换为相同的委托类型(但泛型参数类型不同)。泛型类型参数可以是以下任何一种形式。

  • 不变量,意味着泛型类型参数不能改变。

  • 逆变量,意味着泛型类型参数可以从一个类更改为它的某个派生类。C# 用in 关键字标识逆变量。

  • 协变量,意味着泛型类型参数可以从一个类更改为它的某个基类。C# 用 out 关键字标识协变量。

 

泛型和其他成员

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

 

可验证型和约束

约束的作用是限制能指定成泛型实参的类型数量。通过限制类型的数量,可以对哪些类型执行更多的操作。

编译器/CLR 允许向类型参数应用各种约束。可以用一个主要约束、一个次要约束以及/或者一个构造器约束来约束类型参数。

主要约束

主要约束可以是代表非密封类的一个引用类型。不能指定以下特殊引用类型:SystemObject,SystemArray,System.Delegate,System.MulticastDelegate,System.ValueType,System.Enum 或者 System.Void。

指定引用类型约束时,相当于向编译器承诺:一个指定的类型实参要么是与约束类型相同的类型,要么是从约束类型派生的类型。例如以下泛型类:

internal sealed class PrimaryConstraintOfStream<T> where T : Stream {
    public void M(T stream) {
        stream.Close();// OK
    }
}

 

有两个特殊的主要约束:class 和 struct。其中,class 约束向编译器承诺类型实参是引用类型。任何类类型、接口类型、委托类型或者数组类型都满足这个约束。

struct 约束向编译器承诺类型实参是值类型。包括枚举在内的任何值类型都满足这个约束。但是编译器和 CLR 将任何System.Nullable<T> 值类型视为特殊类型,不满足这个 struct 约束。

 

次要约束

次要约束代表接口类型。这种约束向编译器承诺类型实参实现了接口。这种约束向编译器承诺类型实参实现了接口。由于能指定多个接口约束,所以类型实参必须实现了所有接口约束。

还有一种次要约束称为 类型参数约束,有时也称为 裸类型约束。它允许一个泛型类型或方法规定:指定的类型实参要么就是约束的类型,要么就是约束的类型的派生类。一个类型参数可以指定零个或者多个类型参数约束。下面这个泛型方法演示了如何使用类型参数约束:

private static List<TBase> ConvertIList<T, TBase>(IList<T> list) where T : TBase {
    List<TBase> baseList = new List<TBase>(list.Count);
    for (Int32 index = 0; index < list.Count; index++) {
        baseList.Add(list[index]);
    }
    return baseList;
}

ConvertList 方法指定了两个类型参数,其中T 参数由Tbase 类型参数约束。意味着不管为T 指定什么类型实参,都必须兼容于为 TBase 指定的类型实参。

 

构造器约束

类型参数可指定零个或一个构造器约束,它向编译器承诺类型实参是实现了公共无参构造器的非抽象类型。以下示例类使用构造器约束来约束它的类型参数:

internal sealed class ConstructorConstraint<T> where T : new() {
    public static T Factory() {
        // Allowed because all value types implicitly
        // have a public, parameterless constructor and because
        // the constraint requires that any specified reference
        // type also have a public, parameterless constructor
        return new T();
    }
}

 

在上述例子中 new T() 是合法的,因为已知 T 是拥有公共无参构造器的类型。对所有值类型来说,这一点(拥有公共无参构造器)肯定成立。对于作为类型实参指定的任何引用类型,这一点也成立,因为构造器约束要求它必须成立。

 

其他可验证性问题

1、将泛型类型的变量转型为其他类型是非法的,除非转型为与约束兼容的类型。

private static void CastingAGenericTypeVariable1<T>(T obj) {
    Int32 x = (Int32) obj; // Error
    String s = (String) obj; // Error
}
private static void CastingAGenericTypeVariable2<T>(T obj) {
    Int32 x = (Int32) (Object) obj; // No error
    String s = (String) (Object) obj; // No error
}

2、将泛型类型变量设为 null 是非法的,除非将泛型类型约束成引用类型。

3、无论泛型类型是否被约束,使用== 或 != 操作符将泛型类型变量与 null 进行比较都是合法的。如果T 被约束成 struct , C# 编译器会报错。值类型的变量不能与null 进行比较,因为结果始终一样。

4、如果泛型类型参数不能肯定是引用类型,对同一个泛型类型的两个变量进行比较是非法的:

private static void ComparingTwoGenericTypeVariables<T>(T o1, T o2) {
    if (o1 == o2) { } // Error
}

5、将操作符用于泛型类型的操作会引发大量问题。C# 知道如何解释应用于基元类型的操作符(比如+,- ,* 和/)。但是不能将这些操作符用于泛型类型的变量。编译器在编译时确定不了类型。

 

posted on 2019-10-13 11:17  拾掇的往昔  阅读(267)  评论(0编辑  收藏  举报

导航