目 录CONTENT

文章目录

CSharp(四十七) 类型参数约束的定义、使用和注意事项

C# 中类型参数约束的定义、使用和注意事项

一、什么是类型参数约束

类型参数约束,是泛型中的一个重要概念。

它的作用是:

限制泛型类型参数必须满足某些条件。

先看一个普通泛型方法:

static void Print<T>(T value)
{
    Console.WriteLine(value);
}

这里的 T 可以是任意类型:

Print(100);
Print("hello");
Print(DateTime.Now);

这很灵活。

但是灵活也带来一个问题:

因为 T 什么类型都可能是,所以编译器不知道它一定有什么成员。

例如:

static void PrintName<T>(T value)
{
    Console.WriteLine(value.Name); // 错误
}

这段代码会报错。

因为 T 可能是 Student,有 Name 属性;也可能是 int,没有 Name 属性。

编译器不能冒险。

这时就可以使用类型参数约束。

interface IHasName
{
    string Name { get; }
}

static void PrintName<T>(T value) where T : IHasName
{
    Console.WriteLine(value.Name);
}

这里的:

where T : IHasName

就是类型参数约束。

它告诉编译器:

T 必须实现 IHasName 接口,所以 T 一定有 Name 属性。

一句话理解:

类型参数约束就是给泛型的 T 加规则,告诉编译器“这个 T 必须是什么样的类型”。


二、为什么需要类型参数约束

类型参数约束主要解决三个问题:

  1. 限制泛型能接收的类型。
  2. 让泛型内部可以安全使用某些成员。
  3. 让错误在编译阶段提前暴露。

1. 限制泛型能接收的类型

例如:

static void PrintValueType<T>(T value) where T : struct
{
    Console.WriteLine(value);
}

这个方法只允许值类型:

PrintValueType(100);
PrintValueType(3.14);
PrintValueType(true);

不允许引用类型:

// 错误:string 是引用类型
PrintValueType("hello");

2. 让泛型内部安全使用成员

例如:

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

static T FindById<T>(List<T> list, int id) where T : IEntity
{
    foreach (T item in list)
    {
        if (item.Id == id)
        {
            return item;
        }
    }

    return default(T);
}

因为有:

where T : IEntity

所以方法内部可以访问:

item.Id

3. 让错误提前暴露

如果写了约束:

static void Save<T>(T entity) where T : IEntity
{
    Console.WriteLine(entity.Id);
}

那么下面代码会在编译时报错:

Save("hello"); // 错误:string 没有实现 IEntity

这比运行时才出错更安全。


三、类型参数约束的基本语法

类型参数约束使用 where 关键字。

基本格式:

泛型声明 where 类型参数 : 约束
{
}

泛型方法:

static void Method<T>(T value) where T : class
{
}

泛型类:

class Repository<T> where T : class
{
}

泛型接口:

interface IRepository<T> where T : IEntity
{
}

多个类型参数:

class Mapper<TSource, TResult>
    where TSource : class
    where TResult : new()
{
}

注意:

where 写在泛型声明后面,用来说明类型参数必须满足什么条件。


四、常见约束总览

C# 中常见的类型参数约束如下:

约束写法 含义
where T : class T 必须是引用类型
where T : struct T 必须是非可空值类型
where T : new() T 必须有 public 无参数构造方法
where T : BaseClass T 必须继承某个基类
where T : IInterface T 必须实现某个接口
where T : U T 必须是另一个类型参数 U 或其派生类型
where T : unmanaged T 必须是非托管类型
where T : notnull T 不能是可空类型
where T : Enum T 必须是枚举类型
where T : Delegate T 必须是委托类型

初学阶段最重要的是:

  • class
  • struct
  • new()
  • 基类约束
  • 接口约束

后面的 unmanagednotnullEnumDelegate 可以作为进阶内容。


五、class 约束

class 约束表示:

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

语法:

where T : class

示例:

static void PrintObject<T>(T value) where T : class
{
    if (value == null)
    {
        Console.WriteLine("对象是 null");
    }
    else
    {
        Console.WriteLine(value);
    }
}

可以调用:

PrintObject("hello");
PrintObject(new Student());

不能调用:

// 错误:int 是值类型
PrintObject(100);

