C# 中泛型类型的定义、使用和注意事项
一、什么是泛型
泛型,英文叫 Generic。
在 C# 中,泛型可以理解为:
写代码时先不固定具体类型,等真正使用时再指定类型。
例如,我们平时写一个保存整数的类:
class IntBox
{
public int Value { get; set; }
}
这个类只能保存 int。
如果还想保存 string,可能又要写一个:
class StringBox
{
public string Value { get; set; }
}
如果还要保存 double、Student、Product,难道每种类型都写一个类吗?
这时就可以使用泛型:
class Box<T>
{
public T Value { get; set; }
}
使用时再指定具体类型:
Box<int> intBox = new Box<int>();
intBox.Value = 100;
Box<string> stringBox = new Box<string>();
stringBox.Value = "你好";
这里的 T 就是一个类型占位符。
可以这样理解:
T像一个空位置,先占着,等使用时再把真正的类型放进去。
二、为什么需要泛型
泛型主要解决三个问题:
- 代码重复
- 类型不安全
- 装箱和拆箱带来的性能问题
1. 减少重复代码
如果没有泛型,我们可能要写很多类似的类。
class IntBox
{
public int Value { get; set; }
}
class StringBox
{
public string Value { get; set; }
}
class DoubleBox
{
public double Value { get; set; }
}
它们结构几乎一样,只是类型不同。
用泛型后,一个类就够了:
class Box<T>
{
public T Value { get; set; }
}
然后:
Box<int> intBox = new Box<int>();
Box<string> stringBox = new Box<string>();
Box<double> doubleBox = new Box<double>();
这就是泛型的第一个好处:
用一份代码,适配多种类型。
2. 提高类型安全
早期没有泛型集合时,经常使用 ArrayList。
ArrayList list = new ArrayList();
list.Add(100);
list.Add("hello");
list.Add(true);
它什么都能放进去,看起来很灵活,但也很危险。
取出来时容易出错:
int number = (int)list[1]; // 运行时报错,因为 list[1] 是 string
使用泛型集合后:
List<int> numbers = new List<int>();
numbers.Add(100);
numbers.Add(200);
// numbers.Add("hello"); // 编译时报错
错误会提前在编译阶段发现,而不是等程序运行时才爆炸。
这就是泛型的第二个好处:
类型更明确,错误更早发现。
3. 避免不必要的装箱和拆箱
如果把值类型放进 object,会发生装箱。
object obj = 100; // 装箱
int number = (int)obj; // 拆箱
装箱和拆箱会带来额外性能开销。
使用泛型集合:
List<int> numbers = new List<int>();
numbers.Add(100);
int 会以正确的类型保存,不需要频繁装箱和拆箱。
这就是泛型的第三个好处:
类型安全的同时,性能也更好。
三、泛型类型的基本语法
泛型类型常见语法:
class 类名<T>
{
// 在类内部可以使用 T
}
例如:
class Box<T>
{
public T Value { get; set; }
public void SetValue(T value)
{
Value = value;
}
public T GetValue()
{
return Value;
}
}
使用:
Box<int> box = new Box<int>();
box.SetValue(123);
int value = box.GetValue();
这里:
Box<T>是泛型类。T是类型参数。Box<int>是把T替换成int后得到的具体类型。SetValue(T value)中的T会变成int。T GetValue()的返回值也会变成int。
四、T 是什么意思
T 是英文 Type 的缩写,表示类型。
它不是固定关键字,而是一个常见命名习惯。
下面这些都可以:
class Box<T>
{
}
class Box<TValue>
{
}
class Box<TItem>
{
}
甚至这样也能写:
class Box<ABC>
{
}
但不推荐。
常见命名习惯:
| 名称 | 含义 |
|---|---|
T |
通用类型 |
TValue |
值类型或值对象 |
TKey |
键 |
TResult |
返回结果 |
TItem |
集合中的元素 |
TEntity |
实体对象 |
例如字典:
Dictionary<TKey, TValue>
意思是:
一个键值对集合,
TKey表示键的类型,TValue表示值的类型。
五、泛型类
泛型类是最常见的泛型类型。
1. 定义泛型类
class Box<T>
{
public T Value { get; set; }
}
2. 使用泛型类
Box<int> intBox = new Box<int>();
intBox.Value = 100;
Box<string> stringBox = new Box<string>();
stringBox.Value = "C#";
3. 泛型类中的方法
class Box<T>
{
private T value;
public void Set(T value)
{
this.value = value;
}
public T Get()
{
return value;
}
}
使用:
Box<double> box = new Box<double>();
box.Set(3.14);
double result = box.Get();
这个类一旦写成 Box<double>,类中的 T 就都可以理解为 double。
六、泛型结构体
结构体也可以是泛型。
struct Pair<T>
{
public T First { get; set; }
public T Second { get; set; }
}
使用:
Pair<int> numbers = new Pair<int>
{
First = 10,
Second = 20
};
Pair<string> names = new Pair<string>
{
First = "张三",
Second = "李四"
};
如果两个值的类型不同,可以写两个类型参数:
struct Pair<TFirst, TSecond>
{
public TFirst First { get; set; }
public TSecond Second { get; set; }
}
使用:
Pair<string, int> student = new Pair<string, int>
{
First = "小明",
Second = 18
};
七、泛型接口
接口也可以使用泛型。
interface IRepository<T>
{
void Add(T item);
T GetById(int id);
}
这个接口表示:
一个仓储接口,可以添加某种类型的数据,也可以根据 id 查询这种类型的数据。
定义实体:
class Student
{
public int Id { get; set; }
public string Name { get; set; }
}
实现接口:
class StudentRepository : IRepository<Student>
{
public void Add(Student item)
{
Console.WriteLine("添加学生:" + item.Name);
}
public Student GetById(int id)
{
return new Student
{
Id = id,
Name = "小明"
};
}
}
使用:
IRepository<Student> repository = new StudentRepository();
repository.Add(new Student { Id = 1, Name = "小红" });
Student student = repository.GetById(1);
泛型接口的好处是:
接口规则可以复用,但具体操作的数据类型可以变化。
八、泛型方法
泛型不只可以用在类、结构体、接口上,也可以用在方法上。
1. 定义泛型方法
static void Print<T>(T value)
{
Console.WriteLine(value);
}
使用:
Print<int>(100);
Print<string>("你好");
Print<double>(3.14);
很多时候,C# 可以自动推断类型:
Print(100);
Print("你好");
Print(3.14);
2. 有返回值的泛型方法
static T GetDefault<T>()
{
return default(T);
}
使用:
int number = GetDefault<int>(); // 0
string text = GetDefault<string>(); // null
bool flag = GetDefault<bool>(); // false
3. 交换两个变量
static void Swap<T>(ref T a, ref T b)
{
T temp = a;
a = b;
b = temp;
}
使用:
int x = 10;
int y = 20;
Swap(ref x, ref y);
Console.WriteLine(x); // 20
Console.WriteLine(y); // 10
也可以交换字符串:
string first = "A";
string second = "B";
Swap(ref first, ref second);
这就是泛型方法的好处:
方法逻辑相同,类型可以不同。
九、泛型委托
委托也可以是泛型。
C# 中常见的 Action 和 Func 就是泛型委托。
Action<string> print = text =>
{
Console.WriteLine(text);
};
Func<int, int, int> add = (a, b) =>
{
return a + b;
};
也可以自定义泛型委托:
delegate TResult Converter<TInput, TResult>(TInput input);
使用:
Converter<string, int> stringToInt = text =>
{
return int.Parse(text);
};
int number = stringToInt("123");
泛型委托适合表达:
一段可以接收某种类型,并返回另一种类型的行为。
十、多个类型参数
泛型可以有多个类型参数。
例如:
class Result<TData, TError>
{
public TData Data { get; set; }
public TError Error { get; set; }
public bool Success { get; set; }
}
使用:
Result<string, string> result1 = new Result<string, string>
{
Data = "操作成功",
Error = null,
Success = true
};
也可以这样:
Result<Student, string> result2 = new Result<Student, string>
{
Data = new Student { Id = 1, Name = "小明" },
Error = null,
Success = true
};
再看字典:
Dictionary<string, int> scores = new Dictionary<string, int>();
scores["小明"] = 95;
scores["小红"] = 88;
这里:
string是键的类型。int是值的类型。
十一、常见泛型集合
C# 中最常用的泛型类型之一就是集合。
1. List
List<T> 表示某种类型的列表。
List<int> numbers = new List<int>();
numbers.Add(1);
numbers.Add(2);
numbers.Add(3);
对象列表:
List<Student> students = new List<Student>();
students.Add(new Student { Id = 1, Name = "小明" });
students.Add(new Student { Id = 2, Name = "小红" });
2. Dictionary<TKey, TValue>
Dictionary<TKey, TValue> 表示键值对集合。
Dictionary<string, int> ages = new Dictionary<string, int>();
ages["小明"] = 18;
ages["小红"] = 19;
3. Queue
Queue<T> 表示队列,先进先出。
Queue<string> queue = new Queue<string>();
queue.Enqueue("第一个");
queue.Enqueue("第二个");
Console.WriteLine(queue.Dequeue()); // 第一个
4. Stack
Stack<T> 表示栈,后进先出。
Stack<string> stack = new Stack<string>();
stack.Push("第一个");
stack.Push("第二个");
Console.WriteLine(stack.Pop()); // 第二个
5. HashSet
HashSet<T> 表示不重复集合。
HashSet<int> set = new HashSet<int>();
set.Add(1);
set.Add(1);
set.Add(2);
Console.WriteLine(set.Count); // 2
这些集合中的 <T> 都表示集合中元素的类型。
十二、泛型约束
泛型很灵活,但也有一个问题:
如果不知道 T 到底是什么类型,就不能随便调用它的成员。
例如:
static void PrintName<T>(T item)
{
// 错误:编译器不知道 T 一定有 Name 属性
Console.WriteLine(item.Name);
}
因为 T 可能是 int、string、DateTime,它们不一定都有 Name 属性。
这时可以使用泛型约束。
泛型约束用 where 关键字。
十三、where T : class
class Repository<T> where T : class
{
public void Add(T item)
{
Console.WriteLine("添加对象");
}
}
这里:
where T : class
表示:
T 必须是引用类型。
可以使用:
Repository<Student> studentRepository = new Repository<Student>();
不能使用:
// 错误:int 是值类型
Repository<int> intRepository = new Repository<int>();
常见引用类型包括:
- class
- string
- array
- delegate
- interface
十四、where T : struct
class ValueBox<T> where T : struct
{
public T Value { get; set; }
}
这里:
where T : struct
表示:
T 必须是值类型。
可以使用:
ValueBox<int> intBox = new ValueBox<int>();
ValueBox<double> doubleBox = new ValueBox<double>();
ValueBox<DateTime> dateBox = new ValueBox<DateTime>();
不能使用:
// 错误:string 是引用类型
ValueBox<string> stringBox = new ValueBox<string>();
常见值类型包括:
- int
- double
- bool
- DateTime
- enum
- struct
十五、where T : new()
class Factory<T> where T : new()
{
public T Create()
{
return new T();
}
}
这里:
where T : new()
表示:
T 必须有一个无参数构造方法。
如果没有这个约束,下面代码不能编译:
return new T();
因为编译器不知道 T 能不能被 new 出来。
使用:
class Student
{
public string Name { get; set; }
}
Factory<Student> factory = new Factory<Student>();
Student student = factory.Create();
如果类只有带参数构造方法,没有无参数构造方法,就不能满足 new() 约束。
注意:
如果同时有多个约束,
new()通常写在最后。
例如:
class Repository<T> where T : class, new()
{
}
十六、where T : 基类
可以限制 T 必须继承某个基类。
class Animal
{
public string Name { get; set; }
public void Eat()
{
Console.WriteLine(Name + " 正在吃东西");
}
}
class Dog : Animal
{
}
泛型约束:
class AnimalBox<T> where T : Animal
{
public void Feed(T animal)
{
animal.Eat();
}
}
因为写了:
where T : Animal
所以在 AnimalBox<T> 中,编译器知道 T 一定是 Animal 或 Animal 的子类。
因此可以调用:
animal.Eat();
使用:
AnimalBox<Dog> dogBox = new AnimalBox<Dog>();
dogBox.Feed(new Dog { Name = "小狗" });
十七、where T : 接口
可以限制 T 必须实现某个接口。
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,所以可以安全调用 Print()。
这类约束非常常见。
十八、多个约束
一个类型参数可以有多个约束。
class Repository<T> where T : class, IEntity, new()
{
public T Create()
{
T entity = new T();
entity.Id = 1;
return entity;
}
}
假设接口是:
interface IEntity
{
int Id { get; set; }
}
这里:
where T : class, IEntity, new()
表示:
T必须是引用类型。T必须实现IEntity接口。T必须有无参数构造方法。
约束顺序通常是:
where T : class, 接口, new()
或者:
where T : 基类, 接口, new()
十九、default(T)
泛型中经常会看到:
default(T)
它表示:
获取 T 类型的默认值。
不同类型默认值不同:
| 类型 | 默认值 |
|---|---|
int |
0 |
double |
0 |
bool |
false |
char |
'\0' |
| 引用类型 | null |
| 结构体 | 所有字段都是默认值 |
示例:
static T GetDefault<T>()
{
return default(T);
}
使用:
Console.WriteLine(GetDefault<int>()); // 0
Console.WriteLine(GetDefault<bool>()); // False
string text = GetDefault<string>(); // null
新版 C# 中也可以写:
return default;
二十、泛型和 var 的区别
初学者容易把泛型的 T 和 var 混淆。
它们完全不是一回事。
1. var 是局部变量类型推断
var age = 18;
编译器会推断 age 是 int。
但 age 的类型仍然是确定的。
这不等于泛型。
2. T 是类型参数
class Box<T>
{
public T Value { get; set; }
}
T 是一个类型占位符,使用泛型类时再指定。
Box<int> box = new Box<int>();
对比:
| 对比项 | var |
泛型 T |
|---|---|---|
| 本质 | 让编译器推断局部变量类型 | 类型参数 |
| 使用位置 | 方法内部的局部变量 | 类、接口、方法、委托等定义中 |
| 是否创建通用代码 | 否 | 是 |
| 类型何时确定 | 声明变量时 | 使用泛型类型或方法时 |
二十一、泛型和 object 的区别
如果 object 能接收所有类型,为什么还需要泛型?
例如:
class ObjectBox
{
public object Value { get; set; }
}
使用:
ObjectBox box = new ObjectBox();
box.Value = 100;
box.Value = "hello";
看起来很灵活,但问题很多。
1. 取值需要强制转换
int number = (int)box.Value;
如果类型不对,运行时报错。
2. 编译器不能提前发现错误
box.Value = "hello";
int number = (int)box.Value; // 运行时报错
3. 值类型可能发生装箱拆箱
box.Value = 100; // int 装箱成 object
使用泛型:
class Box<T>
{
public T Value { get; set; }
}
Box<int> box = new Box<int>();
box.Value = 100;
int number = box.Value;
不需要强制转换,类型也更安全。
一句话:
object是什么都能装,但不够安全;泛型是使用时指定类型,既灵活又安全。
二十二、泛型类型可以嵌套使用
泛型类型可以互相嵌套。
List<List<int>> matrix = new List<List<int>>();
表示一个二维列表。
Dictionary<string, List<int>> scoreMap = new Dictionary<string, List<int>>();
表示:
键是学生姓名,值是这个学生的多个成绩。
使用:
scoreMap["小明"] = new List<int> { 90, 85, 95 };
scoreMap["小红"] = new List<int> { 88, 92 };
还可以更复杂:
Dictionary<string, List<Student>> classStudents;
表示:
一个班级名对应多个学生。
注意:
泛型嵌套太深会降低可读性,可以考虑定义一个有名字的类来表达业务含义。
二十三、泛型类型推断
泛型方法调用时,很多时候可以省略类型参数。
static void Print<T>(T value)
{
Console.WriteLine(value);
}
完整写法:
Print<int>(100);
Print<string>("hello");
简写:
Print(100);
Print("hello");
编译器会根据传入参数推断 T 的类型。
但是有些情况不能推断。
例如:
static T CreateDefault<T>()
{
return default(T);
}
调用时没有参数,编译器无法从参数推断 T:
// 错误:无法推断 T
var value = CreateDefault();
必须明确指定:
int number = CreateDefault<int>();
string text = CreateDefault<string>();
二十四、泛型静态成员
泛型类型的静态成员是按具体类型分别保存的。
看例子:
class Counter<T>
{
public static int Count;
}
使用:
Counter<int>.Count++;
Counter<int>.Count++;
Counter<string>.Count++;
Console.WriteLine(Counter<int>.Count); // 2
Console.WriteLine(Counter<string>.Count); // 1
为什么?
因为:
Counter<int>
和:
Counter<string>
是两个不同的封闭泛型类型。
所以它们各自有自己的静态字段。
教学时可以这样说:
泛型类换了类型参数,就像生成了不同版本的类;每个版本有自己的静态成员。
二十五、泛型继承
泛型类也可以继承。
1. 子类仍然是泛型
class BaseRepository<T>
{
public void Add(T item)
{
Console.WriteLine("添加数据");
}
}
class Repository<T> : BaseRepository<T>
{
}
使用:
Repository<Student> repository = new Repository<Student>();
2. 子类指定具体类型
class StudentRepository : BaseRepository<Student>
{
}
这时 StudentRepository 就不是泛型类了,它固定操作 Student。
使用:
StudentRepository repository = new StudentRepository();
repository.Add(new Student { Id = 1, Name = "小明" });
3. 子类增加新的类型参数
class CacheRepository<T, TKey> : BaseRepository<T>
{
public TKey Key { get; set; }
}
泛型继承时要注意:
父类需要什么类型参数,子类要么继续传递,要么指定具体类型。
二十六、泛型和可空类型
C# 中的可空值类型也是泛型。
Nullable<int> number = 10;
它通常写成简写形式:
int? number = 10;
两者含义一样。
int? age = null;
int 本来是值类型,不能直接为 null。
但是 int? 可以。
本质上:
int?
就是:
Nullable<int>
这也是泛型在 .NET 中的典型应用。
二十七、协变和逆变的简单了解
这是泛型的进阶内容,初学阶段只需要简单了解。
有些泛型接口支持类型转换,比如:
IEnumerable<string> strings = new List<string>();
IEnumerable<object> objects = strings;
这是因为 IEnumerable<out T> 中的 out 表示协变。
简单理解:
如果只从泛型对象中读取数据,某些情况下子类型集合可以当成父类型集合使用。
还有一种 in,叫逆变,常见于比较器、委托等场景。
例如:
Action<object> printObject = obj => Console.WriteLine(obj);
Action<string> printString = printObject;
初学阶段不要被这些概念吓到。
先掌握:
- 泛型类
- 泛型方法
- 泛型接口
- 泛型约束
- 常用泛型集合
协变和逆变可以以后进阶学习。
二十八、注意事项一:不要滥用泛型
泛型是为了解决“同一套逻辑适配多种类型”的问题。
如果某个类只会处理一种明确类型,就不一定需要泛型。
不推荐:
class StudentService<T>
{
}
如果这个服务永远只处理学生,直接写:
class StudentService
{
}
更清楚。
判断是否需要泛型,可以问自己:
这段代码是否真的要适配多种类型?
如果答案是否定的,就不要为了高级而泛型。
二十九、注意事项二:类型参数命名要清楚
简单泛型可以用 T:
class Box<T>
{
}
多个类型参数时,建议用更清楚的名字:
class Result<TData, TError>
{
}
不推荐:
class Result<T, U>
{
}
虽然能运行,但业务含义不明显。
更推荐:
class Result<TData, TError>
{
public TData Data { get; set; }
public TError Error { get; set; }
}
清晰命名可以降低学习和维护成本。
三十、注意事项三:泛型里不能随便使用 T 的成员
错误示例:
static void PrintName<T>(T item)
{
Console.WriteLine(item.Name);
}
原因:
编译器不知道 T 是否一定有 Name 属性。
解决方式一:使用接口约束。
interface IHasName
{
string Name { get; }
}
static void PrintName<T>(T item) where T : IHasName
{
Console.WriteLine(item.Name);
}
解决方式二:不使用泛型,直接使用明确类型。
static void PrintName(Student student)
{
Console.WriteLine(student.Name);
}
原则:
泛型不是动态类型,编译器只允许你使用它能确定存在的成员。
三十一、注意事项四:new T() 需要 new() 约束
错误写法:
static T Create<T>()
{
return new T();
}
编译器会报错,因为它不知道 T 是否有无参数构造方法。
正确写法:
static T Create<T>() where T : new()
{
return new T();
}
使用:
Student student = Create<Student>();
注意:
new()约束要求类型必须有 public 的无参数构造方法。
三十二、注意事项五:class 和 struct 约束不能乱用
where T : class
表示只允许引用类型。
where T : struct
表示只允许非可空值类型。
不要为了“看起来严谨”随便加约束。
例如:
class Box<T> where T : class
{
public T Value { get; set; }
}
这会导致:
Box<int> box = new Box<int>(); // 错误
如果 Box<T> 本来应该支持 int,这个约束就加错了。
原则:
约束越多,能使用的类型越少;只有确实需要时才加约束。
三十三、注意事项六:泛型集合要指定正确类型
List<int> numbers = new List<int>();
这个列表只能放 int。
不能写:
// 错误
numbers.Add("hello");
如果你想保存学生,就应该写:
List<Student> students = new List<Student>();
不要为了偷懒写:
List<object> list = new List<object>();
除非你确实需要保存多种完全不同的类型。
否则 List<object> 会让类型变得不明确,取值时也更容易出错。
三十四、注意事项七:泛型嵌套不要过深
下面这种类型虽然合法,但可读性很差:
Dictionary<string, List<Dictionary<int, List<Student>>>> data;
看到这种代码时,别人很难马上理解它代表什么业务含义。
可以考虑定义类:
class ClassScoreGroup
{
public string ClassName { get; set; }
public List<StudentScoreGroup> Groups { get; set; }
}
有名字的类比深层泛型嵌套更适合表达复杂业务。
原则:
泛型可以嵌套,但不要让类型声明变成一长串难读的代码。
三十五、注意事项八:泛型不是越通用越好
有些代码看起来很通用,但实际难以理解。
例如:
class Processor<TInput, TOutput, TContext, TOptions, TResult>
{
}
如果每个类型参数都有明确意义,这样可以。
但如果只是为了“万能”,代码会变得很抽象。
更好的做法是:
- 先满足当前真实需求。
- 类型参数有明确责任。
- 不要为了未来可能发生的变化过早设计。
教学时可以提醒:
泛型是工具,不是装饰。能让代码更清楚才用。
三十六、注意事项九:理解编译时类型安全
泛型的很多错误是在编译阶段发现的。
例如:
List<int> numbers = new List<int>();
numbers.Add("hello");
这段代码编译都过不了。
这是一件好事。
因为越早发现错误,修复成本越低。
对比 ArrayList:
ArrayList list = new ArrayList();
list.Add(100);
list.Add("hello");
int number = (int)list[1]; // 运行时才报错
泛型让很多问题提前暴露。
一句话:
泛型不是限制你,而是在编译阶段帮你拦住错误。
三十七、注意事项十:泛型类型参数不是变量
下面写法是错误的理解:
class Box<T>
{
public void Test()
{
// T 不是普通变量,不能这样使用
}
}
T 表示类型,不表示某个具体对象。
正确用法:
private T value;
public T GetValue()
{
return value;
}
也就是说:
T可以用在类型位置。T不能当成一个普通变量直接使用。
例如:
T item;
List<T> list;
Func<T, bool> condition;
这些都是正确的。
三十八、完整示例:泛型缓存类
下面写一个泛型缓存类,用来保存不同类型的数据。
using System;
using System.Collections.Generic;
class Cache<T>
{
private Dictionary<string, T> data = new Dictionary<string, T>();
public void Set(string key, T value)
{
data[key] = value;
}
public T Get(string key)
{
if (data.ContainsKey(key))
{
return data[key];
}
return default(T);
}
}
class Student
{
public int Id { get; set; }
public string Name { get; set; }
}
class Program
{
static void Main()
{
Cache<int> scoreCache = new Cache<int>();
scoreCache.Set("math", 95);
int score = scoreCache.Get("math");
Console.WriteLine(score);
Cache<Student> studentCache = new Cache<Student>();
studentCache.Set("student1", new Student { Id = 1, Name = "小明" });
Student student = studentCache.Get("student1");
Console.WriteLine(student.Name);
}
}
这个例子中:
Cache<int>表示缓存整数。Cache<Student>表示缓存学生对象。- 同一套缓存逻辑可以用于不同类型。
三十九、完整示例:泛型仓储
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 可以管理任意实体类型,但这个类型必须实现 IEntity。
因此在 Repository<T> 内部可以安全访问:
item.Id
四十、课堂讲解建议
讲泛型时,可以按这个顺序:
- 先讲为什么需要泛型:避免为每种类型写重复代码。
- 用
Box<T>讲清楚T是类型占位符。 - 用
List<int>、List<string>讲泛型集合。 - 对比
object,说明泛型的类型安全。 - 讲泛型类、泛型方法、泛型接口。
- 讲
where约束,解释为什么泛型里不能随便调用成员。 - 最后讲注意事项和常见错误。
学生最容易混淆的点:
T是类型,不是变量。List<int>只能放int。Func<T>中的T也是类型参数。where T : class不是让T变成某个类,而是限制T必须是引用类型。new T()必须有where T : new()约束。
可以用这句话帮助理解:
泛型就是“先写一套通用模板,使用时再把具体类型填进去”。
四十一、练习题
练习 1:定义泛型 Box
定义一个 Box<T> 类,可以保存任意类型的值。
参考答案:
class Box<T>
{
public T Value { get; set; }
}
使用:
Box<int> intBox = new Box<int>();
intBox.Value = 100;
Box<string> stringBox = new Box<string>();
stringBox.Value = "hello";
练习 2:定义泛型方法 Print
定义一个泛型方法 Print<T>,可以打印任意类型的值。
参考答案:
static void Print<T>(T value)
{
Console.WriteLine(value);
}
使用:
Print(100);
Print("你好");
Print(3.14);
练习 3:定义泛型交换方法
定义一个泛型方法 Swap<T>,交换两个变量的值。
参考答案:
static void Swap<T>(ref T a, ref T b)
{
T temp = a;
a = b;
b = temp;
}
练习 4:定义带约束的泛型方法
定义一个接口 IPrintable,然后写一个泛型方法,只允许打印实现了 IPrintable 的对象。
参考答案:
interface IPrintable
{
void Print();
}
static void PrintItem<T>(T item) where T : IPrintable
{
item.Print();
}
练习 5:定义泛型仓储
定义一个 Repository<T>,内部使用 List<T> 保存数据,并提供 Add 方法。
参考答案:
class Repository<T>
{
private List<T> items = new List<T>();
public void Add(T item)
{
items.Add(item);
}
}
四十二、总结
泛型是 C# 中非常重要的语法,广泛用于集合、委托、接口、方法、框架和业务代码中。
可以记住下面几句话:
- 泛型就是先不固定类型,使用时再指定类型。
- 泛型可以减少重复代码。
- 泛型可以提高类型安全。
- 泛型可以避免很多不必要的强制转换。
T是类型参数,不是普通变量。- 泛型类、泛型方法、泛型接口、泛型委托都很常见。
where用来给类型参数添加约束。new T()需要where T : new()约束。- 泛型集合如
List<T>、Dictionary<TKey, TValue>是最常见的泛型应用。 - 泛型不是越多越好,只有需要适配多种类型时才使用。
一句话概括:
泛型让我们用一套代码处理多种类型,同时保持类型安全和代码清晰,是 C# 中非常核心的基础能力。