目 录CONTENT

文章目录

CSharp(五十) 泛型约束(where)详解

C# 泛型约束(where)详解


一、为什么需要泛型约束?

1.1 从一个问题出发

看这段代码,你觉得哪里不对?

// 一个比较两个数大小的方法
static T GetMax<T>(T a, T b)
{
    if (a > b)       // ❌ 编译错误!T 类型不确定能不能用 > 比较
        return a;
    return b;
}

问题T 可以是任何类型——intstringPerson……编译器不知道 T 能不能用 > 比较,所以直接报错。

解决方案——给 T 加个约束:"T 必须是可以比较的类型":

// ✅ 加了 where 约束,编译器就知道了
static T GetMax<T>(T a, T b) where T : IComparable<T>
{
    if (a.CompareTo(b) > 0)   // ✅ 现在 T 一定有 CompareTo 方法
        return a;
    return b;
}

一句话where 约束就是告诉编译器"这个 T 至少是谁",限制 T 的类型范围,让你能在方法里安全地使用 T。


1.2 生活类比

泛型 = "随便一个东西"
泛型 + where 约束 = "随便一个东西,但必须满足条件"

比如:
  泛型方法:void Fly<T>(T thing)        → 随便什么东西,让我飞
  加约束:  void Fly<T>(T thing) where T : 鸟  → 随便什么,但必须是鸟
                                           → 现在可以安全地调用 thing.扇翅膀() 了

二、六种约束类型详解

2.1 where T : struct —— 值类型约束

限制 T 必须是值类型(int、double、bool、struct 等)。

// 只接受值类型——因为需要确保 T 不能为 null
static T GetDefault<T>() where T : struct
{
    return default(T);  // 值类型的默认值是 0/false/空结构体
}

// ✅ 可以
int a = GetDefault<int>();         // 0
double b = GetDefault<double>();   // 0.0
bool c = GetDefault<bool>();       // false

// ❌ 不行
// string s = GetDefault<string>();  // 编译错误!string 是引用类型

实用场景——处理可空值类型:

// 安全地把 int? 转成 int,如果 null 就返回默认值
static int SafeUnwrap<T>(T? nullable) where T : struct
{
    return (int)(object)(nullable ?? default(T));
}

2.2 where T : class —— 引用类型约束

限制 T 必须是引用类型(class、interface、delegate、string 等)。

// 只有引用类型才能和 null 比较
static bool IsNull<T>(T item) where T : class
{
    return item == null;  // ✅ T 肯定是引用类型,可以比 null
}

string s1 = "hello";
string s2 = null;
Console.WriteLine(IsNull(s1));  // False
Console.WriteLine(IsNull(s2));  // True

// IsNull(5);  // ❌ 编译错误!int 是值类型

实用场景——确保能安全判空:

static void PrintIfNotNull<T>(T item) where T : class
{
    if (item != null)
        Console.WriteLine(item.ToString());
}

2.3 where T : new() —— 无参构造函数约束

限制 T 必须有无参构造函数,这样你就可以在方法里 new T()

// 工厂方法——创建一个 T 类型的实例
static T Create<T>() where T : new()
{
    return new T();  // ✅ T 一定有无参构造
}

// ✅ 可以
List<int> list = Create<List<int>>();       // 空列表
StringBuilder sb = Create<StringBuilder>(); // 空 StringBuilder

// ❌ 不行
// string s = Create<string>();  // 编译错误!string 没有无参构造
// int n = Create<int>();        // 编译错误!int 不是类(但 struct 有隐式无参构造,
//                                 不过 int 不满足 class 约束,这里是 new() 单独约束)

实用场景——创建指定类型的实例:

// 创建一个装满默认值的数组
static T[] CreateArray<T>(int size) where T : new()
{
    T[] array = new T[size];
    for (int i = 0; i < size; i++)
        array[i] = new T();
    return array;
}