常见引用类型包括:

  • class
  • string
  • 数组
  • 接口类型
  • 委托类型

注意:

string 虽然看起来像基本类型,但它是引用类型。


六、struct 约束

struct 约束表示:

类型参数必须是非可空值类型。

语法:

where T : struct

示例:

static void PrintValue<T>(T value) where T : struct
{
    Console.WriteLine(value);
}

可以调用:

PrintValue(100);
PrintValue(3.14);
PrintValue(true);
PrintValue(DateTime.Now);

不能调用:

// 错误:string 是引用类型
PrintValue("hello");

也不能用可空值类型:

int? age = 18;

// 错误:Nullable<int> 不满足 struct 约束
PrintValue(age);

常见值类型包括:

  • int
  • double
  • bool
  • char
  • DateTime
  • decimal
  • enum
  • 自定义 struct

注意:

where T : struct 不是要求 T 一定是自己写的结构体,而是要求 T 是值类型。


七、new() 约束

new() 约束表示:

类型参数必须有 public 无参数构造方法。

语法:

where T : new()

为什么需要它?

看下面代码:

static T Create<T>()
{
    return new T(); // 错误
}

这会报错。

因为编译器不知道 T 是否能被 new 出来。

正确写法:

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

使用:

Student student = Create<Student>();

示例类:

class Student
{
    public string Name { get; set; }
}

这个类可以满足 new() 约束,因为它有默认无参数构造方法。

下面这个类不能满足:

class Teacher
{
    public string Name { get; set; }

    public Teacher(string name)
    {
        Name = name;
    }
}

因为它只有带参数构造方法。

如果想满足 new() 约束,需要加一个 public 无参数构造方法:

class Teacher
{
    public string Name { get; set; }

    public Teacher()
    {
    }

    public Teacher(string name)
    {
        Name = name;
    }
}

注意:

new() 约束通常要写在所有约束的最后。

例如:

where T : class, new()

八、基类约束

基类约束表示:

类型参数必须继承某个基类,或者就是这个基类本身。

示例:

class Animal
{
    public string Name { get; set; }

    public void Eat()
    {
        Console.WriteLine(Name + " 正在吃东西");
    }
}
class Dog : Animal
{
}

class Cat : Animal
{
}

泛型方法:

static void Feed<T>(T animal) where T : Animal
{
    animal.Eat();
}

使用:

Dog dog = new Dog { Name = "小狗" };
Cat cat = new Cat { Name = "小猫" };

Feed(dog);
Feed(cat);

因为约束了:

where T : Animal

所以编译器知道 T 一定拥有 Animal 的成员。

因此可以调用:

animal.Eat();

不能使用无关类型:

// 错误:string 没有继承 Animal
Feed("hello");

九、接口约束

接口约束表示:

类型参数必须实现某个接口。

这是实际开发中非常常用的约束。

示例:

interface IPrintable
{
    void Print();
}
class Report : IPrintable
{
    public void Print()
    {
        Console.WriteLine("打印报表");
    }
}

泛型方法:

static void PrintItem<T>(T item) where T : IPrintable
{
    item.Print();
}

使用:

Report report = new Report();
PrintItem(report);

因为 T 被约束为必须实现 IPrintable,所以方法内部可以调用:

item.Print();

再看一个更常见的实体接口:

interface IEntity
{
    int Id { get; set; }
}
static T FindById<T>(List<T> list, int id) where T : IEntity
{
    foreach (T item in list)
    {
        if (item.Id == id)
        {
            return item;
        }
    }

    return default(T);
}

这就是接口约束的典型用法:

只要求对象具备某种能力,不关心它具体是什么类。


十、多个接口约束

一个类型参数可以同时要求实现多个接口。

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

interface IPrintable
{
    void Print();
}

泛型方法:

static void SaveAndPrint<T>(T item)
    where T : IEntity, IPrintable
{
    Console.WriteLine("保存对象,Id = " + item.Id);
    item.Print();
}

这里要求:

  • T 必须实现 IEntity
  • T 必须实现 IPrintable

所以方法内部既可以访问:

item.Id

也可以调用:

item.Print();

十一、组合约束

约束可以组合使用。

例如:

class Repository<T> where T : class, IEntity, new()
{
    public T Create()
    {
        T entity = new T();
        entity.Id = 1;
        return entity;
    }
}

