C# 泛型(Generic)(手把手讲解)

更新时间:

💡一则或许对你有用的小广告

欢迎加入小哈的星球 ,你将获得:专属的项目实战 / 1v1 提问 / Java 学习路线 / 学习打卡 / 每月赠书 / 社群讨论

截止目前, 星球 内专栏累计输出 90w+ 字,讲解图 3441+ 张,还在持续爆肝中.. 后续还会上新更多项目,目标是将 Java 领域典型的项目都整一波,如秒杀系统, 在线商城, IM 即时通讯,权限管理,Spring Cloud Alibaba 微服务等等,已有 3100+ 小伙伴加入学习 ,欢迎点击围观

前言

在 C# 开发中,泛型(Generic)是一个既基础又强大的特性。它允许开发者通过参数化类型的方式,编写更灵活、高效且类型安全的代码。对于编程初学者而言,泛型可能是一个抽象的概念;而对中级开发者来说,深入理解泛型的底层逻辑和应用场景则能显著提升编码效率。本文将从泛型的起源、核心概念、实际案例到高级技巧,逐步展开讲解,帮助读者系统性掌握这一重要工具。


一、泛型是什么?为什么需要它?

1.1 泛型的定义

泛型是 C# 2.0 引入的一项语言特性,其核心思想是“类型参数化”。通过在类、接口、方法等类型声明时添加类型参数(如 <T>),开发者可以在不指定具体类型的情况下,编写通用的代码逻辑。例如,一个泛型列表 List<T> 可以存储任意类型的元素(如 intstring 或自定义类),而无需为每种类型单独实现一套代码。

1.2 泛型的诞生背景

在泛型出现之前,开发者通常通过 object 类型或继承 IList 接口来实现通用容器。例如:

// 非泛型列表的实现  
public class MyList {  
    private object[] items;  
    public void Add(object item) {  
        // ...  
    }  
    public object Get(int index) {  
        return items[index];  
    }  
}  

但这种方式存在两个问题:

  1. 类型安全缺失:返回的 object 需要强制类型转换,容易引发 InvalidCastException
  2. 性能损耗:频繁的装箱(Boxing)和拆箱(Unboxing)会降低程序效率。

泛型的出现解决了这些问题:

  • 类型安全:通过类型参数 T 确保所有操作都基于同一类型。
  • 零性能开销:编译时会为每个具体类型(如 intstring)生成专用代码,避免运行时的类型转换。

1.3 泛型的比喻

想象泛型像一个“万能快递箱”:

  • 通用性:无论装的是书籍、衣服还是电子产品,箱子的结构都一致。
  • 安全性:箱子的锁只能对应特定类型的物品(例如,书籍箱不能装大件家具)。
  • 高效性:每个箱子根据内容物定制大小和材质,避免空间浪费。

二、泛型的基本使用

2.1 泛型类的定义与实例化

定义泛型类时,需在类名后添加类型参数列表,例如:

public class MyGenericClass<T> {  
    private T value;  
    public void Set(T input) {  
        value = input;  
    }  
    public T Get() {  
        return value;  
    }  
}  

使用时,指定具体类型即可:

var intBox = new MyGenericClass<int>();  
intBox.Set(100);  
int result = intBox.Get(); // 直接获取 int 类型,无需转换  

2.2 泛型方法

泛型方法允许在方法层级定义类型参数,例如:

public class Util {  
    public static T GetDefaultValue<T>() {  
        return default(T);  
    }  
}  

调用时:

int zero = Util.GetDefaultValue<int>(); // 0  
string empty = Util.GetDefaultValue<string>(); // ""  

2.3 内置泛型集合

C# 提供了丰富的泛型集合类,如 List<T>Dictionary<TKey, TValue> 等。例如:

// 泛型列表的使用  
List<string> names = new List<string>();  
names.Add("Alice");  
names.Add("Bob");  
foreach (string name in names) {  
    Console.WriteLine(name); // 输出 Alice、Bob  
}  

三、泛型约束(Constraints)

3.1 约束的必要性

泛型的灵活性可能带来类型不兼容的风险。例如,若希望泛型类 MyGenericClass<T>T 必须实现 IEnumerable 接口,则需添加约束。

