文章目录

C# 泛型:<T> 类型参数与约束

发布于 2026-04-17 15:28:28 · 浏览 9 次 · 评论 0 条

C# 泛型:<T> 类型参数与约束

泛型是 C# 中强大的工具,它允许你编写灵活且可重用的代码。核心在于类型参数 <T>,它像一个占位符,在使用时才会被具体的类型替换。然而,由于编译器在编译阶段并不知道 T 到底是什么,它会限制你对 T 的操作。为了解决这个问题,必须使用“约束”来告诉编译器 T 必须具备的特征。


1. 定义基础泛型类型

打开你的开发环境,创建一个新的控制台应用程序。定义一个简单的泛型类,T 代表待定的类型。

public class Box
{
    public T Content { get; set; }

    public Box(T content)
    {
        Content = content;
    }
}

实例化该类:

var intBox = new Box<int>(123);
var stringBox = new Box<string>("Hello");

在这个阶段,T 可以是任何类型。编译器仅允许你调用 object 类型自带的方法(如 ToString(), Equals()),因为它对 T 一无所知。


2. 遇到的限制:为什么需要约束?

尝试Box 类中添加一个方法,用于比较两个 Box 的内容。

public bool IsEqual(Box other)
{
    return Content.CompareTo(other.Content) == 0;
}

编译代码。此时编译器会报错,提示 'T' 不包含 CompareTo 的定义。这是因为 T 可能是 int,也可能是 Image 或自定义类,而并非所有类型都有 CompareTo 方法。


3. 添加接口约束

为了让上述代码工作,修改类定义,添加 where T : IComparable 约束。这告诉编译器:T 必须是实现了 IComparable 接口的类型。

public class Box where T : IComparable
{
    public T Content { get; set; }

    public Box(T content)
    {
        Content = content;
    }

    public bool IsEqual(Box other)
    {
        // 现在 Content 保证拥有 CompareTo 方法
        return Content.CompareTo(other.Content) == 0;
    }
}

再次编译代码,错误消失。现在 Box 只能接受实现了 IComparable 的类型(如 int, string)。


4. 常见的泛型约束列表

C# 提供了多种约束类型。参考下表了解常用约束及其作用。

约束类型 描述 示例
where T : struct T 必须是值类型(如 int, bool, struct public void Process() where T : struct
where T : class T 必须是引用类型(如 class, interface, string, delegate public void Process() where T : class
where T : new() T 必须有一个无参的公共构造函数 public void Create() where T : new()
where T : <BaseClass> T 必须是指定的基类或其派生类 public void Process() where T : Person
where T : <Interface> T 必须是指定的接口 public void Sort() where T : IComparable
where T : U T 必须是另一个泛型参数 U 或其派生类 public void Process(List list) where T : U

5. 组合使用约束

new() 约束与其他约束(如类或接口)同时存在时,new() 必须放在约束列表的最后一位

编写以下代码,演示如何要求 T 既是引用类型,又实现了 IDisposable 接口,并且有无参构造函数:

public class ResourceManager where T : class, IDisposable, new()
{
    public void Execute()
    {
        // 1. 因为有 new() 约束,可以创建实例
        T resource = new T();

        try
        {
            // 执行操作...
        }
        finally
        {
            // 2. 因为有 IDisposable 约束,可以调用 Dispose
            resource.Dispose();
        }
    }
}

如果写错顺序(例如 where T : new(), class),编译器将报错。


6. 约束检查流程逻辑

当编译器遇到带有约束的泛型方法调用时,它会执行严格的检查流程。下图描述了当尝试使用 new T() 以及将 T 赋值为 null 时的编译器逻辑:

graph TD A["泛型类型 T"] --> B{"T 有 new() 约束?"} B -- 否 --> C["禁止: new T()"] B -- 是 --> D["允许: new T()"] D --> E{"T 有 class 约束?"} E -- 是 --> F["允许: T = null"] E -- 否 --> G{"T 是 Nullable?"} G -- 否 --> H["禁止: T = null"] G -- 是 --> F

这个流程解释了为什么你不能在值类型(struct)泛型中直接赋值 null,除非该类型本身是可空类型(如 int?),或者你显式添加了 class 约束。


7. 实战步骤:创建带约束的缓存工具类

动手创建一个实用的例子:一个只能存储引用类型的简单内存缓存。

  1. 定义SimpleCache添加 class 约束确保键是引用类型,new() 约束确保值可以被实例化。
  2. 声明一个私有字典字段用于存储数据。
  3. 实现 GetOrAdd 方法:如果缓存中没有数据,则创建一个新的。
using System;
using System.Collections.Generic;

public class SimpleCache<TKey, TValue> 
    where TKey : class // TKey 必须是引用类型
    where TValue : new() // TValue 必须有无参构造函数
{
    private Dictionary<TKey, TValue> _storage = new Dictionary<TKey, TValue>();

    public TValue GetOrAdd(TKey key)
    {
        // 检查字典中是否存在该键
        if (_storage.TryGetValue(key, out TValue value))
        {
            return value;
        }

        // 如果不存在,利用 new() 约束创建新实例
        value = new TValue();

        // 添加到字典
        _storage[key] = value;

        return value;
    }
}

使用这个工具类:

var cache = new SimpleCache<string, List<int>>();

// 第一次调用,创建新的 List<int> 并存入
var list1 = cache.GetOrAdd("myKey");

list1.Add(100);

// 第二次调用,直接取出已存在的 List<int>
var list2 = cache.GetOrAdd("myKey");

Console.WriteLine(list2.Count); // 输出 1

在这个例子中,如果尝试使用 SimpleCache<int, string>int 不是 class),编译器会直接拦截并报错,从而在编译期就保证了类型安全。

评论 (0)

暂无评论,快来抢沙发吧!

扫一扫,手机查看

扫描上方二维码,在手机上查看本文