这里:

where T : class, IEntity, new()

表示:

  1. T 必须是引用类型。
  2. T 必须实现 IEntity
  3. T 必须有 public 无参数构造方法。

这样在类内部就可以:

T entity = new T();
entity.Id = 1;

如果没有 new() 约束,不能写 new T()

如果没有 IEntity 约束,不能写 entity.Id

约束不是装饰,它们直接决定泛型内部能做什么。


十二、约束的顺序规则

多个约束不是随便写的,有一定顺序。

常见顺序:

where T : class, 接口1, 接口2, new()

或者:

where T : 基类, 接口1, 接口2, new()

注意:

  1. classstructunmanagednotnull 这类约束通常放在最前面。
  2. 基类约束通常放在接口约束前面。
  3. 接口约束可以有多个。
  4. new() 约束必须放在最后。
  5. classstruct 不能同时使用。
  6. 基类约束不能和 struct 同时使用。

正确:

where T : class, IEntity, new()

错误:

// 错误:new() 必须放最后
where T : new(), class, IEntity

错误:

// 错误:不能既要求引用类型又要求值类型
where T : class, struct

十三、多个类型参数分别约束

如果泛型有多个类型参数,每个类型参数可以有自己的约束。

class Mapper<TSource, TResult>
    where TSource : IEntity
    where TResult : class, new()
{
    public TResult Map(TSource source)
    {
        Console.WriteLine("源对象 Id:" + source.Id);

        TResult result = new TResult();
        return result;
    }
}

这里:

where TSource : IEntity

约束 TSource

where TResult : class, new()

约束 TResult

不要把多个类型参数的约束混在一起理解。

可以这样读:

TSource 必须是什么,TResult 必须是什么,分别写清楚。


十四、类型参数约束:where T : U

这种约束表示:

T 必须是 U,或者是 U 的派生类型。

示例:

static void CopyToList<T, U>(List<T> source, List<U> target)
    where T : U
{
    foreach (T item in source)
    {
        target.Add(item);
    }
}

假设有类:

class Animal
{
}

class Dog : Animal
{
}

使用:

List<Dog> dogs = new List<Dog>();
List<Animal> animals = new List<Animal>();

CopyToList(dogs, animals);

因为 DogAnimal 的子类,所以 where T : U 成立。

这个约束在初学阶段不常用,但在写通用集合、转换工具时会遇到。


十五、unmanaged 约束

unmanaged 约束表示:

T 必须是非托管类型。

简单理解,非托管类型通常是那些不包含引用类型字段的值类型。

例如:

  • int
  • double
  • bool
  • char
  • decimal
  • 枚举
  • 只包含非托管字段的结构体

示例:

static void PrintSize<T>() where T : unmanaged
{
    Console.WriteLine(sizeof(T));
}

使用:

PrintSize<int>();
PrintSize<double>();

这类约束常见于:

  • 高性能代码
  • 底层内存操作
  • 与非托管代码交互

初学阶段只需要知道:

unmanagedstruct 更严格,它要求值类型内部不能包含引用类型字段。


十六、notnull 约束

notnull 约束表示:

T 不能是可空类型。

示例:

class NotNullBox<T> where T : notnull
{
    public T Value { get; set; }
}

它可以用于:

NotNullBox<int> box1 = new NotNullBox<int>();
NotNullBox<string> box2 = new NotNullBox<string>();

在启用可空引用类型检查时,下面写法会有警告或不允许:

NotNullBox<string?> box = new NotNullBox<string?>();

注意:

notnull 主要和 C# 的可空引用类型检查一起使用,它更多是帮助编译器进行空值分析。

初学阶段不需要一开始就深挖它,先理解“限制 T 不能是可空类型”即可。


十七、Enum 约束

Enum 约束表示:

T 必须是枚举类型。

示例:

static void PrintEnumValues<T>() where T : Enum
{
    foreach (T value in Enum.GetValues(typeof(T)))
    {
        Console.WriteLine(value);
    }
}

定义枚举:

enum OrderStatus
{
    Created,
    Paid,
    Shipped,
    Completed
}

使用:

PrintEnumValues<OrderStatus>();

输出:

Created
Paid
Shipped
Completed