// 使用
StringBuilder[] builders = CreateArray<StringBuilder>(3);
// 创建了 3 个 StringBuilder 对象

new() 约束的特殊规则:

  • 必须是放到所有约束的最后面
  • 不能和 struct 约束一起用(因为 struct 保证有隐式无参构造,但两者不能同时出现)
// ✅ new() 放最后
delegate T Factory<T>() where T : class, new();

// ❌ new() 没放最后
// delegate T Factory<T>() where T : new(), class;  // 编译错误!

// ❌ new() 不能和 struct 一起
// delegate T Factory<T>() where T : struct, new();  // 编译错误!冗余

2.4 where T : 基类名 —— 基类约束

限制 T 必须是指定基类或其派生类。

class Animal
{
    public string Name { get; set; }
    public virtual void Speak() => Console.WriteLine("...");
}

class Dog : Animal
{
    public override void Speak() => Console.WriteLine($"{Name}: 汪汪!");
}

class Cat : Animal
{
    public override void Speak() => Console.WriteLine($"{Name}: 喵喵!");
}

// T 必须是 Animal 或其子类
static void MakeSpeak<T>(T animal) where T : Animal
{
    // ✅ 可以安全访问 Animal 的属性和方法
    animal.Speak();
    Console.WriteLine($"{animal.Name} 叫了一声");
}

// 使用
MakeSpeak(new Dog { Name = "旺财" });  // 旺财: 汪汪!  旺财 叫了一声
MakeSpeak(new Cat { Name = "咪咪" });  // 咪咪: 喵喵!  咪咪 叫了一声

// MakeSpeak("hello");  // ❌ 编译错误!string 不继承 Animal

实用场景——动物园管理系统:

class Zoo
{
    // 只收动物
    static List<T> CreateGroup<T>() where T : Animal, new()
    {
        return new List<T>();
    }
}

var dogGroup = CreateGroup<Dog>();     // ✅
// var stringGroup = CreateGroup<string>();  // ❌ string 不是动物

2.5 where T : 接口名 —— 接口约束

限制 T 必须实现了指定接口。(这是最常用的约束!)

单个接口约束

// T 必须实现 IComparable<T>——这样就能做比较了
static T GetMax<T>(T a, T b) where T : IComparable<T>
{
    return a.CompareTo(b) > 0 ? a : b;
}

Console.WriteLine(GetMax(10, 20));           // 20
Console.WriteLine(GetMax(3.14, 2.71));       // 3.14
Console.WriteLine(GetMax("apple", "banana"));// banana(按字母序)

多个接口约束

interface INameable { string Name { get; } }
interface IScorable { int Score { get; } }

class Student : INameable, IScorable
{
    public string Name { get; set; }
    public int Score { get; set; }
}

class Athlete : INameable, IScorable
{
    public string Name { get; set; }
    public int Score { get; set; }
}

// T 必须同时实现 INameable 和 IScorable
static void PrintRanking<T>(T item) where T : INameable, IScorable
{
    Console.WriteLine($"{item.Name}: {item.Score}分");
}

PrintRanking(new Student { Name = "张三", Score = 92 });  // 张三: 92分
PrintRanking(new Athlete { Name = "李四", Score = 88 });  // 李四: 88分

实用场景——泛型仓库:

interface IEntity
{
    int Id { get; set; }
}

class User : IEntity { public int Id { get; set; } public string Name { get; set; } }
class Order : IEntity { public int Id { get; set; } public double Amount { get; set; } }

// 通用仓库——只允许实现了 IEntity 的类型
class Repository<T> where T : class, IEntity, new()
{
    private List<T> _items = new List<T>();
    private int _nextId = 1;

    public void Add(T item)
    {
        item.Id = _nextId++;  // ✅ 因为 T 实现了 IEntity,所以有 Id
        _items.Add(item);
    }

    public T FindById(int id)
    {
        return _items.FirstOrDefault(i => i.Id == id);
    }

