第五章 泛型

​ 本章介绍类, 接口, 方法的泛型. 用于委托的泛型参见第 8 章.

泛型概述

​ 不同于 C++ 中模板在实例化时需要模板的源代码且会为每个模板的实例化创建单独的二进制码, C# 的泛型是 C# 语言的一种结构, 是 CLR (公共语言运行库)定义的.

性能

​ 使用泛型集合类可以避免大量装箱和拆箱操作, 进而提升性能表现. 如果对值使用非泛型集合类(位于命名空间System.Collections中, 如ArrayList), 在把值类型转化为引用类型(boxing, 装箱), 和把引用类型转化为值类型(unboxing, 拆箱)时会产生较大性能损失, 尤其在遍历很多项时.

  • 如果在把值类型传递给需要对象的变量, 装箱就会自动进行; 装箱后的值类型可以通过拆箱重新变为值类型, 但拆箱时需要使用强制类型转换.

System.Collections.Generic命名空间中的泛型集合类(如List<T>)不会对值类型使用对象(如List<int>), 而是直接使用值类型存储.

var list1 = new ArrayList();
list1.Add(44);	//boxing
int i1 = (int)list[0];	//unboxing
foreach(int i2 in list)
{
    Console.WriteLine(i2);	//unboxing
}

var list2 = List<int>();
list2.Add(44);	//no boxing
int i3 = list2[0];	//no unboxing, no cast needed
foreach(int i4 in list2)
{
    Console.WriteLine(i4);
}

二进制代码的重用与代码重用

​ 泛型类可以只定义一次, 用许多不同类型实例化, 而不需要像 C++ 一样访问源代码. ​ 泛型类型可以在一种语言中定义, 在任何其他 .NET 语言中使用.

​ 在使用不同类型实例化泛型类型时, 会创建多少代码? 因为泛型类定义放在程序集中, 所以使用特定类型实例化泛型类不会在 IL 代码中复制这些类. 但是, 在 JIT(Just-In-Time) 编译器把泛型类编译为本地代码时, 会给每个值类型创建一个新类, 而使引用类型共享同一个本地类所有相同的实现代码. 这是因为每个值类型对内存的要求不相同, 涉及分配不同的栈内存, 所以要为每个值类型实例化一个新类; 而引用在栈上的内存都相同, 故引用类型实例化的泛型类栈内存布局相同, 所以不同引用类型实例化的泛型类可以使用相同的代码.

命名约定

​ 一般情况下使用T作为泛型类型的名称, 如public class List<T>. ​ 如果泛型类型有特定要求(如需要实现特定接口或派生自特定类), 或者使用了不止一个泛型类型, 就应该使用描述性的名称:

public delegate void EventHandler<TEventArgs>(object sender, TEventArgs e);
public delegate TOutput Converter<TInput, TOutput>(TInput from);

创建泛型类

​ 下面创建一个泛型链表, 语法如下.

public class LinkedList<T> : IEnumerable<T>
{
    readonly private LinkedListNode<T> Head = new LinkedListNode<T>();
    public LinkedListNode<T> First { get; private set; }
    public LinkedListNode<T> Last { get; private set; }
    public LinkedList()
    {
        First = Head.Next;
        Last = Head;
    }
    public void Append(T value)
    {
        Last.Next = new LinkedListNode<T>(value);
        Last.Next.Prev = Last;
        Last = Last.Next;
        Last.Next = null;
    }
    public IEnumerator<T> GetEnumerator()
    {
        var current = Head.Next;
        while (current != null)
        {
            yield return current.value;
            current = current.Next;
        }
    }

    IEnumerator IEnumerable.GetEnumerator() =>
        GetEnumerator();
}

public class LinkedListNode<T>
{
    public T value;
    public LinkedListNode<T> Prev = null;
    public LinkedListNode<T> Next = null;
    public LinkedListNode() { }
    public LinkedListNode(T _value) => value = _value;

}

泛型类的功能

默认值

​ 不能把null赋值给泛型类型, 因为泛型类型可能是不可空的值类型. 此时可以使用default关键字, 将null赋予引用类型, 将 0 赋予值类型.