3.2 约束类型与语法

C# 支持多种约束类型,常见用法如下表:

约束类型语法作用说明
类型约束where T : SomeClassT 必须是 SomeClass 或其子类
接口约束where T : IInterfaceT 必须实现 IInterface 接口
new() 约束where T : new()T 必须有无参构造函数
多重约束where T : ClassA, I1T 同时满足多个约束条件

示例代码

// 要求 T 必须是可空类型,并且可比较  
public class MyComparer<T> where T : struct, IComparable<T> {  
    public bool Compare(T a, T b) {  
        return a.CompareTo(b) == 0;  
    }  
}  

3.3 约束的比喻

约束如同“快递箱的尺寸限制”:

  • 如果规定箱子只能装 A4 纸(类型约束),则不能放入超过尺寸的物体。
  • 要求箱子必须防水(接口约束),则所有放入的物品必须满足这一条件。

四、协变与逆变(Covariance & Contravariance)

4.1 基本概念

协变和逆变是泛型类型间兼容性的扩展规则,允许在特定场景下使用更灵活的类型转换。

协变(Covariance)

允许将 IEnumerable<Derived> 转换为 IEnumerable<Base>,即“子类型到父类型”的转换。例如:

IEnumerable<Animal> animals = new List<Dog>(); // 允许  

逆变(Contravariance)

允许将 Action<Base> 转换为 Action<Derived>,即“父类型到子类型”的转换。例如:

Action<Animal> feedAnimal = (a) => { /* ... */ };  
Action<Dog> feedDog = feedAnimal; // 允许  

4.2 泛型接口与委托的标记

要启用协变或逆变,需在接口或委托定义时添加 out(协变)或 in(逆变)修饰符。例如:

// 协变接口  
public interface IEnumerable<out T> { /* ... */ }  

// 逆变委托  
public delegate void Action<in T>(T item);  

五、泛型的高级技巧与最佳实践

5.1 通过反射动态创建泛型类型

// 动态创建泛型列表  
Type genericListType = typeof(List<>).MakeGenericType(typeof(string));  
var stringList = Activator.CreateInstance(genericListType);  

5.2 泛型与性能优化

由于泛型在编译时生成专用代码,避免了 object 的装箱开销。例如:

// 非泛型版本  
public static void AddNumbers(List<object> list) {  
    int sum = 0;  
    foreach (var item in list) {  
        sum += (int)item; // 每次都需要拆箱  
    }  
}  

// 泛型版本  
public static void AddNumbers<T>(List<T> list) where T : struct, IConvertible {  
    int sum = 0;  
    foreach (var item in list) {  
        sum += Convert.ToInt32(item); // 直接转换,无需装箱  
    }  
}  

5.3 泛型与代码复用

通过泛型可减少重复代码。例如,实现一个通用的“缓存”类:

public class GenericCache<TKey, TValue> {  
    private Dictionary<TKey, TValue> _cache = new();  
    public void Add(TKey key, TValue value) => _cache[key] = value;  
    public TValue Get(TKey key) => _cache[key];  
}  

六、常见问题与解决方案

6.1 泛型类型参数的命名规范

通常使用单字母命名(如 TUKV),也可根据语义命名(如 TProduct)。

6.2 泛型与多态的结合

// 泛型方法与基类的结合  
public void ProcessItems<T>(IEnumerable<T> items) where T : BaseClass {  
    foreach (T item in items) {  
        item.VirtualMethod(); // 调用子类重写的方法  
    }  
}  

6.3 泛型与继承的注意事项

  • 泛型类本身不能继承非泛型类。
  • 泛型类的约束需继承自指定基类或接口。

结论

C# 泛型通过类型参数化实现了代码的复用性、类型安全性和高效性。从基础的集合类到复杂的通用算法,泛型贯穿于整个 C# 生态系统。掌握泛型的约束、协变逆变以及高级技巧,能让开发者编写出更优雅、健壮的代码。

在实际项目中,建议优先使用内置泛型集合类(如 List<T>Dictionary<TKey, TValue>),并根据需求自定义泛型工具类。通过持续实践,泛型将成为你应对复杂开发场景的得力工具。


(全文约 1800 字)

最新发布