如果传入非枚举类型:

// 错误:int 不是枚举类型
PrintEnumValues<int>();

Enum 约束适合写枚举工具方法。


十八、Delegate 约束

Delegate 约束表示:

T 必须是委托类型。

示例:

static void PrintDelegateInfo<T>(T handler) where T : Delegate
{
    Console.WriteLine("委托类型:" + typeof(T).Name);
    Console.WriteLine("方法名称:" + handler.Method.Name);
}

使用:

Action action = () => Console.WriteLine("Hello");

PrintDelegateInfo(action);

Delegate 约束在日常教学中不算高频,但在写事件工具、委托工具时会遇到。


十九、约束用在泛型类上

类型参数约束可以用在泛型类上。

interface IEntity
{
    int Id { get; set; }
}
class Repository<T> where T : IEntity
{
    private List<T> items = new List<T>();

    public void Add(T item)
    {
        items.Add(item);
    }

    public T GetById(int id)
    {
        foreach (T item in items)
        {
            if (item.Id == id)
            {
                return item;
            }
        }

        return default(T);
    }
}

因为 TIEntity 约束,所以可以写:

item.Id

使用:

class Student : IEntity
{
    public int Id { get; set; }
    public string Name { get; set; }
}
Repository<Student> repository = new Repository<Student>();
repository.Add(new Student { Id = 1, Name = "小明" });

Student student = repository.GetById(1);

二十、约束用在泛型方法上

类型参数约束也可以用在泛型方法上。

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

使用:

Student student = Create<Student>();

再看接口约束:

static void PrintId<T>(T item) where T : IEntity
{
    Console.WriteLine(item.Id);
}

使用:

PrintId(new Student { Id = 1, Name = "小明" });

泛型方法的约束只影响这个方法,不影响整个类。


二十一、约束用在泛型接口上

泛型接口也可以加约束。

interface IRepository<T> where T : IEntity
{
    void Add(T item);
    T GetById(int id);
}

实现接口:

class StudentRepository : IRepository<Student>
{
    private List<Student> students = new List<Student>();

    public void Add(Student item)
    {
        students.Add(item);
    }

    public Student GetById(int id)
    {
        foreach (Student student in students)
        {
            if (student.Id == id)
            {
                return student;
            }
        }

        return null;
    }
}

因为接口约束了:

where T : IEntity

所以 IRepository<T> 只能用于实现了 IEntity 的类型。


二十二、约束用在泛型委托上

泛型委托也可以加约束。

delegate void EntityHandler<T>(T entity) where T : IEntity;

使用:

EntityHandler<Student> handler = student =>
{
    Console.WriteLine(student.Id);
};

如果类型没有实现 IEntity,就不能使用:

// 错误
EntityHandler<string> stringHandler;

泛型委托约束在日常业务中不如泛型类和泛型方法常见,但语法是一样的。


二十三、类型参数约束和 object 的区别

有人可能会问:

既然泛型这么麻烦,为什么不直接用 object?

例如:

static void PrintId(object item)
{
    // 不能直接 item.Id
}

如果想访问 Id,只能强制转换:

static void PrintId(object item)
{
    IEntity entity = (IEntity)item;
    Console.WriteLine(entity.Id);
}

这有风险:

PrintId("hello"); // 运行时报错

使用泛型约束:

static void PrintId<T>(T item) where T : IEntity
{
    Console.WriteLine(item.Id);
}

下面代码编译就过不了:

PrintId("hello"); // 编译时报错

对比:

写法 错误发现时间 类型是否清晰
object + 强制转换 运行时 不够清晰
泛型 + 约束 编译时 更清晰

一句话:

object 是把类型信息藏起来,泛型约束是把类型规则说清楚。


二十四、类型参数约束和 as/is 的区别

也有人会这样写:

static void PrintId<T>(T item)
{
    if (item is IEntity entity)
    {
        Console.WriteLine(entity.Id);
    }
}

这种写法可以运行,但含义不同。

它表示:

传什么都可以,如果刚好是 IEntity,就打印 Id。

而约束写法:

static void PrintId<T>(T item) where T : IEntity
{
    Console.WriteLine(item.Id);
}

表示:

只有实现了 IEntity 的类型才能传进来。

对比:

