泛型

泛型是C#语言自2.0版本及其公共语言运行时(CLR)以来引入的一项新特性。泛型引入了类型参数的概念,使得开发者可以在声明和实例化类或方法时推迟指定一个或多个类型。泛型的最常见用途是创建集合类。泛型类型的引入旨在最大化代码复用、类型安全和性能。

泛型类

有时你需要创建一个类来管理某种类型的对象,但不对这些对象进行修改。没有泛型时,通常的做法(简化版)是像这样:

public class SomeObjectContainer
{
    private object _obj;

    public SomeObjectContainer(object obj)
    {
        this._obj = obj;
    }

    public object GetObject()
    {
        return this._obj;
    }
}

它的使用示例如下:

class Program
{
    static void Main(string[] args)
    {
        SomeObjectContainer container = new SomeObjectContainer(25);
        SomeObjectContainer container2 = new SomeObjectContainer(5);

        Console.WriteLine((int)container.GetObject() + (int)container2.GetObject());
        Console.ReadKey(); // 等待用户按任意键,以便查看结果
    }
}

注意,我们每次从容器中获取对象时,都必须将其强制转换回原始数据类型(在这个例子中是 int)。在像这样的小程序中,这一切是清楚的。但在程序更复杂且包含多个容器的情况下,我们必须小心,确保容器中应该只包含 int 类型,否则会抛出 InvalidCastException

此外,如果我们选择的原始数据类型是值类型,例如 int,每次访问集合元素时,由于C#的自动装箱特性,将会产生性能损失。

然而,我们可以通过用 try-catch 块包围每个不安全的区域,或者为每种需要的类型创建一个单独的“容器”来避免强制转换。虽然这两种方法可以工作(并且已经使用了很多年),但现在已经不必要了,因为泛型提供了一个更优雅的解决方案。

为了让我们的“容器”类支持任何对象并避免强制转换,我们将每个原有的对象类型替换为新的名称(在这种情况下是 T),并在类名后面加上 <T> 标记,表示 T 类型是泛型/任意类型。

注意:你可以选择任何名称并使用多个泛型类型,例如 <genKey, genVal>

public class GenericObjectContainer<T>
{
    private T _obj;

    public GenericObjectContainer(T obj)
    {
        this._obj = obj;
    }

    public T GetObject()
    {
        return this._obj;
    }
}

这并没有太大的不同,结果是简洁且安全的使用:

class Program
{
    static void Main(string[] args)
    {
        GenericObjectContainer<int> container = new GenericObjectContainer<int>(25);
        GenericObjectContainer<int> container2 = new GenericObjectContainer<int>(5);
        Console.WriteLine(container.GetObject() + container2.GetObject());

        Console.ReadKey(); // 等待用户按任意键,以便查看结果
    }
}

泛型确保你只需为“容器”指定类型一次,从而避免了上述问题,并避免了值类型(例如 intshortstring 或任何自定义结构体)在每次操作存储元素时引发的自动装箱性能损失。

虽然这个例子远非实际应用,但它确实展示了泛型的某些有用场景:

  • 你需要在一个类中存储单一类型的对象
  • 你不需要修改对象
  • 你需要以某种方式操作对象
  • 你希望将“值类型”(例如 intshortstring 或任何自定义结构体)存储在集合类中,而不产生每次操作存储元素时的性能损失。

泛型接口

泛型接口接受一个或多个类型参数,类似于泛型类:

public interface IContainer<T>
{
    T GetObject();
    void SetObject(T value);
}

public class StringContainer : IContainer<string>
{
    private string _str;
    
    public string GetObject()
    {
        return _str;
    }
    
    public void SetObject(string value)
    {
        _str = value;
    }
}

public class FileWithString : IContainer<string>
{
    // ...
}

class Program
{
    static void Main(string[] args)
    {
        IContainer<string> container = new StringContainer();
        
        container.SetObject("test");

        Console.WriteLine(container.GetObject());
        container = new FileWithString();

        container.SetObject("another test");

        Console.WriteLine(container.GetObject());
        Console.ReadKey();
    }
}

泛型接口在可能有多个实现的情况下非常有用。例如,List<T> 类(稍后会讨论)和 LinkedList<T> 类都实现了 IEnumerable<T> 接口。List<T> 类有一个构造函数,它基于一个实现了 IEnumerable<T> 接口的现有对象创建一个新列表,因此我们可以编写如下代码:

LinkedList<int> linkedList = new LinkedList<int>();

linkedList.AddLast(1);
linkedList.AddLast(2);
linkedList.AddLast(3);
// linkedList 现在包含 1, 2 和 3。

List<int> list = new List<int>(linkedList);

// 现在 list 也包含 1, 2 和 3!

泛型方法

泛型方法与泛型类和接口非常相似:

using System;
using System.Collections.Generic;

public static bool ArrayContains<T>(T[] array, T element)
{
    foreach (T e in array)
    {
        if (e.Equals(element))
        {
            return true;
        }
    }

    return false;
}

这个方法可以用于搜索任何类型的数组:

using System;
using System.Collections.Generic;

class Program
{
    static void Main(string[] args)
    {
        string[] strArray = { "string one", "string two", "string three" };
        int[] intArray = { 123, 456, 789 };
        
        Console.WriteLine(ArrayContains<string>(strArray, "string one")); // True
        Console.WriteLine(ArrayContains<int>(intArray, 135)); // False
    }
}

类型约束

你可以在任何泛型类、接口或方法中使用 where 关键字指定一个或多个类型约束。以下示例显示了所有可能的类型约束:

public class MyClass<T, U, V, W>
    where T : class,        // T 应该是引用类型(数组、类、委托、接口)
        new()               // T 应该具有无参数的公共构造函数
    where U : struct        // U 应该是值类型(byte、double、float、int、long、struct、uint 等)
    where V : MyOtherClass, // V 应该从 MyOtherClass 派生
        IEnumerable<U>      // V 应该实现 IEnumerable<U>
    where W : T,            // W 应该从 T 派生
        IDisposable         // W 应该实现 IDisposable
{
    // ...
}

这些类型约束通常在以下情况下是必要的:

  • 创建泛型类型的新实例(new() 约束)
  • 对泛型类型的变量使用 foreachIEnumerable<T> 约束)
  • 对泛型类型的变量使用 usingIDisposable 约束)
最后修改: 2025年01月12日 星期日 00:04