public class LinkedListNode<T>
{
	public T value;
	public LinkedListNode<T> Prev = null;
	public LinkedListNode<T> Next = null;
	public LinkedListNode() => value = default;
	public LinkedListNode(T _value) => value = _value;
}

约束

​ 使用where关键字为泛型类型添加约束, 指定对泛型类型的特定要求.

约束 说明
where T : struct T必须是不可为 null 的值类型, 其中包含 record struct 类型. 由于所有值类型都具有可访问的无参数构造函数(无论是声明的还是隐式的), 因此 struct 约束表示 new() 约束, 并且不能与 new() 约束结合使用. struct 约束也不能与 unmanaged 约束结合使用.
where T : class T必须是引用类型. 此约束还应用于任何类、接口、委托或数组类型. 在可为 null 的上下文中, T 必须是不可为 null 的引用类型.
where T : class? T必须是可为 null 或不可为 null 的引用类型. 此约束还应用于任何类、接口、委托或数组类型(包括记录).
where T : notnull T必须是不可为 null 的类型. 参数可以是不可为 null 的引用类型, 或不可为 null 的值类型.
where T : unmanaged T必须是不可为 null 的非托管类型. unmanaged 约束表示 struct 约束, 且不能与 struct 约束或 new() 约束结合使用.
where T : new() T必须具有公共无参数构造函数. 与其他约束一起使用时, new() 约束必须最后指定. new() 约束不能与 structunmanaged 约束结合使用.
where T : Foo T必须是Foo或派生自基类Foo. 在可为 null 的上下文中, T 必须是从指定基类派生的不可为 null 的引用类型.
where T : Foo? T必须是Foo或派生自基类Foo. 在可为 null 的上下文中, T 可以是从指定基类派生的可为 null 或不可为 null 的类型.
where T : IFoo T必须是接口IFoo或实现接口IFoo. 可指定多个接口约束. 约束接口也可以是泛型. 在的可为 null 的上下文中, T 必须是实现指定接口的不可为 null 的类型.
where T : IFoo? T必须是接口IFoo或实现接口IFoo. 可指定多个接口约束. 约束接口也可以是泛型. 在可为 null 的上下文中, T 可以是可为 null 的引用类型、不可为 null 的引用类型或值类型. T 不能是可为 null 的值类型.
where T1 : T2 T1 提供的类型参数必须是为泛型参数 T2 提供的类型参数或派生自为 T2 提供的类型参数. 在可为 null 的上下文中, 如果 T2 是不可为 null 的引用类型, T 必须是不可为 null 的引用类型. 如果 T2 是可为 null 的引用类型, 则 T 可以是可为 null 的引用类型, 也可以是不可为 null 的引用类型.
where T : default 重写方法或提供显式接口实现时, 如果需要指定不受约束的类型参数, 此约束可解决歧义. default 约束表示基方法, 但不包含 classstruct 约束. 有关详细信息, 请参阅default约束规范建议.
where T : allows ref struct 此反约束声明 T 的类型参数可以是 ref struct 类型. 该泛型类型或方法必须遵循 T 的任何实例的引用安全规则, 因为它可能是 ref struct.

​ 可以合并对同一泛型参数的多个约束, 约束间使用,隔开, 部分约束间有位置要求; 对多个泛型参数的约束, 即多个where字句间使用空白字符分隔.

class Foo<TTypeWithInterface, TValueType>
    where TTypeWithInterface: IFoo, new()
    where TValueType: struct
{
    //members... 
}

继承

​ 泛型类型的继承中, 基类可以是泛型类或非泛型类, 其中泛型类可以使用当前泛型类型参数, 也可以直接特化, 可以特化全部泛型参数或部分泛型参数.

public class Base<T> { }
public class Derived1<T>: Base<T> { }
public class Derived2<T1, T2>: Base<string> { }
public class Derived3<T1>: Derived2<int, T1> { }
public class Derived4: Derived1<double> { }

静态成员

​ 泛型类的静态成员在不同类型的实例化中为独立的成员.