写法 含义
is / as 运行时判断是不是某种类型
where 约束 编译时要求必须满足某种类型规则

如果业务要求“必须是实体”,用约束更清楚。

如果业务允许传任意对象,只是遇到实体时特殊处理,才适合用 is


二十五、类型参数约束和 dynamic 的区别

dynamic 可以绕过编译期检查。

static void PrintName(dynamic item)
{
    Console.WriteLine(item.Name);
}

这段代码编译能过。

但是如果运行时传入没有 Name 的对象:

PrintName(100);

运行时会报错。

泛型约束更安全:

interface IHasName
{
    string Name { get; }
}

static void PrintName<T>(T item) where T : IHasName
{
    Console.WriteLine(item.Name);
}

这样错误会在编译时发现。

一句话:

dynamic 是先放过,运行时再说;泛型约束是编译时就把规则讲清楚。


二十六、注意事项一:约束不是越多越好

约束可以让代码更安全,但不是越多越好。

不推荐:

class Box<T> where T : class, new()
{
    public T Value { get; set; }
}

如果 Box<T> 只是保存一个值,它其实不一定需要 classnew() 约束。

这样会导致:

Box<int> box = new Box<int>(); // 不能用了

也会导致没有无参数构造方法的引用类型不能使用。

原则:

只有泛型内部确实需要某种能力时,才添加对应约束。

例如:

  • new T(),才加 new()
  • 要访问 Id,才加 IEntity
  • 要限制引用类型,才加 class

二十七、注意事项二:new() 约束必须写最后

正确:

where T : class, IEntity, new()

错误:

where T : new(), class, IEntity

原因:

C# 要求 new() 约束放在约束列表的最后。

这是语法规则,记住即可。


二十八、注意事项三:class 和 struct 不能同时使用

错误:

where T : class, struct

原因:

  • class 要求 T 是引用类型。
  • struct 要求 T 是值类型。

一个类型不可能既是引用类型又是值类型。

所以这两个约束互相冲突。


二十九、注意事项四:struct 约束不包括可空值类型

很多初学者会以为:

int?

也是值类型,所以能满足:

where T : struct

但实际上不行。

示例:

static void Print<T>(T value) where T : struct
{
    Console.WriteLine(value);
}
int? number = 10;

// 错误
Print(number);

因为 struct 约束要求的是非可空值类型。


三十、注意事项五:有约束也不代表能调用所有成员

例如:

static void Test<T>(T value) where T : class
{
    // value.Name 仍然不一定能用
}

class 只说明 T 是引用类型。

它没有说明 T 一定有 Name 属性。

如果要访问 Name,应该使用接口或基类约束:

interface IHasName
{
    string Name { get; }
}
static void PrintName<T>(T value) where T : IHasName
{
    Console.WriteLine(value.Name);
}

记住:

约束只能保证它声明过的能力,没声明的能力仍然不能随便用。


三十一、注意事项六:基类约束只能有一个

C# 中一个类只能继承一个基类。

所以泛型约束中也只能有一个基类约束。

正确:

where T : Animal, IPrintable, new()

错误:

where T : Animal, Person

如果你需要多个能力,应该使用接口:

where T : IWalkable, IEatable

三十二、注意事项七:接口约束可以有多个

与基类不同,接口可以实现多个。

where T : IEntity, IPrintable, IValidatable

这表示 T 必须同时实现这三个接口。

这种写法适合多个能力组合。

但是也不要堆太多接口。

如果约束越来越长,可能说明类型设计需要重新整理。


三十三、注意事项八:约束会影响调用者

一旦给泛型加了约束,调用者就必须满足约束。

例如:

static void Save<T>(T item) where T : IEntity
{
    Console.WriteLine(item.Id);
}

调用者只能传实现了 IEntity 的类型:

Save(new Student());

不能传:

Save("hello");

所以设计约束时要考虑清楚:

这个泛型真的应该限制这么窄吗?

约束太少,内部不能安全使用成员。

约束太多,外部使用会变困难。


三十四、注意事项九:约束不能表达所有业务规则

泛型约束只能表达类型层面的规则。

例如它能表达:

  • 必须是引用类型
  • 必须是值类型
  • 必须实现某个接口
  • 必须有无参数构造方法