    public List<T> GetAll() => _items;
}

// 使用
var userRepo = new Repository<User>();
userRepo.Add(new User { Name = "张三" });
userRepo.Add(new User { Name = "李四" });
// userRepo.Add(new Order());  // 不报错,但语义不合适(应分开 repo)

2.6 where T : U —— 裸类型约束

限制 T 必须是 U 或 U 的派生类。(比较少用,但特定场景很强大)

// T 必须继承自 U
static T CreateDerived<T, U>() where T : U, new()
{
    return new T();  // 创建 U 的派生类
}

class Animal { }
class Dog : Animal { }
class Cat : Animal { }

// 使用
Animal animal = CreateDerived<Dog, Animal>();   // 返回 Dog
Animal animal2 = CreateDerived<Cat, Animal>();   // 返回 Cat

实用场景——复制并替换类型:

// 把 List<TSource> 复制成 List<TTarget>,要求 TSource 是 TTarget 的子类
static List<TTarget> ConvertList<TSource, TTarget>(List<TSource> source)
    where TSource : TTarget
{
    List<TTarget> result = new List<TTarget>();
    foreach (TSource item in source)
        result.Add(item);
    return result;
}

List<Dog> dogs = new List<Dog> { new Dog(), new Dog() };
List<Animal> animals = ConvertList<Dog, Animal>(dogs);  // Dog 是 Animal 的子类 ✅

七、多个约束的组合

// 多种约束可以同时使用,用逗号分隔
// 注意顺序:类名/struct/class 在最前,接口在中间,new() 在最后

// T 必须是引用类型、实现两个接口、有无参构造
static T Create<T>() where T : class, IComparable<T>, ICloneable, new()
{
    return new T();
}

// 多个类型参数可以各自有约束
static void Process<TInput, TOutput>(TInput input)
    where TInput : class
    where TOutput : struct, IComparable<TOutput>
{
    // TInput 必须是 class
    // TOutput 必须是 struct 且可比较
}

八、完整实战示例——泛型计算器

using System;
using System.Collections.Generic;

// ===== 实体接口 =====
interface IEntity
{
    int Id { get; set; }
    DateTime CreatedAt { get; set; }
}

// ===== 实体基类 =====
class BaseEntity : IEntity
{
    public int Id { get; set; }
    public DateTime CreatedAt { get; set; } = DateTime.Now;
}

// ===== 具体实体 =====
class Student : BaseEntity
{
    public string Name { get; set; }
    public int Score { get; set; }
    public override string ToString() => $"[{Id}] {Name} - {Score}分 (创建于{CreatedAt:yyyy-MM-dd})";
}

class Teacher : BaseEntity
{
    public string Name { get; set; }
    public string Subject { get; set; }
    public override string ToString() => $"[{Id}] {Name} 老师 - {Subject}";
}


// ===== 通用仓库 =====
class Repository<T> where T : class, IEntity, new()
{
    private List<T> _items = new List<T>();
    private int _nextId = 1;

    public T Add(Action<T> configure = null)
    {
        T item = new T();
        item.Id = _nextId++;
        item.CreatedAt = DateTime.Now;
        configure?.Invoke(item);
        _items.Add(item);
        return item;
    }

    public T GetById(int id)
    {
        return _items.FirstOrDefault(i => i.Id == id);
    }

    public List<T> GetAll() => _items;

    public List<T> Where(Func<T, bool> predicate)
    {
        return _items.Where(predicate).ToList();
    }

    public int Count => _items.Count;
}


// ===== 通用报告生成器 =====
class Program
{
    // T 必须继承 BaseEntity → 确保有 Id 和 CreatedAt
    // T 必须有 Name 属性 → 加 INameable 约束
    interface INameable { string Name { get; } }

    // 让 Student 和 Teacher 实现 INameable
    //(实际上 Student 和 Teacher 都已经有 Name 属性,直接加接口即可)