public class StaticDemo<T>
{
    public static int x;
}
StaticDemo<string>.x = 4;
StaticDemo<int>.x = 5;
Console.WriteLine(StaticDemo<string>.x);	//output: 4

泛型接口

​ 允许在接口中使用泛型. .NET 为不同的情况提供了许多泛型接口, 例如ICollection<T>, IComparable<in T>, IEnumerable<out T>, IExtensibleObject<T>. 同一个接口常常存在比较老的非泛型版本, 例如 .NET 1.0 有基于objectIComparable接口. IComparable<in T>基于一个泛型类型:

public interface IComparable<in T> 
{
    int CompareTo(T other);
}

比较老的非泛型接口IComparable需要一个带CompareTo()方法的对象, 这需要强制转换为特定的类型:

public class Person: IComparable
{
    public int CompareTo(object obj)
    {
        Person other = obj as Person;
        return this.Lastname.CompareTo(other.Lastname);	

    }
}

而实现泛型的版本, 不再需要强制类型转换:

public class Person: IComparable<Person>
{
    public int CompareTo(Person other) =>
        Lastname.CompareTo(other.Lastname);//调用string的CompareTo方法
}

协变和抗变(Covariance and Contravariance)

协变抗变是泛型接口和委托之间的类型兼容性概念. 它们描述了当类型参数发生变化时, 类型之间的兼容性规则. 协变和抗变用于处理泛型类型参数的继承关系.

协变(covariance)允许使用更具体的类型. 这意味着, 如果你有一个泛型接口或委托, 它的类型参数是协变的, 那么你可以在使用父类类型时将其替换为子类类型. 协变适用于输出类型. ​ 抗变(contravariance)允许使用更泛化的类型. 这意味着, 如果你有一个泛型接口或委托, 它的类型参数是抗变的, 那么你可以在使用子类类型时将其替换为父类类型. 抗变适用于输入类型.

​ .NET 中参数类型是抗变的(即参数类型的使用是抗变的, 相对实际参数, 参数类型使用更泛化的类型), 例如可以将派生类Rectangle对象传递给接受基类Shape对象的参数, 因为这种情况下派生类拥有基类的所有成员:

public void Display(Shape o) { }
Display(rec);	//var rec = new Rectangle();

​ 方法的返回类型是协变的(即返回类型的使用是协变的, 相对被赋值对象, 返回类型使用更具体的类型). 例如可以使用方法返回的Rectangle对象赋值给Shape引用; 而当方法返回一个基类Shape对象时, 不能把它赋值给派生类Rectangle引用, 因为 Shape 不一定总是 Rectangle, 即基类并不总是拥有派生类所有功能.

public Rectangle GetRectangle() { }
Shape s = GetRectangle();

​ 在 .NET Framework 4 之前, 这种行为不适用于泛型; 自 C# 4 之后, 扩展后的语言支持泛型接口和泛型委托的协变和抗变.

​ 如果泛型参数不使用inout标注, 则类型将被定义为不变的(invariant).

​ 在C#中, 协变和抗变仅适用于泛型接口和泛型委托, 不适用于泛型类或结构.

泛型接口的协变

​ 如果泛型类型使用out关键字标注, 泛型接口就是协变的.

public interface IIndex<out T>
{
    T this[int index] { get; }
    int Count { get; }
}

在实际使用时, 可以将IIndex<Rectangle>赋值给IIndex<Shape>, IIndex<Rectangle>实际上是IIndex<Shape>的子类. 这时泛型接口的子类型序关系与原本的ShapeRectangle关系相同, 即Rectangle也是Shape的子类. 这种情况下, 程序在允许使用基类Shape的地方使用了派生类Rectangle, 即使用了更具体的类型.

泛型接口的抗变

​ 如果泛型类型参数使用in关键字标注, 泛型接口就是抗变的.

public interface IDisplay<in T>
{
    void Show(T item);
}

这会使IDisplay<Shape>实际上成为IDisplay<Rectangle>的子类, 可以将IDisplay<Shape>赋值给IDisplay<Rectangle>, 这时泛型接口的子类型序关系与原本的ShapeRectangle关系相反. 这种情况下, 程序在允许使用派生类Rectangle的地方使用了基类Shape, 即使用了更泛化的类型.