但它不能直接表达:

  • 年龄必须大于 18
  • 字符串不能为空
  • 价格必须大于 0
  • 集合数量必须小于 100

这些属于业务规则,需要在方法体中判断:

static void Register<T>(T user) where T : IUser
{
    if (user.Age < 18)
    {
        throw new ArgumentException("用户年龄必须大于等于 18");
    }
}

不要把泛型约束当成万能校验器。


三十五、注意事项十:不要为了访问属性就滥用反射

有些人为了访问 Name,可能会用反射:

static void PrintName<T>(T item)
{
    var property = typeof(T).GetProperty("Name");
    var value = property.GetValue(item);
    Console.WriteLine(value);
}

这虽然能做,但不适合普通业务代码和教学入门。

问题是:

  • 编译器无法检查属性名是否正确。
  • 性能更差。
  • 代码更复杂。
  • 出错往往发生在运行时。

更推荐接口约束:

interface IHasName
{
    string Name { get; }
}
static void PrintName<T>(T item) where T : IHasName
{
    Console.WriteLine(item.Name);
}

原则:

能用接口约束表达的能力,就不要优先用反射硬取。


三十六、完整示例:带约束的泛型仓储

下面写一个泛型仓储类,用来保存和查找实体。

using System;
using System.Collections.Generic;

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

class Student : IEntity
{
    public int Id { get; set; }
    public string Name { get; set; }
}

class Product : IEntity
{
    public int Id { get; set; }
    public string Title { get; set; }
}

class Repository<T> where T : IEntity
{
    private List<T> items = new List<T>();

    public void Add(T item)
    {
        items.Add(item);
    }

    public T GetById(int id)
    {
        foreach (T item in items)
        {
            if (item.Id == id)
            {
                return item;
            }
        }

        return default(T);
    }
}

class Program
{
    static void Main()
    {
        Repository<Student> studentRepository = new Repository<Student>();

        studentRepository.Add(new Student { Id = 1, Name = "小明" });

        Student student = studentRepository.GetById(1);
        Console.WriteLine(student.Name);

        Repository<Product> productRepository = new Repository<Product>();

        productRepository.Add(new Product { Id = 10, Title = "键盘" });

        Product product = productRepository.GetById(10);
        Console.WriteLine(product.Title);
    }
}

重点:

class Repository<T> where T : IEntity

它表示:

Repository 可以管理很多种类型,但这些类型都必须是实体,也就是必须有 Id。

所以仓储内部可以安全使用:

item.Id

三十七、完整示例:class + new() 约束

下面写一个对象工厂。

using System;

class Factory
{
    public static T Create<T>() where T : class, new()
    {
        return new T();
    }
}

class Student
{
    public string Name { get; set; }
}

class Program
{
    static void Main()
    {
        Student student = Factory.Create<Student>();

        student.Name = "小明";

        Console.WriteLine(student.Name);
    }
}

这里:

where T : class, new()

表示:

  • T 必须是引用类型。
  • T 必须有 public 无参数构造方法。

因此方法内部可以写:

return new T();

三十八、完整示例:接口约束和业务能力

下面写一个通知方法,只允许通知“能接收消息”的对象。

using System;

interface IReceiver
{
    void Receive(string message);
}

class User : IReceiver
{
    public string Name { get; set; }

    public void Receive(string message)
    {
        Console.WriteLine(Name + " 收到消息:" + message);
    }
}

class Robot : IReceiver
{
    public void Receive(string message)
    {
        Console.WriteLine("机器人收到消息:" + message);
    }
}

class Program
{
    static void Main()
    {
        User user = new User { Name = "小明" };
        Robot robot = new Robot();

        SendMessage(user, "你好");
        SendMessage(robot, "开始工作");
    }

    static void SendMessage<T>(T receiver, string message)
        where T : IReceiver
    {
        receiver.Receive(message);
    }
}

这里的约束:

where T : IReceiver

表示:

只要这个类型能接收消息,就可以传进来。

这体现了接口约束的优势:

不关心具体类型,只关心它具备什么能力。


三十九、完整示例:多个类型参数约束

using System;

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

class Student : IEntity
{
    public int Id { get; set; }
    public string Name { get; set; }
}

class StudentDto
{
    public int Id { get; set; }
}