    static void Main()
    {
        // ===== 学生仓库 =====
        var studentRepo = new Repository<Student>();
        studentRepo.Add(s => { s.Name = "张三"; s.Score = 92; });
        studentRepo.Add(s => { s.Name = "李四"; s.Score = 85; });
        studentRepo.Add(s => { s.Name = "王五"; s.Score = 58; });
        studentRepo.Add(s => { s.Name = "赵六"; s.Score = 95; });

        // ===== 教师仓库 =====
        var teacherRepo = new Repository<Teacher>();
        teacherRepo.Add(t => { t.Name = "陈老师"; t.Subject = "数学"; });
        teacherRepo.Add(t => { t.Name = "刘老师"; t.Subject = "语文"; });


        Console.WriteLine("===== 学生列表 =====");
        foreach (var s in studentRepo.GetAll())
            Console.WriteLine(s);

        Console.WriteLine($"\n学生总数: {studentRepo.Count}");
        Console.WriteLine($"平均分: {studentRepo.GetAll().Average(s => s.Score):F1}");


        Console.WriteLine("\n===== 教师列表 =====");
        foreach (var t in teacherRepo.GetAll())
            Console.WriteLine(t);


        // ===== 通用统计方法——用约束确保安全 =====
        Console.WriteLine("\n===== 统计学分情况 =====");
        ShowPassed(studentRepo.GetAll());
    }

    // 约束:T 必须有 Score 属性
    interface IScorable { int Score { get; } }

    static void ShowPassed<T>(List<T> items) where T : IScorable
    {
        int passed = 0;
        int total = items.Count;

        foreach (T item in items)
        {
            if (item.Score >= 60)  // ✅ 因为约束了 IScorable,所以有 Score
                passed++;
        }

        Console.WriteLine($"总人数: {total}");
        Console.WriteLine($"及格: {passed}");
        Console.WriteLine($"不及格: {total - passed}");
        Console.WriteLine($"及格率: {(double)passed / total * 100:F1}%");
    }
}

输出:

===== 学生列表 =====
[1] 张三 - 92分 (创建于2026-06-29)
[2] 李四 - 85分 (创建于2026-06-29)
[3] 王五 - 58分 (创建于2026-06-29)
[4] 赵六 - 95分 (创建于2026-06-29)

学生总数: 4
平均分: 82.5

===== 教师列表 =====
[1] 陈老师 老师 - 数学
[2] 刘老师 老师 - 语文

===== 统计学分情况 =====
总人数: 4
及格: 3
不及格: 1
及格率: 75.0%

九、约束的适用位置

9.1 哪些地方可以用 where 约束?

// ✅ 泛型类
class MyClass<T> where T : class { }

// ✅ 泛型方法
static void MyMethod<T>() where T : struct { }

// ✅ 泛型接口
interface IMyInterface<T> where T : new() { }

// ✅ 泛型委托
delegate T MyDelegate<T>() where T : class, new();

// ✅ 泛型结构体
struct MyStruct<T> where T : struct { }

9.2 不同位置的约束示例

// 类级约束
class DataStore<T> where T : class, new()
{
    private T _data;

    public DataStore()
    {
        _data = new T();  // ✅ 类级约束保证了 new()
    }

    // 方法级约束——在类约束之上再加方法独有的约束
    public void Process<U>(U input) where U : IComparable<U>
    {
        // T 受类约束(class, new())
        // U 受方法约束(IComparable<U>)
        Console.WriteLine(input.CompareTo(input));  // ✅
    }
}

十、常见易错点(避坑指南)

坑1:约束太宽,导致无法调用所需方法

// ❌ 约束太宽
static void Print<T>(T item)
{
    // Console.WriteLine(item.Name);  // 编译错误!T 没有 Name
}

// ✅ 加接口约束
interface INameable { string Name { get; } }
static void Print<T>(T item) where T : INameable
{
    Console.WriteLine(item.Name);  // ✅
}