泛型结构

​ 类似于类, 结构也可以是泛型的. 它们的区别主要在于泛型结构没有继承特性.

Nullable<T>

Nullable<T>由 .NET Framework 定义, 用来解决将数据库或其他地方的可空数据映射到 C# 的不可空数据类型的问题. 下面展示了如何定义一个Nullable<T>的简化版本. ​ 结构Nullable<T>定义了约束要求类型 T 必须是结构. 对类使用该结构没有意义, 因为类本身就是可空的. 除了定义的 T 类型之外, 唯一的内粗开销是hasValue字段, 它确定变量是否为空. 除此之外还定义了只读属性HasValueValue, 以及一些运算符重载. 把Nullable<T>类型强制转换为 T 类型是显式定义的, 因为当hasValuefalse时, 试图使用值会抛出一个异常. 这种转换是隐式的.

struct Nullable<T>
    where T : struct
{
    public Nullable(T value)
    {
        _hasValue = true;
        _value = value;
    }
    private bool _hasValue;
    public bool HasValue => _hasValue;
    private T _value;
    public T Value
    {
        get
        {
            if (!_hasValue)
                throw new InvalidOperationException("no value");
            return _value;
        }
    }
    public static explicit operator T(Nullable<T> value) => value.Value;
    public static implicit operator Nullable<T>(T value) => 
        new Nullable<T>(value);
    public override string ToString() =>
        !HasValue ? string.Empty : _value.ToString();
        //一些运算符重载省略
}

​ C# 中使用可空类型可以不用使用泛型结构的语法, 而是通过?运算符, 使用type?语法声明可空类型变量.

int? x = GetNullableType();	//return an int? type variable
if (x == null)
{
    Console.WriteLine("x is null");
}
else if (x < 0)
{
    Console.WriteLine("x is negative");
}

​ 知道可空类型如何定义后, 下面就可以使用可空类型. ​ 可空类型参与加法运算时, 操作数只要有值为null, 其结果就是null. ​ 非可空类型总是可以成功转换为相应可空类型, 且这种转换是隐式的. 但从可空类型转换为非可空类型可能会失败, 因为null无法转换成相应类型. 如果试图这样做, 就会抛出InvalidOperationException异常. 因此从可空类型转换为不可空类型需要进行显式转换.

int a = 4;
int? b = a;
int c = (int)b;

除了显示转换外还可以使用合并运算符??将可空类型转换为不可空类型.??是二元运算符, 若左操作数不为null则表达式返回相应不可空类型的左操作数值; 若左操作数为null则返回值为右操作数.

int? x;
//do something...
int a = x ?? 0;

泛型方法

​ 允许在方法中使用泛型. 泛型方法可以在泛型或非泛型类中定义.

void Swap<T>(ref T x, ref T y)
{
    T temp = x;
    x = y;
    y = temp;
}

调用时可以显式把类型赋予泛型类型, 也可以让编译器通过参数推断需要调用的泛型方法.

int a = 9, b = 8;
Swap<int>(ref a, ref b);
Swap(ref a, ref b);

带约束的泛型方法

​ 泛型方法也可以使用where子句来添加约束. 用法与泛型类相同.

泛型方法规范

​ 一个方法可能有多个泛型与非泛型的重载. ​ 类似 C++ 的重载决议, 一般情况下, 对于有多个重载版本的方法调用, 如果可用, 编译器会优先使用非泛型版本或非泛型参数匹配的泛型参数少的版本; 否则使用泛型版本. ​ 所调用的方法是在编译期间而不是运行期间决定的, 因此有如下程序运行结果:

//Foo
public void Foo<T>(T obj);
public void Foo(int obj);

//Bar
public class MethodOverloads
{
    public void Bar<T>(T obj) => Foo(obj);
}

//Function call
var test = new MethodOverloads();
test.Bar(44);	//这里调用Foo<T>(T obj)而不是Foo(int obj)