泛型类和泛型方法兼具可重用性、类型安全性和效率,这是非泛型类和非泛型方法无法实现的。 泛型通常与集合以及作用于集合的方法一起使用。 System.Collections.Generic 命名空间包含几个基于泛型的集合类。本文主要介绍C# 泛型。

1、泛型

泛型将类型参数的概念引入 .NET,使用泛型类型可以最大限度地重用代码、保护类型安全性以及提高性能。泛型最常见的用途是创建集合类。.NET 类库在 System.Collections.Generic 命名空间中包含几个新的泛型集合类。 应尽可能使用这些类来代替某些类,如 System.Collections 命名空间中的 ArrayList

1)可以创建自己的泛型接口、泛型类、泛型方法、泛型事件和泛型委托。

2)可以对泛型类进行约束以访问特定数据类型的方法。

3)在泛型数据类型中所用类型的信息可在运行时通过使用反射来获取。

2、定义使用泛型

型是为所存储或使用的一个或多个类型具有占位符(类型形参)的类、结构、接口和方法。 泛型集合类可以将类型形参用作其存储的对象类型的占位符;类型形参呈现为其字段的类型和其方法的参数类型。 泛型方法可将其类型形参用作其返回值的类型或用作其形参之一的类型。 下面看一下一个简单的泛型类定认:

例如,

public class Generic<T>
{
public T Field;
}

创建泛型类的实例时,指定用于替代类型形参的实际类型。 在类型形参出现的每一处位置用选定的类型进行替代,这会建立一个被称为构造泛型类的新泛型类。 将得到根据你选择的类型而定制的类型安全类。

例如,

public static void Main()
{
    Generic<string> g = new Generic<string>();
    g.Field = "A string";
    //...
    Console.WriteLine("Generic.Field           = \"{0}\"", g.Field);
    Console.WriteLine("Generic.Field.GetType() = {0}", g.Field.GetType().FullName);
}

3、泛型相关概念

1)泛型类型定义 是用作模板的类、结构或接口声明,带有可包含或使用的类型的占位符。 例如, System.Collections.Generic.Dictionary<TKey,TValue> 类可以包含两种类型:密钥和值。 由于泛型类型定义只是一个模板,所以你无法创建作为泛型类型定义的类、结构或接口的实例。

2)泛型类型参数(或 类型参数)是泛型类型或方法定义中的占位符。 System.Collections.Generic.Dictionary<TKey,TValue> 泛型类型具有两个类型形参 TKeyTValue,它们分别代表密钥和值的类型。

3)构造泛型类型(或 构造类型)是为泛型类型定义的泛型类型形参指定类型的结果。

4)泛型类型实参 是被泛型类型形参所替代的任何类型。

5)常见术语 泛型类型 包括构造类型和泛型类型定义。

6)借助泛型类型参数的 协变 和 逆变,可以使用类型自变量的派生程度比目标构造类型更高(协变)或更低(逆变)的构造泛型类型。 协变和逆变统称为“变体” 。 有关详细信息,请参阅协变和逆变。

7)约束 是对泛型类型参数的限制。 例如,你可能会将一个类型形参限制为实现 System.Collections.Generic.IComparer<T> 泛型接口的类型,以确保可对该类型的实例进行排序。 此外,你还可以将类型形参限制为具有特定基类、具有无参数构造函数或作为引用类型或值类型的类型。 泛型类型的用户不能替换不满足约束条件的类型实参。

4、泛型方法定义

泛型方法定义 是具有两个形参列表的方法:泛型类型形参列表和形参列表。 类型形参可作为返回类型或形参类型出现,例如,

T Generic<T>(T arg)
{
    T temp = arg;
    //...
    return temp;
}

泛型方法可出现在泛型或非泛型类型中。 值得注意的是,方法不会仅因为它属于泛型类型或甚至因为它有类型为封闭类型泛型参数的形参而成为泛型方法。 只有当方法有属于自己的类型形参列表时才是泛型方法。 在以下代码中,只有方法 G 是泛型方法。

例如,

class A
{
    T G<T>(T arg)
    {
        T temp = arg;
        //...
        return temp;
    }
}
class Generic<T>
{
    T M(T arg)
    {
        T temp = arg;
        //...
        return temp;
    }
}

5、泛型约束

约束告知编译器类型参数必须具备的功能。 在没有任何约束的情况下,类型参数可以是任何类型。 编译器只能假定 System.Object 的成员,它是任何 .NET 类型的最终基类。 有关详细信息,请参阅使用约束的原因。 如果客户端代码使用不满足约束的类型,编译器将发出错误。 通过使用 where 上下文关键字指定约束。 下表列出了各种类型的约束:

约束

描述

where T : struct

类型参数必须是不可为 null 的值类型。 

有关可为 null 的值类型的信息,

请参阅可为 null 的值类型。 

由于所有值类型都具有可访问的无参数构造函数,