坑2:new() 必须放最后

// ❌ 顺序错误
// class MyClass<T> where T : new(), class { }  // 编译错误!

// ✅ 正确顺序:class/struct → 接口 → new()
class MyClass<T> where T : class, IComparable<T>, new() { }

坑3:值类型不能用作基类约束

// ❌ 值类型不能作为基类约束
// static void Foo<T>() where T : int { }  // 编译错误!

// ✅ 用 struct 约束代替
static void Foo<T>() where T : struct { }

坑4:密封类(sealed)作基类约束没意义

// ⚠️ 技术上可以,但逻辑上没意义
// 因为 sealed 类不能被继承,约束它跟不写泛型直接写死类型没区别
static void Foo<T>() where T : string { }  // 可以编译,但不如直接写 string

坑5:struct 约束的 T 不能判空

static bool IsNull<T>(T item) where T : struct
{
    // return item == null;  // ❌ 编译错误!值类型不能和 null 比较
    return false;
}

坑6:约束不能形成循环

// ❌ 循环约束,永远无法满足
// class A<T> where T : B { }
// class B : A<B> { }  // 逻辑上循环了

坑7:枚举约束(C# 7.3+)

// C# 7.3 之后,可以约束 T 为枚举类型
static string GetName<T>(T value) where T : Enum
{
    return value.ToString();
}

Console.WriteLine(GetName(DayOfWeek.Monday));  // Monday

坑8:无参构造函数约束不能传参

class Person
{
    public string Name { get; set; }
    // 没有无参构造!
    public Person(string name) { Name = name; }
}

static T Create<T>() where T : new()
{
    return new T();
}

// Create<Person>();  // ❌ 编译错误!Person 没有无参构造函数

十一、约束速查表

约束 写法 含义 能做的事
值类型 where T : struct T 必须是值类型 安全判零、可空处理
引用类型 where T : class T 必须是引用类型 可以 == null 判空
无参构造 where T : new() T 必须有无参构造函数 new T()
基类 where T : Animal T 必须是 Animal 的子类 使用 Animal 的方法/属性
接口 where T : IDisposable T 必须实现某接口 调用接口方法
多接口 where T : IA, IB T 必须实现多个接口 调用多个接口方法
裸类型 where T : U T 必须派生自 U 在泛型参数间建立继承关系
非托管 where T : unmanaged T 必须是非托管类型 底层/指针操作
枚举 where T : Enum T 必须是枚举 枚举操作
组合 where T : class, IA, new() 同时满足多个约束 以上所有

组合约束顺序规则

正确顺序:class/struct/基类 → 接口 → new() → unmanaged

✅ where T : class, IComparable<T>, new()
✅ where T : Animal, IDisposable
✅ where T : struct, IComparable<T>
❌ where T : new(), class           — new() 没放最后
❌ where T : IComparable<T>, class  — class 没放最前
❌ where T : struct, new()          — struct 和 new() 冗余

十二、总结

三句话记住 where 约束

1. where 约束 = 给泛型参数划范围 —— "T 至少是xxx"

2. 有约束才能用方法 —— 约束了 IComparable<T>,
   就能调 CompareTo

3. 最常用的三种:
   where T : class     → 能判空
   where T : new()     → 能 new T()
   where T : 接口      → 能调接口方法

记忆口诀

泛型不加约束,T 是张白纸
加了 where 限制,T 就有了本事

class 能判空,struct 值类型
接口给能力,new() 能创建
基类给保障,顺序不要忘

先类后接口,构造最后放
多个类型参,各自加约束

一句话总结where 约束就是泛型的"补充说明"——告诉编译器"这个 T 虽然不确定具体是什么,但至少是 xxx",这样你就能在泛型代码里安全地使用 T 的成员。最常用套路:where T : class(能判空)、where T : 接口(能调接口方法)、where T : new()(能 new 出来)。

0
博主关闭了当前页面的评论