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. 限制泛型能接收的类型
例如:
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 必须是委托类型 |
初学阶段最重要的是:
classstructnew()- 基类约束
- 接口约束
后面的 unmanaged、notnull、Enum、Delegate 可以作为进阶内容。
五、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);
常见引用类型包括:
classstring- 数组
- 接口类型
- 委托类型
注意:
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);
常见值类型包括:
intdoubleboolcharDateTimedecimalenum- 自定义
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必须实现IEntityT必须实现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()
表示:
T必须是引用类型。T必须实现IEntity。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()
注意:
class、struct、unmanaged、notnull这类约束通常放在最前面。- 基类约束通常放在接口约束前面。
- 接口约束可以有多个。
new()约束必须放在最后。class和struct不能同时使用。- 基类约束不能和
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);
因为 Dog 是 Animal 的子类,所以 where T : U 成立。
这个约束在初学阶段不常用,但在写通用集合、转换工具时会遇到。
十五、unmanaged 约束
unmanaged 约束表示:
T 必须是非托管类型。
简单理解,非托管类型通常是那些不包含引用类型字段的值类型。
例如:
intdoubleboolchardecimal- 枚举
- 只包含非托管字段的结构体
示例:
static void PrintSize<T>() where T : unmanaged
{
Console.WriteLine(sizeof(T));
}
使用:
PrintSize<int>();
PrintSize<double>();
这类约束常见于:
- 高性能代码
- 底层内存操作
- 与非托管代码交互
初学阶段只需要知道:
unmanaged比struct更严格,它要求值类型内部不能包含引用类型字段。
十六、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);
}
}
因为 T 有 IEntity 约束,所以可以写:
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> 只是保存一个值,它其实不一定需要 class 和 new() 约束。
这样会导致:
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出来。
四十、课堂讲解建议
讲类型参数约束时,可以按下面顺序:
- 先复习泛型:
T可以代表任意类型。 - 用
item.Name报错的例子引出问题。 - 说明编译器不知道
T一定有什么成员。 - 引出
where T : 接口。 - 再讲
class、struct、new()。 - 用仓储例子讲接口约束的实际意义。
- 最后讲组合约束和约束顺序。
学生最容易混淆的点:
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;
}
}
四十二、总结
类型参数约束是泛型中的核心语法,用来限制类型参数必须满足某些条件。
可以记住下面几句话:
- 类型参数约束使用
where关键字。 - 约束可以限制
T必须是引用类型、值类型、某个基类、某个接口等。 - 有了约束,泛型内部才能安全使用约束保证的成员。
where T : class表示 T 必须是引用类型。where T : struct表示 T 必须是非可空值类型。where T : new()表示 T 必须有 public 无参数构造方法。where T : 接口表示 T 必须实现该接口。where T : 基类表示 T 必须继承该基类或就是该基类。new()约束必须写在最后。- 约束不是越多越好,只在泛型内部确实需要某种能力时添加。
- 泛型约束可以让错误在编译期提前暴露,比运行时强制转换更安全。
一句话概括:
类型参数约束就是给泛型的类型参数制定规则,让泛型既保持灵活,又能在编译阶段保证安全。
评论区