1. 为什么我们需要泛型?
我第一次接触泛型是在处理一个集合操作时。当时我需要一个既能存储整数又能存储字符串的列表,但又不希望它们混在一起。使用ArrayList虽然能实现,但每次取出元素都要进行类型转换,代码既冗长又容易出错。这就是泛型要解决的核心问题——类型安全与代码复用。
泛型本质上是一种参数化类型,允许我们在定义类、接口或方法时使用类型参数。这个类型参数在实际使用时才确定具体类型。想象一下,泛型就像是一个模板,你可以用这个模板创建出处理不同数据类型的版本,而无需为每种类型都重写一遍代码。
在C#中,泛型最常见的应用场景就是集合类。比如List
-
编译时类型检查:编译器能在编译阶段就发现类型不匹配的错误,而不是等到运行时才抛出异常。
-
避免装箱拆箱:对于值类型,非泛型集合需要装箱操作,而泛型集合可以直接操作值类型,性能更高。
2. 泛型基础语法详解
2.1 泛型类定义
定义一个泛型类非常简单,只需要在类名后面加上尖括号和类型参数即可。类型参数通常用单个大写字母表示,常见的有T(Type)、TKey、TValue等。
csharp复制public class MyGenericClass<T>
{
private T _value;
public MyGenericClass(T value)
{
_value = value;
}
public T GetValue()
{
return _value;
}
}
这个简单的泛型类可以存储任何类型的值。使用时:
csharp复制var intClass = new MyGenericClass<int>(42);
var stringClass = new MyGenericClass<string>("Hello");
2.2 泛型方法
泛型不仅可以应用于整个类,也可以只应用于单个方法。这在工具类中特别有用。
csharp复制public static class Utility
{
public static T Max<T>(T a, T b) where T : IComparable<T>
{
return a.CompareTo(b) > 0 ? a : b;
}
}
这个方法可以比较任何实现了IComparable接口的类型:
csharp复制int maxInt = Utility.Max(3, 5);
string maxString = Utility.Max("apple", "banana");
2.3 类型约束
类型约束是泛型中非常重要的概念,它限制了可以用作泛型参数的类型。常见的约束有:
- where T : struct - T必须是值类型
- where T : class - T必须是引用类型
- where T : new() - T必须有无参构造函数
- where T : BaseClass - T必须继承自BaseClass
- where T : Interface - T必须实现Interface
csharp复制public class Repository<T> where T : class, IEntity, new()
{
public void Add(T entity)
{
// 实现添加逻辑
}
}
这个例子中,T必须是引用类型、实现IEntity接口并且有无参构造函数。
3. 泛型集合深入解析
3.1 List的内部实现
List
List
csharp复制// 不好的做法 - 会多次扩容
var list = new List<int>();
for (int i = 0; i < 10000; i++) list.Add(i);
// 好的做法 - 预先分配足够空间
var list = new List<int>(10000);
for (int i = 0; i < 10000; i++) list.Add(i);
3.2 Dictionary<TKey, TValue>的哈希机制
Dictionary使用哈希表来存储键值对,查找效率接近O(1)。它的性能很大程度上取决于GetHashCode方法的实现。
一个好的GetHashCode应该:
- 对于相等的对象返回相同的哈希码
- 哈希码应该尽可能均匀分布
- 应该是快速计算的
csharp复制public class Person
{
public string Name { get; set; }
public int Age { get; set; }
public override int GetHashCode()
{
unchecked // 允许整数溢出
{
int hash = 17;
hash = hash * 23 + (Name?.GetHashCode() ?? 0);
hash = hash * 23 + Age.GetHashCode();
return hash;
}
}
}
3.3 自定义泛型集合
有时我们需要创建自己的泛型集合。下面是一个简单的泛型栈实现:
csharp复制public class MyStack<T>
{
private readonly List<T> _items = new List<T>();
public void Push(T item)
{
_items.Add(item);
}
public T Pop()
{
if (_items.Count == 0)
throw new InvalidOperationException("Stack is empty");
var item = _items[_items.Count - 1];
_items.RemoveAt(_items.Count - 1);
return item;
}
public T Peek()
{
return _items[_items.Count - 1];
}
}
4. 高级泛型技巧
4.1 协变和逆变
C# 4.0引入了协变(out)和逆变(in)的概念,允许在泛型接口和委托中使用更灵活的类型转换。
协变允许使用比指定类型更派生的类型,用out关键字标记:
csharp复制IEnumerable<Derived> derived = new List<Derived>();
IEnumerable<Base> bases = derived; // 协变
逆变允许使用比指定类型更通用的类型,用in关键字标记:
csharp复制Action<Base> actionBase = (b) => Console.WriteLine(b);
Action<Derived> actionDerived = actionBase; // 逆变
4.2 泛型缓存
静态字段在泛型类中是按封闭类型共享的。这意味着List
csharp复制public class Cache<T>
{
private static readonly Dictionary<string, T> _cache = new Dictionary<string, T>();
public static void Add(string key, T value)
{
_cache[key] = value;
}
public static T Get(string key)
{
return _cache.TryGetValue(key, out var value) ? value : default;
}
}
4.3 泛型与反射
有时我们需要在运行时处理泛型类型。这时就需要使用反射了。
csharp复制Type openType = typeof(List<>);
Type closedType = openType.MakeGenericType(typeof(int));
object list = Activator.CreateInstance(closedType);
这个方法在编写通用框架代码时特别有用,比如依赖注入容器。
5. 泛型在实际项目中的应用
5.1 仓储模式实现
泛型在实现仓储模式时非常有用。下面是一个基本的泛型仓储接口和实现:
csharp复制public interface IRepository<T> where T : class
{
T GetById(int id);
IEnumerable<T> GetAll();
void Add(T entity);
void Update(T entity);
void Delete(T entity);
}
public class Repository<T> : IRepository<T> where T : class
{
private readonly DbContext _context;
private readonly DbSet<T> _dbSet;
public Repository(DbContext context)
{
_context = context;
_dbSet = context.Set<T>();
}
public T GetById(int id)
{
return _dbSet.Find(id);
}
// 其他方法实现...
}
5.2 通用服务层
在服务层中,我们经常需要处理各种DTO的转换。泛型可以帮助我们减少重复代码:
csharp复制public interface IMapper<TFrom, TTo>
{
TTo Map(TFrom source);
}
public class Service<TModel, TDto>
{
private readonly IRepository<TModel> _repository;
private readonly IMapper<TModel, TDto> _mapper;
public Service(IRepository<TModel> repository, IMapper<TModel, TDto> mapper)
{
_repository = repository;
_mapper = mapper;
}
public TDto GetById(int id)
{
var model = _repository.GetById(id);
return _mapper.Map(model);
}
// 其他方法...
}
5.3 通用验证器
使用泛型可以创建灵活的验证框架:
csharp复制public interface IValidator<T>
{
ValidationResult Validate(T entity);
}
public class Validator<T> : IValidator<T>
{
private readonly List<IValidationRule<T>> _rules = new List<IValidationRule<T>>();
public Validator(IEnumerable<IValidationRule<T>> rules)
{
_rules.AddRange(rules);
}
public ValidationResult Validate(T entity)
{
var result = new ValidationResult();
foreach (var rule in _rules)
{
if (!rule.IsValid(entity))
{
result.AddError(rule.ErrorMessage);
}
}
return result;
}
}
6. 泛型性能考量
6.1 泛型与JIT编译
泛型在.NET中的实现方式是通过JIT(即时编译)为每个值类型生成特定的代码,而为所有引用类型共享一份代码。这意味着:
- 使用List
和List 会生成不同的原生代码 - 但List
和List
这种设计在保证类型安全的同时,也兼顾了性能。
6.2 避免装箱拆箱
使用泛型集合(如List
csharp复制// 使用ArrayList - 需要装箱
ArrayList list = new ArrayList();
list.Add(1); // 装箱
int value = (int)list[0]; // 拆箱
// 使用List<T> - 无需装箱
List<int> genericList = new List<int>();
genericList.Add(1); // 无装箱
int genericValue = genericList[0]; // 无拆箱
6.3 泛型方法的性能
泛型方法通常比使用object作为参数的非泛型方法性能更好,特别是对于值类型:
csharp复制// 非泛型方法
public static object Max(object a, object b)
{
return ((IComparable)a).CompareTo(b) > 0 ? a : b;
}
// 泛型方法
public static T Max<T>(T a, T b) where T : IComparable<T>
{
return a.CompareTo(b) > 0 ? a : b;
}
对于值类型,非泛型版本需要装箱,而泛型版本不需要。
7. 常见问题与解决方案
7.1 泛型类型推断失败
有时编译器无法推断泛型方法的类型参数,这时需要显式指定类型:
csharp复制// 无法推断类型
var result = Utility.Max(1, 2.5); // 错误
// 显式指定类型
var result = Utility.Max<double>(1, 2.5); // 正确
7.2 泛型与运算符
泛型类型不能直接使用运算符,因为编译器不知道T是否支持这些运算符。解决方法:
- 使用dynamic(性能较差)
- 使用表达式树
- 添加约束并使用接口
csharp复制public static T Add<T>(T a, T b)
{
dynamic da = a, db = b;
return da + db;
}
7.3 泛型与null
对于可能为值类型的泛型参数,直接与null比较会有问题:
csharp复制public static bool IsNull<T>(T value)
{
// 对于值类型,这总是false
return value == null;
// 更好的方式
return EqualityComparer<T>.Default.Equals(value, default(T));
}
8. 泛型最佳实践
-
命名约定:使用有意义的类型参数名,如TKey、TValue,而不仅仅是T。
-
约束最小化:只添加必要的约束,保持泛型的灵活性。
-
文档注释:为泛型类型和方法添加详细的XML注释,说明类型参数的预期用途。
-
避免过度泛化:不是所有东西都需要做成泛型的,只有在真正需要处理多种类型时才使用泛型。
-
性能测试:对于性能关键的泛型代码,应该针对不同类型进行性能测试。
-
单元测试:为泛型代码编写针对不同类型参数的测试用例。
-
考虑可读性:过于复杂的泛型代码可能难以理解和维护,需要在灵活性和可读性之间找到平衡。
csharp复制/// <summary>
/// 表示一个键值对集合
/// </summary>
/// <typeparam name="TKey">键的类型</typeparam>
/// <typeparam name="TValue">值的类型</typeparam>
public class KeyValuePair<TKey, TValue>
{
public TKey Key { get; }
public TValue Value { get; }
public KeyValuePair(TKey key, TValue value)
{
Key = key;
Value = value;
}
}