因此 struct 约束表示 new() 约束,

并且不能与 new() 约束结合使用。

 struct 约束也不能与 unmanaged 约束结合使用。

where T : class

类型参数必须是引用类型。 

此约束还应用于任何类、接口、

委托或数组类型。

 在 C#8.0 或更高版本中的可为 null 上下文中,

T 必须是不可为 null 的引用类型。

where T : class?

类型参数必须是可为 null 或不可为 null 的引用类型。 

此约束还应用于任何类、接口、委托或数组类型。

where T : notnull

类型参数必须是不可为 null 的类型。 

参数可以是 C# 8.0 

或更高版本中的不可为 null 的引用类型,

也可以是不可为 null 的值类型。

where T : unmanaged

类型参数必须是不可为 null 的非托管类型。 

unmanaged 约束表示 struct 约束,

且不能与 struct 约束或 new() 约束结合使用。

where T : new()

类型参数必须具有公共无参数构造函数。

 与其他约束一起使用时,new() 约束必须最后指定。 

new() 约束不能与 struct 

和 unmanaged 约束结合使用。

where T : <base class name>

类型参数必须是指定的基类或派生自指定的基类。

 在 C# 8.0 及更高版本中的可为 null 上下文中,

T 必须是从指定基类派生的不可为 null 的引用类型。

where T : <base class name>?

类型参数必须是指定的基类或派生自指定的基类。

 在 C# 8.0 及更高版本中的可为 null 上下文中,

T 可以是从指定基类派生的可为 null 

或不可为 null 的类型。

where T : <interface name>

类型参数必须是指定的接口或实现指定的接口。 

可指定多个接口约束。 约束接口也可以是泛型。

 在 C# 8.0 及更高版本中的可为 null 上下文中,

T 必须是实现指定接口的不可为 null 的类型。

where T : <interface name>?

类型参数必须是指定的接口或实现指定的接口。

 可指定多个接口约束。 约束接口也可以是泛型。

 在 C# 8.0 中的可为 null 上下文中,

T 可以是可为 null 的引用类型、

不可为 null 的引用类型或值类型。 

T 不能是可为 null 的值类型。

where T : U

为 T 提供的类型参数必须是为 U 

提供的参数或派生自为 U 提供的参数。

 在可为 null 的上下文中,

如果 U 是不可为 null 的引用类型,

T 必须是不可为 null 的引用类型。

 如果 U 是可为 null 的引用类型,

则 T 可以是可为 null 的引用类型,

也可以是不可为 null 的引用类型。

注意:使用约束指定类型参数的功能和预期。 声明这些约束意味着你可以使用约束类型的操作和方法调用。 如果泛型类或方法对泛型成员使用除简单赋值之外的任何操作或调用 System.Object 不支持的任何方法,则必须对类型参数应用约束。

例如,

public class Employee
{
    public Employee(string name, int id) => (Name, ID) = (name, id);
    public string Name { get; set; }
    public int ID { get; set; }
}
public class GenericList<T> where T : Employee
{
    private class Node
    {
        public Node(T t) => (Next, Data) = (null, t);
        public Node Next { get; set; }
        public T Data { get; set; }
    }
    private Node head;
    public void AddHead(T t)
    {
        Node n = new Node(t) { Next = head };
        head = n;
    }
    public IEnumerator<T> GetEnumerator()
    {
        Node current = head;
        while (current != null)
        {
            yield return current.Data;
            current = current.Next;
        }
    }
    public T FindFirstOccurrence(string s)
    {
        Node current = head;
        T t = null;
        while (current != null)
        {
            //The constraint enables access to the Name property.
            if (current.Data.Name == s)
            {
                t = current.Data;
                break;
            }
            else
            {
                current = current.Next;
            }
        }
        return t;
    }
}

6、泛型委托

委托可以定义它自己的类型参数。 引用泛型委托的代码可以指定类型参数以创建封闭式构造类型,就像实例化泛型类或调用泛型方法一样,例如,

public delegate void Del<T>(T item);
public static void Notify(int i) { }
Del<int> m1 = new Del<int>(Notify);

根据典型设计模式定义事件时,泛型委托特别有用,因为发件人参数可以为强类型,无需在它和 Object 之间强制转换。例如,

delegate void StackEventHandler<T, U>(T sender, U eventArgs);
class Stack<T>
{
    public class StackEventArgs : System.EventArgs { }
    public event StackEventHandler<Stack<T>, StackEventArgs> stackEvent;
    protected virtual void OnStackChanged(StackEventArgs a)
    {
        stackEvent(this, a);
    }
}
class SampleClass
{
    public void HandleStackChange<T>(Stack<T> stack, Stack<T>.StackEventArgs args) { }
}
public static void Test()
{
    Stack<double> s = new Stack<double>();
    SampleClass o = new SampleClass();
    s.stackEvent += o.HandleStackChange;
}