在 C# 编程中,代码的复用性和灵活性是至关重要的。
在传统编程方式中,若需处理不同数据类型的相似逻辑,往往需要为每个类型编写重复代码。例如,针对int
和string
的集合操作需分别实现,这不仅冗余,还可能导致类型安全隐患。
在C# 2.0引入泛型后,它彻底改变了开发者编写可复用代码的方式。C#泛型(Generics)通过延迟类型指定(或称 类型参数化)的机制,允许开发者编写可复用的类型安全代码,更通过消除装箱拆箱操作显著优化了性能。接下来,我们就来深入探讨一下 C# 泛型的使用吧。
示例:要求实现输入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}");
}
List
中,T
就是类型参数。T
、TKey
、TValue
等有意义的类型参数名
使用一对尖括号 + 类型参数
T
,如MyGenericClass
、List
这种形式来定义泛型对象。
泛型类是最常见的泛型形式。它允许我们定义一个类,其行为可以独立于具体的数据类型。
T
是类型参数,代表任何数据类型。创建对象时,需要指定实际的数据类型。 public class Box<T>
{
private T _item;
public void Set(T item)
{
_item = item;
}
public T Get()
{
return _item;
}
}
在这个例子中,Box
是一个泛型类,T 是类型参数。我们可以通过指定具体的类型来实例化这个类:
Box<int> intBox = new Box<int>();
intBox.Set(42);
Console.WriteLine(intBox.Get()); // 输出:42
Box<string> stringBox = new Box<string>();
stringBox.Set("Hello, World!");
Console.WriteLine(stringBox.Get()); // 输出:Hello, World!
实例化 泛型对象的时候,必须指明具体的数据类型(如Box
、Box
),否则是无法实例化的。
public class Dictionary<TKey, TValue>
{
// 实现细节...
}
MyGeneric
public class MyGenericClass<T1,T2>
{
public void Test(T1 t1,T2 t2)
{
Console.WriteLine($"result:T={t1.GetType().Name};V={t2.GetType().Name}");
}
}
泛型类定义的时候与普通使用上基本相同,只不过类名后面多了个尖括号,尖括号中放了泛型参数用于占位
//普通类
public class MyClass
//泛型类
public class MyGenericClass<T>
泛型接口(如IEnumerable
)支持统一操作不同数据类型的集合。
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)
{
// 实现逻辑
}
}
泛型方法允许我们在方法级别上使用类型参数。这使得方法可以独立于具体类型,从而提高代码的复用性。
除了类之外,我们还可以定义泛型方法,即使它们所在的类不是泛型类:
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
是类型参数。它接受两个参数并返回较大的值。我们可以通过指定具体的类型来调用这个方法:
int maxInt = Utility.GetMax(10, 20);
Console.WriteLine(maxInt); // 输出:20
string maxString = Utility.GetMax("Apple", "Banana");
Console.WriteLine(maxString); // 输出:Banana
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
是泛型类。
泛型委托允许我们定义通用的委托类型,其行为可以独立于具体的数据类型。
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); // 输出:20
MyDelegate<string> stringDelegate = GetMax;
string maxString = stringDelegate("Apple", "Banana");
Console.WriteLine(maxString); // 输出:Banana
}
}
在这个例子中,MyDelegate 是一个泛型委托,它接受两个参数并返回一个值。我们可以通过指定具体的类型来使用这个委托。
若误以为静态成员跨类型共享,可能导致数据不一致或逻辑错误。示例如下:
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 = 1
StaticGeneric<string> staticGenericString=new StaticGeneric<string>();
Console.WriteLine($"Count = {StaticGeneric<string>.Count}"); //输出:Count = 1
}
}
在C#中,静态成员与泛型结合使用时需特别注意以下几点
StaticGeneric
)的静态成员与具体类型参数绑定,不同类型参数的实例视为不同类型。StaticGeneric.Count
结果为1,StaticGeneric.Count
结果仍为1获取类型默认值
default
来获取类型的默认值。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;
}
}
如上图,Add用于计算t1和t2之和的时候,直接使用int sum= t1+t2,会报错,因为还没有实例化,没有指定数据类型,无法直接适用于加法,但是如果使用dynamic,就可以跳过编译类型检查,改为在运行时解析这些操作。 就可以完成相关的业务逻辑。
IEnumerable
→ IEnumerable
)Action
→ Action
)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# 协变与逆变深入解析
约束类型 | 语法 | 说明 |
---|---|---|
基类约束 | where T : BaseClass |
T 必须继承自某个基类 |
接口约束 | where T : IInterface |
T 必须实现某个接口 |
值类型约束 | where T : struct |
T必须是值类型 |
引用类型约束 | where T : class |
T必须是引用类型 |
无参构造函数 | where T : new() |
T必须有无参构造函数 |
where
关键字限制类型参数,增强安全性引用类型约束示例:
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 被限制为必须实现 IComparable
接口。这意味着我们只能使用满足该约束的类型来实例化 Box
。
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 my_Generic3 = new MyGeneric3(new Score());
// 这样使用则没有问题
MyGeneric3<User> my_Generic3 = new MyGeneric3<User>(new User());
上例中,如果将Score 类 作为类型参数 传入,则会报错,因为该约束限制类型参数必须 有一个无参数的构造函数
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.");
}
}
}
注意:
非泛型集合(如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]; // 拆箱 + 类型不安全
泛型集合(如List
)在编译时即强制类型匹配,杜绝此类问题。
泛型集合List
彻底解决了这些问题:
List<int> numbers = new List<int>();
numbers.Add(42); // 无需装箱
numbers.Add("text"); // 编译时直接报错!
int val = numbers[0]; // 直接获取int类型
泛型避免装箱(Boxing)与拆箱(Unboxing)操作。例如,List
直接操作值类型,而ArrayList
需将int
装箱为object
,显著提升效率。
值类型处理效率对比测试:
操作类型 | 1000万次操作耗时 |
---|---|
ArrayList | 520ms |
List |
85ms |
提升幅度 | 6倍+ |
原因剖析:
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,因为泛型集合避免了装箱和拆箱操作。
通过泛型可编写通用逻辑,适应多种数据类型。例如,泛型方法Swap
可交换任意类型的变量:
void Swap<T>(ref T a, ref T b) {
T temp = a;
a = b;
b = temp;
}
反射(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);
通过泛型接口与DI容器结合,实现服务通用化:
services.AddScoped(typeof(IValidator<>), typeof(ProductValidator));
此配置可为所有实体类型自动提供验证逻辑。
public class Cache<T>
{
public static DateTime CreatedTime { get; } = DateTime.Now;
// 每个不同的T类型都会创建独立的静态字段
}
仓储模式的现代化实现:
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();
}
C#的集合框架大量使用了泛型,如List
、Dictionary
等,它们都提供了类型安全的操作,并避免了装箱拆箱带来的性能损耗。
var list = new List<int>();
list.Add(1);
Console.WriteLine(list[0]); // 输出: 1
创建自己的泛型类或方法可以帮助我们编写更具复用性的代码。比如,创建一个简单的缓存机制:
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];
}
}
T
、TKey
、TValue
等有意义的类型参数名回到目录页:C#/.NET 知识汇总
希望以上内容可以帮助到大家,如文中有不对之处,还请批评指正。
参考资料:
.NET泛型集合源码解析
泛型与设计模式实践
微软官方泛型文档
泛型性能优化白皮书
设计模式中的泛型应用案例集