总目录
前言
在 C# 编程中,代码的复用性和灵活性是至关重要的。
在传统编程方式中,若需处理不同数据类型的相似逻辑,往往需要为每个类型编写重复代码。例如,针对int
和string
的集合操作需分别实现,这不仅冗余,还可能导致类型安全隐患。
在C# 2.0引入泛型后,它彻底改变了开发者编写可复用代码的方式。C#泛型(Generics)通过延迟类型指定(或称 类型参数化)的机制,允许开发者编写可复用的类型安全代码,更通过消除装箱拆箱操作显著优化了性能。接下来,我们就来深入探讨一下 C# 泛型的使用吧。
一、什么是泛型
1. 基本概念
- 泛型(Generics)是一种编程范式,它允许我们在定义类、方法或接口时,使用占位符(类型参数)代替具体的类型。在实际使用时,这些占位符会被具体的类型替换,从而实现类型安全的代码复用。
- 通过使用泛型,我们可以编写适用于多种数据类型的代码,而无需为每种类型单独写代码,这不仅提高了代码的复用性,还增强了类型安全性和效率。
2. 泛型的优点
- 类型安全:由于泛型代码在编译时会进行类型检查,因此减少了运行时出现错误的可能性。
- 性能提升:泛型在运行时会生成特定类型的代码,避免了装箱和拆箱操作,提高了性能。
- 代码复用:通过泛型,我们可以编写多种数据类型通用的类和方法,减少重复代码。
- 可读性增强:泛型代码更清晰,意图更明确。
3. 痛点场景
示例:要求实现输入int ,string,datetime类型的值的时候,打印出对应的类型和值
public class CommonMethod{//打印int的数据类型和值public static void ShowInt(int a){Console.WriteLine($"result:type={a.GetType().Name},value={a}");}//打印string的数据类型和值public static void ShowString(string s){Console.WriteLine($"result:type={s.GetType().Name},value={s}");}//打印DateTime 的数据类型和值public static void ShowDateTime(DateTime dt){Console.WriteLine($"result:type={dt.GetType().Name},value={dt}");}}
以上示例中除了传入参数的数据类型不同,其余的处理逻辑相同,明显代码没有得到复用,于是简化代码如下:
// 打印输入参数的数据类型和值public static void ShowObject(object o){Console.WriteLine($"result:type={o.GetType().Name},value={o}");}
示例中,直接使用object 完成了代码的复用,让代码变得更加简洁通用,但是在这个过程中存在数据类型转化,也就涉及到了装箱和拆箱,严重的影响程序的性能。
那么现在能不能找一个 既能满足 代码复用的需求 又能避免装箱和拆箱带来性能损耗的方法呢?
有,那就是使用泛型,使用泛型优化示例代码:
//打印输入参数的数据类型和值public static void ShowResult<T>(T t){Console.WriteLine($"result:type={t.GetType().Name},value={t}");}
二、如何使用泛型?
1. 类型参数
- 类型参数:是指在定义类、接口或方法时使用的占位符,代表实际使用时指定的数据类型。
- 例如,在
List<T>
中,T
就是类型参数。 - 类型参数 可以代表任何类型,也可识别任何类型
- 例如,在
- 类型参数命名规范:推荐使用
T
、TKey
、TValue
等有意义的类型参数名- 泛型参数一般用T表示,但是不代表不可以使用别的代表,也可使用V、K等自定义的名称,但是推荐命名的时候使用T或者T开头
- 类型参数数量:数量不限,业务中需要几个类型参数,就设置几个类型参数,不过建议类型参数不要过多
使用一对尖括号 + 类型参数
T
,如MyGenericClass<T>
、List<T>
这种形式来定义泛型对象。
2. 泛型类
泛型类是最常见的泛型形式。它允许我们定义一个类,其行为可以独立于具体的数据类型。
1)泛型类
- 定义泛型类的基本语法如下:
- 这里
T
是类型参数,代表任何数据类型。创建对象时,需要指定实际的数据类型。
- 这里
public class Box<T>{private T _item;public void Set(T item){_item = item;}public T Get(){return _item;}}
在这个例子中,Box<T>
是一个泛型类,T 是类型参数。我们可以通过指定具体的类型来实例化这个类:
- 使用泛型类
Box<int> intBox = new Box<int>();intBox.Set(42);Console.WriteLine(intBox.Get()); // 输出:42Box<string> stringBox = new Box<string>();stringBox.Set("Hello, World!");Console.WriteLine(stringBox.Get()); // 输出:Hello, World!
实例化 泛型对象的时候,必须指明具体的数据类型(如Box<int>
、Box<string>
),否则是无法实例化的。
2)多类型参数
- 泛型类可以接受多个类型参数。例如,一个字典类可能需要键和值两种类型:
public class Dictionary<TKey, TValue>
{// 实现细节...
}
- 根据实际业务需求,可以定义多个泛型 类型参数 ,如
MyGeneric<T1,T2>
public class MyGenericClass<T1,T2>{public void Test(T1 t1,T2 t2){Console.WriteLine($"result:T={t1.GetType().Name};V={t2.GetType().Name}");}}
3) 泛型类和普通类
泛型类定义的时候与普通使用上基本相同,只不过类名后面多了个尖括号,尖括号中放了泛型参数用于占位
//普通类
public class MyClass
//泛型类
public class MyGenericClass<T>
3. 泛型接口
泛型接口(如IEnumerable<T>
)支持统一操作不同数据类型的集合。
public interface IRepository<T>
{T GetById(int id);void Add(T item);void Update(T item);void Delete(T item);
}
在这个例子中,IRepository 是一个泛型接口,T 是类型参数。我们可以为不同的类型实现这个接口:
public class User
{public int Id { get; set; }public string Name { get; set; }
}public class UserRepository : IRepository<User>
{public User GetById(int id){// 实现逻辑return new User { Id = id, Name = "John Doe" };}public void Add(User item){// 实现逻辑}public void Update(User item){// 实现逻辑}public void Delete(User item){// 实现逻辑}
}
4. 泛型方法
泛型方法允许我们在方法级别上使用类型参数。这使得方法可以独立于具体类型,从而提高代码的复用性。
1) 泛型方法
除了类之外,我们还可以定义泛型方法,即使它们所在的类不是泛型类:
public class Utility
{public static void Swap<T>(ref T a, ref T b){T temp = a;a = b;b = temp;}
}
在这个例子中,Swap方法可以交换任意类型的两个变量的值。
public class Utility
{public static T GetMax<T>(T a, T b) where T : IComparable<T>{return a.CompareTo(b) > 0 ? a : b;}
}
在这个例子中,GetMax<T>
是一个泛型方法,T
是类型参数。它接受两个参数并返回较大的值。我们可以通过指定具体的类型来调用这个方法:
int maxInt = Utility.GetMax(10, 20);
Console.WriteLine(maxInt); // 输出:20string maxString = Utility.GetMax("Apple", "Banana");
Console.WriteLine(maxString); // 输出:Banana
2)泛型方法和普通方法
public class MyGeneric<T>{public MyGeneric(T t){Console.WriteLine($"result:type={t.GetType().Name};value={t}");}public void Show(T t){Console.WriteLine($"result:type={t.GetType().Name};value={t}");}public void Test<V>(V v){}}
在这个例子中,Show
是 具有泛型参数的 普通方法,Test
是泛型方法,MyGeneric<T>
是泛型类。
5. 泛型委托
泛型委托允许我们定义通用的委托类型,其行为可以独立于具体的数据类型。
public delegate T MyDelegate<T>(T a, T b);public class Program
{public static T GetMax<T>(T a, T b) where T : IComparable<T>{return a.CompareTo(b) > 0 ? a : b;}static void Main(){MyDelegate<int> intDelegate = GetMax;int maxInt = intDelegate(10, 20);Console.WriteLine(maxInt); // 输出:20MyDelegate<string> stringDelegate = GetMax;string maxString = stringDelegate("Apple", "Banana");Console.WriteLine(maxString); // 输出:Banana}
}
在这个例子中,MyDelegate 是一个泛型委托,它接受两个参数并返回一个值。我们可以通过指定具体的类型来使用这个委托。
6. 静态成员与泛型
若误以为静态成员跨类型共享,可能导致数据不一致或逻辑错误。示例如下:
public class StaticGeneric<T>
{public static int Count;public StaticGeneric(){Count++;}
}
public class Program
{public static void Main(){StaticGeneric<int> staticGenericInt=new StaticGeneric<int>();Console.WriteLine($"Count = {StaticGeneric<int>.Count}"); //输出:Count = 1StaticGeneric<string> staticGenericString=new StaticGeneric<string>(); Console.WriteLine($"Count = {StaticGeneric<string>.Count}"); //输出:Count = 1}
}
在C#中,静态成员与泛型结合使用时需特别注意以下几点
- 静态成员不共享:
- 不同类型参数的泛型实例会生成独立的静态成员,导致数据无法共享。
- 避免在泛型类型中声明静态成员:
- 泛型类型(如
StaticGeneric<T>
)的静态成员与具体类型参数绑定,不同类型参数的实例视为不同类型。 - 如
StaticGeneric<int>.Count
结果为1,StaticGeneric<string>.Count
结果仍为1
- 泛型类型(如
7. 泛型与default、dynamic关键字
1)与 default
-
获取类型默认值
- 在泛型中处理默认值时,可以使用
default
来获取类型的默认值。 - 对于引用类型,默认值为null;对于数值类型,默认值为0。
public T GetDefault<T>() {return default(T); // 引用类型返回null,值类型返回0等 }
从 C# 7.1 开始,可以直接使用 default 而不带括号来简化语法:
class GenericExample<T> {public T GetDefaultValue(){return default;} }
- 在泛型中处理默认值时,可以使用
-
设置类型默认值
class GenericExample<T,V>
{private T t;private V v;public GenericExample(){t = default;v = default;}
}
2) 与 dynamic
如上图,Add用于计算t1和t2之和的时候,直接使用int sum= t1+t2,会报错,因为还没有实例化,没有指定数据类型,无法直接适用于加法,但是如果使用dynamic,就可以跳过编译类型检查,改为在运行时解析这些操作。 就可以完成相关的业务逻辑。
8. 协变与逆变中的泛型
- 协变(Covariance):允许子类泛型赋值给父类(
IEnumerable<string>
→IEnumerable<object>
) - 逆变(Contravariance):允许父类泛型赋值给子类(
Action<object>
→Action<string>
)
C#支持通过out
和in
关键字来标记泛型参数是否支持协变或逆变。
//协变接口
public interface ICovariant<out T>
{T Get();
}//逆变接口
public interface IContravariant<in T>
{void Set(T item);
}
集合接口的智能转换:
// 支持将派生类集合赋值给基类变量
IEnumerable<string> strings = new List<string>();
IEnumerable<object> objects = strings; // 协变(Covariance)// 允许处理基类的接口处理派生类
Action<object> objectAction = obj => Console.WriteLine(obj);
Action<string> stringAction = objectAction; // 逆变(Contravariance)
关于 协变与逆变 详见:C# 协变与逆变深入解析
三、泛型约束
1. 泛型约束概览
常用泛型约束概览
约束类型 | 语法 | 说明 |
---|---|---|
基类约束 | where T : BaseClass | T 必须继承自某个基类 |
接口约束 | where T : IInterface | T 必须实现某个接口 |
值类型约束 | where T : struct | T必须是值类型 |
引用类型约束 | where T : class | T必须是引用类型 |
无参构造函数 | where T : new() | T必须有无参构造函数 |
2. 使用泛型约束
- 有时候,我们可能想要对泛型中的类型参数施加一些限制,比如要求该类型必须实现某个接口或继承自某个基类。这时,我们可以使用约束。
- 泛型约束通过
where
关键字限制类型参数,增强安全性
1)单个约束示例
引用类型约束示例:
public class MyClassGeneric<T> where T : class{public MyClassGeneric(T t){Console.WriteLine($"result:type={t.GetType().Name};value={t}");}}public class User{public int Id { get; set; }public string Name { get; set; }}public class Program{public static void Main(){MyClassGeneric<string> myClassGeneric1 = new MyClassGeneric<string>("test");MyClassGeneric<User> myClassGeneric2 = new MyClassGeneric<User>(new User() { Id=1,Name="Jack"});}}
这里的where T : class
表示类型参数T
必须是引用类型。如示例中 使用int 类型,则会报错。
值类型约束示例:
public class MyStructGeneric<T> where T : struct{public My_Generic(T t){Console.WriteLine($"result:type={t.GetType().Name};value={t}");}}//使用MyStructGeneric<int> myStructGeneric = new MyStructGeneric<int>();
这里的where T : struct
表示类型参数T
必须是不可为null的值类型。如示例中 使用int 类型,如果使用 string 类型则会报错,因为string 是引用类型。
接口/基类约束示例:
public class Box<T> where T : IComparable<T>
{private T _item;public void Set(T item){_item = item;}public T Get(){return _item;}
}
在这个例子中,Box<T>
的类型参数 T 被限制为必须实现 IComparable<T>
接口。这意味着我们只能使用满足该约束的类型来实例化 Box<T>
。
public interface IPeople{void GetUserInfo();}public class Chinese : IPeople{public void GetUserInfo(){//throw new NotImplementedException();}}//这个泛型类规定必须是IPeople或者是继承于Ipeople的数据类型才可传入public class MyGeneric4<T> where T : IPeople{public MyGeneric4(){}}//使用MyGeneric4<IPeople> myGeneric4 = new MyGeneric4<IPeople>();MyGeneric4<Chinese> my_Generic44 = new MyGeneric4<Chinese>();
使用的时候 T
必须是IPeople
或者是继承于Ipeople
的数据类型才可传入
无参数构造函数 约束示例:
public class MyGeneric3<T> where T : new (){public MyGeneric3(T t){Console.WriteLine($"result:type={t.GetType().Name};value={t}");}}public class User{public int Id { get; set; }public string Name { get; set; }}public class Score{public Score(string code){//有参数的构造函数}}
// 如果这样使用就会报错
// MyGeneric3<Score> my_Generic3 = new MyGeneric3<Score>(new Score());
// 这样使用则没有问题
MyGeneric3<User> my_Generic3 = new MyGeneric3<User>(new User());
上例中,如果将Score 类 作为类型参数 传入,则会报错,因为该约束限制类型参数必须 有一个无参数的构造函数
2)组合约束示例
public T CreateInstance<T>() where T : Animal, IFly, new()
{return new T();
}
public class GenericClass<T> where T : IComparable, new()
{public void DoSomethingWithGeneric(T input){if (input.CompareTo(default(T)) > 0){Console.WriteLine("Greater than default.");}}
}
注意:
- 约束可以组合使用
- 与其他约束一起使用时,new() 约束必须最后指定。
三、为什么使用泛型?
1. 类型安全性
1) 非泛型集合示例
非泛型集合(如ArrayList
)存储object
类型,需显式转换且易引发运行时错误:
ArrayList list = new ArrayList();
list.Add(1);
list.Add("text");
int num = (int)list[1]; // 运行时异常!
在泛型出现前,ArrayList
等集合类以object
存储元素,导致:
ArrayList list = new ArrayList();
list.Add(1); // 装箱
int num = (int)list[0]; // 拆箱 + 类型不安全
2) 泛型集合示例
泛型集合(如List<T>
)在编译时即强制类型匹配,杜绝此类问题。
泛型集合List<T>
彻底解决了这些问题:
List<int> numbers = new List<int>();
numbers.Add(42); // 无需装箱
numbers.Add("text"); // 编译时直接报错!
int val = numbers[0]; // 直接获取int类型
2. 性能优化
泛型避免装箱(Boxing)与拆箱(Unboxing)操作。例如,List<int>
直接操作值类型,而ArrayList
需将int
装箱为object
,显著提升效率。
值类型处理效率对比测试:
操作类型 | 1000万次操作耗时 |
---|---|
ArrayList | 520ms |
List<T> | 85ms |
提升幅度 | 6倍+ |
原因剖析:
- 避免值类型装箱(Heap内存分配)
- 消除类型检查开销
性能测试
public class Program
{static void Main(){// 使用非泛型集合ArrayList arrayList = new ArrayList();for (int i = 0; i < 100_0000; i++){arrayList.Add(i);}// 使用泛型集合List<int> list = new List<int>();for (int i = 0; i < 100_0000; i++){list.Add(i);}// 测试性能Stopwatch sw = Stopwatch.StartNew();foreach (var item in arrayList){int value = (int)item; // 装箱和拆箱操作}sw.Stop();Console.WriteLine($"非泛型集合:{sw.ElapsedMilliseconds} ms");sw.Restart();foreach (var item in list){int value = item; // 无需装箱和拆箱}Console.WriteLine($"泛型集合:{sw.ElapsedMilliseconds} ms");}
}
运行结果:
非泛型集合:28 ms
泛型集合:7 ms
在这个例子中,使用泛型集合 List 的性能明显优于非泛型集合 ArrayList,因为泛型集合避免了装箱和拆箱操作。
3. 代码复用性
通过泛型可编写通用逻辑,适应多种数据类型。例如,泛型方法Swap<T>
可交换任意类型的变量:
void Swap<T>(ref T a, ref T b) {T temp = a;a = b;b = temp;
}
四、泛型应用场景
1. 泛型与反射
反射(Reflection)允许我们在运行时检查和操作类型的信息。泛型与反射结合使用时,可以实现非常灵活的动态行为。
using System;
using System.Reflection;public class Box<T>
{private T _item;public void Set(T item){_item = item;}public T Get(){return _item;}
}public class Program
{static void Main(){Box<int> intBox = new Box<int>();intBox.Set(42);Type boxType = intBox.GetType();FieldInfo field = boxType.GetField("_item", BindingFlags.NonPublic | BindingFlags.Instance);object value = field.GetValue(intBox);Console.WriteLine(value); // 输出:42}
}
动态创建泛型实例:
Type openType = typeof(List<>);
Type closedType = openType.MakeGenericType(typeof(int));
object list = Activator.CreateInstance(closedType);
Type openType = typeof(Dictionary<,>);
Type closedType = openType.MakeGenericType(typeof(int), typeof(string));
object dict = Activator.CreateInstance(closedType);
当编译器无法推断类型时:
// 错误示例
var result = CreateInstance(typeof(List<>)); // 正确写法
var listType = typeof(List<>);
var specificType = listType.MakeGenericType(typeof(int));
var instance = Activator.CreateInstance(specificType);
2. 泛型在依赖注入中的应用
通过泛型接口与DI容器结合,实现服务通用化:
services.AddScoped(typeof(IValidator<>), typeof(ProductValidator));
此配置可为所有实体类型自动提供验证逻辑。
3. 泛型缓存特性
public class Cache<T>
{public static DateTime CreatedTime { get; } = DateTime.Now;// 每个不同的T类型都会创建独立的静态字段
}
4. 泛型与设计模式
仓储模式的现代化实现:
public interface IRepository<T> where T : class
{T GetById(int id);void Add(T entity);
}public class UserRepository : IRepository<User>
{// 具体实现
}// 依赖注入配置
services.AddScoped<IRepository<User>, UserRepository>();
泛型工厂模式
public interface IFactory<T>
{T Create();
}public class CarFactory : IFactory<Car>
{public Car Create() => new SportsCar();
}
5. 集合框架
C#的集合框架大量使用了泛型,如List<T>
、Dictionary<TKey, TValue>
等,它们都提供了类型安全的操作,并避免了装箱拆箱带来的性能损耗。
var list = new List<int>();
list.Add(1);
Console.WriteLine(list[0]); // 输出: 1
6. 自定义泛型
创建自己的泛型类或方法可以帮助我们编写更具复用性的代码。比如,创建一个简单的缓存机制:
public class Cache<TKey, TValue>
{private Dictionary<TKey, TValue> _cache = new Dictionary<TKey, TValue>();public void Add(TKey key, TValue value){_cache[key] = value;}public TValue Get(TKey key){return _cache[key];}
}
五、最佳实践
1. 合理使用泛型
- 泛型可以提高代码的复用性和性能,但并不是万能的。在某些场景下,过度使用泛型可能会导致代码难以理解和维护。因此,我们需要根据实际情况合理使用泛型。
- 避免过度泛型化,避免过度嵌套泛型类型
- 类型参数命名,使用
T
、TKey
、TValue
等有意义的类型参数名
2. 避免过多的类型参数
- 过多的类型参数会增加代码的复杂性。一般来说,类型参数的数量不应超过 3 个。如果需要更多类型参数,可以考虑将多个类型合并为一个元组或自定义类型。
3. 使用类型约束
- 类型约束可以限制类型参数的范围,从而提高代码的安全性和灵活性。合理使用类型约束可以避免不必要的运行时错误。
结语
回到目录页:C#/.NET 知识汇总
希望以上内容可以帮助到大家,如文中有不对之处,还请批评指正。
参考资料:
.NET泛型集合源码解析
泛型与设计模式实践
微软官方泛型文档
泛型性能优化白皮书
设计模式中的泛型应用案例集