CSharp泛型学习笔记
一、为什么需要泛型
先看一个很常见的场景:有几段代码,逻辑完全一样,只是传入的参数类型或返回类型不同。
public string GetClassType(Teacher teacher)
{
return teacher.GetType().Name;
}
public string GetClassType(Student student)
{
return student.GetType().Name;
}
public string GetClassType(Worker worker)
{
return worker.GetType().Name;
}
public int GetInt(string str)
{
return int.Parse(str);
}
public float GetFloat(string str)
{
return float.Parse(str);
}
上面的代码非常啰嗦:为了处理 Teacher、Student、Worker 三种类型,写了三个几乎一样的方法;为了转 int 和 float,又写了两个方法。
我们当然希望:只写一份逻辑,就能适用于各种类型。泛型就是为此而生的。
二、先用 object 行不行
有人会想:既然 object 是所有类的基类,任何类型都能当 object 传进去,那用一个 object 参数不就通用了吗?
public string GetClassType(object obj)
{
return obj.GetType().Name;
}
这个思路能用,但有两个明显的坑:
1. 性能问题:装箱与拆箱
值类型(如 int)传给 object 参数时,会发生装箱(boxing):int -> object;取出来转回实际类型时,又发生拆箱(unboxing):object -> int。装箱拆箱会额外分配内存、有性能损耗。
2. 类型安全问题:编译能过,运行可能崩
因为参数是 object,编译器无法在编译期检查类型,很多错误只能等到运行时才暴露,直接抛异常。
泛型正好能同时解决这两个问题:编译期就确定类型(类型安全),且不会装箱拆箱(性能好)。
三、泛型的定义
泛型(generic) 是 C# 2.0 推出的语法,专门用于处理"多段代码在不同数据类型上执行相同指令"的情况。
核心思想:用一个占位符 T 代替实际数据类型,等到真正调用时,才指定具体类型。
按定义的位置不同,可以定义:
| 类型 | 写法示例 |
|---|---|
| 泛型方法 | GetUserName<T>() |
| 泛型类 | BaseModel<T> |
| 泛型接口 | IData<T> |
| 泛型委托 | CustHandler<int, string> |
// 泛型方法
public class CommonClass
{
public string GetClassType<T>(T t)
{
return t.GetType().Name;
}
}
// 泛型类
public class MyList<T>
{
List<T> list = new List<T>();
public void Add(T t)
{
list.Add(t);
}
}
泛型思想一句话总结:延迟一切可以延迟的 —— 把类或方法中"数据类型"的确定,延迟到调用时才指定。
四、泛型的特性
- 最大限度重用代码、保护类型安全、提高性能;
- 最常见的用途是创建集合类;
- .NET 类库在
System.Collections.Generic命名空间下提供了List<T>、Dictionary<TKey, TValue>等泛型集合,建议用它代替旧的System.Collections集合; - 可以自定义泛型接口、泛型类、泛型方法、泛型事件、泛型委托;
- 可以对泛型加约束(
where)来限定类型范围; - 泛型类型参数的信息可以在运行时通过反射获取。
五、泛型方法
泛型方法:在声明中包含类型参数的方法,用一个方法满足不同类型需求。
写法就是方法名后面加尖括号 <T>:
public string GetClassType<T>(T t)
{
return t.GetType().Name;
}
int intVal = 5;
GetClassType<int>(intVal); // 显式指定类型参数
其中 T 就是类型参数:声明时不写死类型,用 T 占位;调用时才用实际类型替换。
类型推断
如果方法的参数类型就是类型参数,调用时可以省略类型参数,编译器会根据传入参数自动推断:
int intVal = 5;
GetClassType(intVal); // 省略 <int>,编译器自动推断为 int
类型推断对静态方法和实例方法都适用。但要注意:编译器只能根据传入的参数推断,无法仅根据约束或返回值推断。
所以下面这两种情况不能省略类型参数,必须显式指定:
// 1. 没有参数,无法推断
public string GetClassType<T>()
{
return typeof(T).Name;
}
// 2. 有参数,但参数类型不是类型参数 T,无法推断
public T ToType<T>(string str)
{
Type type = typeof(T);
return (T)Convert.ChangeType(str, type);
}
// 调用时必须显式写类型
string name = GetClassType<Student>();
int num = ToType<int>("3.9");
六、泛型类
泛型类用于封装"不特定于某个具体类型"的操作,最常见用途是列表、哈希表、堆栈、队列、树等集合。因为这些集合无论存什么类型,"添加、删除、计数"的操作方式都基本一样。
先看一个只针对 IncomeInfo 的具体类:
public class IncomeList
{
List<IncomeInfo> list = new List<IncomeInfo>();
public void Add(IncomeInfo income)
{
list.Add(income);
}
public void Delete(IncomeInfo income)
{
list.Remove(income);
}
public int GetCount()
{
return list.Count;
}
}
如果还要处理 ItemInfo、ExpendInfo 等类型,就要重复写一堆类似的类。改成泛型类,一份搞定:
public class DataList<T>
{
List<T> list = new List<T>();
public void Add(T t)
{
list.Add(t);
}
public void Delete(T t)
{
list.Remove(t);
}
public int GetCount()
{
return list.Count;
}
}
// 使用
DataList<ItemInfo> itemList = new DataList<ItemInfo>();
ItemInfo item1 = new ItemInfo() { ItemId = 1, ItemName = "收入" };
ItemInfo item2 = new ItemInfo() { ItemId = 2, ItemName = "支出" };
itemList.Add(item1);
itemList.Add(item2);
itemList.Delete(item1);
int itemCount = itemList.GetCount();
自定义泛型类的三个注意点
- 类型参数的数量:可参数化的类型越多越灵活、可重用性越高,但不能过度(泛化太多反而难读、难维护);
- 约束:要给类型参数加什么约束,以限定类型范围(有些类型不适合当前类的方法);
- 继承拆分:可以把泛型行为拆分成基类和子类。
七、常见集合类型
7.1 ArrayList(不建议使用)
ArrayList 是非泛型的动态数组,能动态增删元素。但它的 Add 方法接受 object,什么类型都能加。
问题:值类型存取会发生装箱拆箱,有性能损耗;且它不是类型安全的。
ArrayList arrList = new ArrayList();
arrList.Add(12);
arrList.Add(35.6);
arrList.Add(new IncomeInfo() { IncomeId = 1, IncomeName = "领工资" });
arrList.RemoveAt(1);
int iVal = Convert.ToInt32(arrList[0]); // 拆箱
结论:不建议用 ArrayList,建议用 List<T>。
7.2 List<T>(首选)
List<T> 是泛型集合,强类型、类型安全,可动态增删元素,适合存储数目不固定的一组信息。是存储列表数据的首选。
常用操作:
List<int> ids = new List<int>();
ids.Add(101); // 添加到末尾
ids.Add(102);
ids.AddRange(new int[] { 103, 104, 105 }); // 一次添加多个
ids.Remove(103); // 移除指定元素
ids.RemoveAt(2); // 移除第 3 个元素
bool bl = ids.Contains(105); // 判断是否存在
int newId = ids.Find(id => id == 103); // 查找元素
int index = ids.IndexOf(104); // 获取索引
List<int> newList = ids.Where(id => id > 103).ToList(); // 条件筛选
ids.Sort(); // 排序
ids.Reverse(); // 翻转
什么时候用 List? 暂存编号集合、对象列表、选中的数据等,是存储列表数据的首选。
7.3 Dictionary<TKey, TValue>
表示键值对集合,从一组"键"映射到一组"值"。也是强类型、类型安全的。通过键检索值速度非常快。
要点:
- 键必须唯一,值可以重复;
- 键和值都是类型参数,可以是任意类型(
string、int、自定义类型等)。
Dictionary<int, string> dicList = new Dictionary<int, string>();
dicList.Add(101, "收入");
dicList.Add(102, "收入");
dicList.Add(103, "支出");
dicList.Add(104, "收入");
dicList.Add(105, "支出");
List<int> itemIds = new List<int>();
// 方式一:遍历每个键值对
foreach (KeyValuePair<int, string> kv in dicList)
{
if (kv.Value == "收入")
{
itemIds.Add(kv.Key);
}
}
// 方式二:遍历每个键
foreach (int key in dicList.Keys)
{
if (dicList[key] == "收入")
{
itemIds.Add(key);
}
}
// 判断键是否存在,再取值
if (dicList.ContainsKey(104))
{
string val = dicList[104];
}
什么时候用 Dictionary? 存储一系列有对应关系的信息,比如"编号 → 名称"、"ID → 对象"等,需要根据键快速查值。
八、泛型约束(where)
泛型定义中可以用 where 子句,给类型参数加约束,限定类型的范围。
约束类型包括:接口约束、基类约束、引用类型约束、值类型约束、无参构造函数约束。
约束规则(必记)
- 可以同时使用多个约束;
- 值类型约束与引用类型约束不能同时使用;
- 引用类型约束不能与基类约束同时使用;
- 如果有基类约束和其他约束,基类约束必须写在最前面;
- 多个约束里如果有
new(),new()必须是最后一个。
8.1 值类型约束(where T : struct)
限定实际类型不能是引用类型(即必须是值类型)。
public T ToType<T>(string str) where T : struct
{
return (T)Convert.ChangeType(str, typeof(T));
}
float f1 = ToType<int>("3.9"); // 正确,int 是值类型
string s = ToType<string>("aaaa"); // 错误,string 是引用类型
8.2 引用类型约束(where T : class)
限定实际类型必须是引用类型。
public class Record<T> where T : class
{
List<T> list = new List<T>();
public void AddRecord(T t)
{
list.Add(t);
}
}
Record<IncomeInfo> income = new Record<IncomeInfo>(); // 正确
Record<double> record1 = new Record<double>(); // 错误,double 是值类型
8.3 基类约束(where T : BaseModel)
限定实际类型必须是某个基类或其子类。
public class BaseModel
{
public int Id { get; set; }
public string Name { get; set; }
public decimal Amount { get; set; }
}
public class IncomeInfo : BaseModel { }
public void GetRecordInfo<T>(T t) where T : BaseModel
{
Console.WriteLine("编号:" + t.Id + ",名称:" + t.Name + ",金额:" + t.Amount);
}
IncomeInfo incomeInfo = new IncomeInfo();
incomeInfo.Id = 1;
incomeInfo.Name = "工资收入";
incomeInfo.Amount = 2000;
GetRecordInfo(incomeInfo); // 正确,IncomeInfo 是 BaseModel 的子类
有了基类约束,编译器就能确定 T 一定有 Id、Name、Amount 这些成员,于是可以在方法里直接访问它们。
8.4 接口约束(where T : IRecord)
限定实际类型必须实现了某个接口。
public interface IRecord
{
decimal CalTotalAmount(decimal amount);
}
public class ExpendInfo : BaseModel, IRecord
{
public decimal Price { get; set; }
public decimal CalTotalAmount(decimal amount)
{
return GenericMethod.totalAmount - amount;
}
}
// 接口约束 + 基类约束同时使用
public decimal GetTotalAmount<T>(T t) where T : BaseModel, IRecord
{
return t.CalTotalAmount(t.Amount);
}
ExpendInfo exInfo = new ExpendInfo();
exInfo.Id = 2;
exInfo.Name = "借钱给朋友";
exInfo.Amount = 1000;
decimal totalAmount = GetTotalAmount(exInfo);
有了接口约束,编译器就能确定 T 实现了 IRecord,可以放心调用 CalTotalAmount 方法。
8.5 无参构造函数约束(where T : new())
限定实际类型必须有无参构造函数,这样就能在泛型里用 new T() 创建对象。
public T CreateObj<T>() where T : new()
{
T t = new T();
return t;
}
IncomeInfo income = CreateObj<IncomeInfo>();
九、一个综合示例
把泛型方法、泛型类、约束串起来,写一个能跑通的小程序:
using System;
using System.Collections.Generic;
class Program
{
// 泛型类:封装通用的增删查
public class DataList<T>
{
private List<T> list = new List<T>();
public void Add(T t) => list.Add(t);
public void Remove(T t) => list.Remove(t);
public int Count => list.Count;
public List<T> GetAll() => list;
}
static void Main()
{
DataList<string> names = new DataList<string>();
names.Add("张三");
names.Add("李四");
names.Remove("张三");
Console.WriteLine("剩余数量:" + names.Count); // 输出:1
// 泛型方法
int num = ParseValue<int>("3.9");
Console.WriteLine("解析结果:" + num); // 输出:3
}
// 值类型约束的泛型方法
static T ParseValue<T>(string str) where T : struct
{
return (T)Convert.ChangeType(str, typeof(T));
}
}
十、面试题
1. 什么是泛型?它解决了什么问题?
答:泛型是用一个类型参数(如 T)代替具体类型,延迟到调用时才确定实际类型的机制。它解决的是"多段代码逻辑相同、只是类型不同"的重复编写问题,能最大限度重用代码。
2. 泛型相比用 object 参数有什么优势?
答:
- 类型安全:编译期就能检查类型,避免运行时才报错;
- 性能更好:避免值类型的装箱拆箱;
- 代码更简洁:调用方不用手动做类型转换(object 需要强转)。
3. 泛型约束有哪些?各自的语法和作用?
答:
| 约束 | 语法 | 作用 |
|---|---|---|
| 值类型约束 | where T : struct |
T 必须是值类型 |
| 引用类型约束 | where T : class |
T 必须是引用类型 |
| 基类约束 | where T : Base |
T 必须是某基类或其子类 |
| 接口约束 | where T : IRecord |
T 必须实现某接口 |
| 无参构造函数约束 | where T : new() |
T 必须有无参构造函数,可用 new T() |
4. 泛型约束有哪些规则?
答:
- 可同时使用多个约束;
- 值类型约束和引用类型约束不能同时用;
- 引用类型约束不能与基类约束同时用;
- 有基类约束时,基类约束必须写在最前面;
- 多个约束里若有
new(),它必须是最后一个。
5. 泛型方法的类型推断在什么情况下不能用?
答:编译器只能根据传入参数推断类型参数,无法仅根据约束或返回值推断。所以下面两种情况必须显式指定类型参数:
- 方法没有参数(如
GetClassType<T>()); - 有参数但参数类型不是类型参数 T(如
ToType<T>(string str))。
6. List<T> 和 ArrayList 有什么区别?
答:
ArrayList是非泛型集合,元素是object,不类型安全,值类型会装箱拆箱,有性能损耗;List<T>是泛型集合,强类型、类型安全、无装箱拆箱,性能更好。
所以现在推荐用 List<T>,不建议用 ArrayList。
7. Dictionary 的键有什么要求?键重复会怎样?
答:Dictionary 的键必须唯一(值可以重复)。如果 Add 一个已存在的键,会抛出 ArgumentException 异常。因此添加前通常先用 ContainsKey 判断,或使用索引器 dict[key] = value 赋值(键存在则覆盖值,不存在则新增)。
8. 值类型约束和引用类型约束能同时用吗?
答:不能。where T : struct 和 where T : class 互斥,同时使用会导致编译错误。
9. 泛型方法和泛型类里的 T 能继承或实现接口吗?如何让编译器知道 T 有某个成员?
答:T 本身不能直接继承或实现,但可以通过约束让编译器"知道"T 有哪些成员。比如加基类约束 where T : BaseModel,编译器就确定 T 有 BaseModel 的成员;加接口约束 where T : IRecord,就能调用 IRecord 的方法。没有约束时,T 只能当 object 用,只能访问 object 的成员。
10. 泛型的类型参数信息在运行时能拿到吗?怎么拿?
答:能。C# 的泛型在运行时保留类型信息(不像 Java 的泛型擦除),可以通过反射获取。比如在泛型方法里用 typeof(T) 拿到 T 对应的 Type 对象。
11. 泛型类可以被继承吗?继承时类型参数如何处理?
答:可以。继承时有两种方式:
- 继承时指定具体类型:
class MyList : List<int>,此时子类不再是泛型类; - 继承时继续传递类型参数:
class MyList<T> : List<T>,子类仍是泛型类。
12. 为什么说"延迟一切可以延迟的"是泛型的核心思想?
答:泛型不要求编写时就知道具体类型,而是用 T 占位,把"确定类型"这件事推迟到真正使用时。这样一份泛型代码就能服务于任意类型,达到代码复用、类型安全、高性能的目的。

浙公网安备 33010602011771号