class Mapper
{
    public static TResult Map<TSource, TResult>(TSource source)
        where TSource : IEntity
        where TResult : class, new()
    {
        TResult result = new TResult();

        Console.WriteLine("正在转换对象,源对象 Id:" + source.Id);

        return result;
    }
}

class Program
{
    static void Main()
    {
        Student student = new Student
        {
            Id = 1,
            Name = "小明"
        };

        StudentDto dto = Mapper.Map<Student, StudentDto>(student);

        Console.WriteLine(dto != null);
    }
}

这里有两个约束:

where TSource : IEntity
where TResult : class, new()

含义:

  • TSource 必须有 Id
  • TResult 必须是引用类型,并且能被 new 出来。

四十、课堂讲解建议

讲类型参数约束时,可以按下面顺序:

  1. 先复习泛型:T 可以代表任意类型。
  2. item.Name 报错的例子引出问题。
  3. 说明编译器不知道 T 一定有什么成员。
  4. 引出 where T : 接口
  5. 再讲 classstructnew()
  6. 用仓储例子讲接口约束的实际意义。
  7. 最后讲组合约束和约束顺序。

学生最容易混淆的点:

  • where T : class 只表示引用类型,不表示某个具体类。
  • where T : struct 只允许非可空值类型。
  • where T : new() 只表示有 public 无参数构造方法。
  • new() 必须写在约束最后。
  • 接口约束是为了让泛型内部可以安全调用接口成员。
  • 约束不是越多越好,只在真正需要时添加。

可以用这句话帮助理解:

泛型约束就像给 T 设置入场条件,只有符合条件的类型才能进来;进来以后,编译器才允许你使用这些条件保证的能力。


四十一、练习题

练习 1:class 约束

写一个泛型方法 PrintReference<T>,只允许引用类型,并打印对象。

参考答案:

static void PrintReference<T>(T value) where T : class
{
    Console.WriteLine(value);
}

练习 2:struct 约束

写一个泛型方法 PrintValue<T>,只允许值类型。

参考答案:

static void PrintValue<T>(T value) where T : struct
{
    Console.WriteLine(value);
}

练习 3:new() 约束

写一个泛型方法 Create<T>,创建一个 T 类型对象。

参考答案:

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

练习 4:接口约束

定义一个接口 IPrintable,然后写一个泛型方法,只允许打印实现了该接口的对象。

参考答案:

interface IPrintable
{
    void Print();
}
static void PrintItem<T>(T item) where T : IPrintable
{
    item.Print();
}

练习 5:实体查找

定义一个 IEntity 接口,要求有 Id 属性。写一个泛型方法,根据 Id 查找列表中的对象。

参考答案:

interface IEntity
{
    int Id { get; set; }
}
static T FindById<T>(List<T> list, int id) where T : IEntity
{
    foreach (T item in list)
    {
        if (item.Id == id)
        {
            return item;
        }
    }

    return default(T);
}

练习 6:组合约束

写一个泛型类 Repository<T>,要求 T 是引用类型,实现 IEntity,并且有无参数构造方法。

参考答案:

class Repository<T> where T : class, IEntity, new()
{
    public T Create()
    {
        T entity = new T();
        entity.Id = 1;
        return entity;
    }
}

四十二、总结

类型参数约束是泛型中的核心语法,用来限制类型参数必须满足某些条件。

可以记住下面几句话:

  1. 类型参数约束使用 where 关键字。
  2. 约束可以限制 T 必须是引用类型、值类型、某个基类、某个接口等。
  3. 有了约束,泛型内部才能安全使用约束保证的成员。
  4. where T : class 表示 T 必须是引用类型。
  5. where T : struct 表示 T 必须是非可空值类型。
  6. where T : new() 表示 T 必须有 public 无参数构造方法。
  7. where T : 接口 表示 T 必须实现该接口。
  8. where T : 基类 表示 T 必须继承该基类或就是该基类。
  9. new() 约束必须写在最后。
  10. 约束不是越多越好,只在泛型内部确实需要某种能力时添加。
  11. 泛型约束可以让错误在编译期提前暴露,比运行时强制转换更安全。

一句话概括:

类型参数约束就是给泛型的类型参数制定规则,让泛型既保持灵活,又能在编译阶段保证安全。

0

评论区