在 C# 中,泛型(Generics) 是一种允许在定义类、接口、方法、委托时使用"未指定类型"(类型参数),在使用时再指定具体类型 的技术。它的核心价值是:用一份代码支持多种数据类型 ,同时保证类型安全(编译时检查),避免传统方法中因使用 object
类型导致的装箱拆箱性能损耗和类型转换错误。
一、为什么需要泛型?(传统方法的问题)
在泛型出现前,若要实现"支持多种类型的通用逻辑"(如集合、工具方法),通常有两种方案,但都存在明显缺陷:
1. 为每种类型写重复代码(代码冗余)
例如,实现一个"整数栈"和"字符串栈",逻辑完全相同但类型不同,必须重复编码:
csharp
// 整数栈
public class IntStack
{
private int[] _items = new int[10];
private int _index = 0;
public void Push(int item) => _items[_index++] = item;
public int Pop() => _items[--_index];
}
// 字符串栈(与IntStack逻辑相同,仅类型不同)
public class StringStack
{
private string[] _items = new string[10];
private int _index = 0;
public void Push(string item) => _items[_index++] = item;
public string Pop() => _items[--_index];
}
2. 使用 object
类型(类型不安全 + 性能损耗)
用 object
作为通用类型(所有类型的基类),可减少代码冗余,但存在问题:
- 类型不安全:编译时无法检查类型,运行时可能因类型不匹配报错;
- 装箱拆箱 :值类型(如
int
)存入object
会装箱(堆内存分配),取出时拆箱(类型转换),损耗性能。
csharp
// 用object实现的"通用"栈
public class ObjectStack
{
private object[] _items = new object[10];
private int _index = 0;
public void Push(object item) => _items[_index++] = item;
public object Pop() => _items[--_index];
}
// 使用时的问题
var stack = new ObjectStack();
stack.Push(10); // int装箱为object
stack.Push("hello"); // 混合存入不同类型(编译不报错)
int num = (int)stack.Pop(); // 第二次Pop取出的是"hello",强制转换为int会抛运行时异常!
泛型的出现正是为了解决这些问题:用一份代码支持多种类型,同时保证类型安全(编译时检查),且无需装箱拆箱(值类型直接处理)。
二、泛型的基本用法
泛型通过类型参数(Type Parameter) 实现通用化,语法上用 <T>
表示(T
是类型参数名,可自定义,如 <TItem>
<TData>
)。
1. 泛型类(Generic Class)
泛型类是最常用的泛型形式,在类定义时声明类型参数,实例化时指定具体类型。
示例:用泛型实现通用栈(解决上述两个问题)
csharp
// 泛型栈类:T是类型参数(表示"待指定的类型")
public class GenericStack<T> // <T> 声明类型参数
{
private T[] _items; // 用T作为数组元素类型
private int _index = 0;
// 构造函数:初始化数组大小
public GenericStack(int capacity)
{
_items = new T[capacity]; // 创建T类型的数组
}
// 入栈:参数类型为T
public void Push(T item)
{
if (_index < _items.Length)
_items[_index++] = item;
else
throw new StackOverflowException("栈已满");
}
// 出栈:返回类型为T
public T Pop()
{
if (_index > 0)
return _items[--_index];
else
throw new InvalidOperationException("栈为空");
}
}
// 使用泛型类:实例化时指定具体类型(如int、string)
class Program
{
static void Main()
{
// 1. 整数栈(指定T为int)
var intStack = new GenericStack<int>(5); // <int> 确定类型参数
intStack.Push(10);
intStack.Push(20);
int num = intStack.Pop(); // 无需类型转换,直接返回int
Console.WriteLine(num); // 输出:20
// 2. 字符串栈(指定T为string)
var strStack = new GenericStack<string>(5);
strStack.Push("hello");
strStack.Push("world");
string str = strStack.Pop(); // 直接返回string
Console.WriteLine(str); // 输出:world
// 3. 类型安全:编译时检查,不允许混合类型
intStack.Push("not int"); // 编译报错!无法将string转换为int
}
}
优势:
- 一份代码支持任意类型(
int
、string
、自定义类等); - 编译时检查类型,避免运行时错误;
- 值类型(如
int
)无需装箱拆箱,性能更优。
2. 泛型方法(Generic Method)
泛型方法是在方法级别声明类型参数,可独立于泛型类使用(即非泛型类中也可定义泛型方法)。
示例:实现通用的"交换两个变量"方法
csharp
public class GenericMethodDemo
{
// 泛型方法:<T> 是方法的类型参数
public static void Swap<T>(ref T a, ref T b)
{
T temp = a;
a = b;
b = temp;
}
}
// 使用泛型方法
class Program
{
static void Main()
{
// 交换int
int x = 1, y = 2;
GenericMethodDemo.Swap(ref x, ref y);
Console.WriteLine($"x={x}, y={y}"); // 输出:x=2, y=1
// 交换string
string s1 = "a", s2 = "b";
GenericMethodDemo.Swap(ref s1, ref s2);
Console.WriteLine($"s1={s1}, s2={s2}"); // 输出:s1=b, s2=a
// 编译时检查:不允许交换不同类型
GenericMethodDemo.Swap(ref x, ref s1); // 编译报错!int和string类型不匹配
}
}
说明:
- 泛型方法的类型参数在方法名后声明(
Swap<T>
); - 调用时可省略类型参数(编译器会自动推断,如
Swap(ref x, ref y)
自动推断T
为int
)。
3. 泛型接口(Generic Interface)
泛型接口允许接口中的方法、属性使用类型参数,解决非泛型接口的类型安全问题(如 IEnumerable
与 IEnumerable<T>
)。
示例:定义通用的"比较"接口
csharp
// 泛型比较接口:比较两个T类型的对象
public interface IComparable<T>
{
int CompareTo(T other); // 参数为T类型,避免object转换
}
// 实现泛型接口:自定义Person类支持比较年龄
public class Person : IComparable<Person>
{
public string Name { get; set; }
public int Age { get; set; }
// 实现CompareTo:比较当前对象与另一个Person的年龄
public int CompareTo(Person other)
{
if (other == null) return 1; // 自身不为null,比null大
return Age.CompareTo(other.Age); // 利用int的CompareTo
}
}
// 使用泛型接口
class Program
{
static void Main()
{
Person p1 = new Person { Name = "张三", Age = 20 };
Person p2 = new Person { Name = "李四", Age = 25 };
int result = p1.CompareTo(p2);
Console.WriteLine(result); // 输出:-1(p1年龄 < p2年龄)
}
}
优势:
- 避免非泛型接口(如
IComparable
)中参数为object
导致的类型转换和装箱问题; - 编译时确保比较的是同类型对象,更安全。
4. 泛型委托(Generic Delegate)
泛型委托允许委托引用"具有任意类型参数的方法",.NET 内置了多个常用泛型委托(如 Func<T>
、Action<T>
)。
示例:自定义泛型委托和使用内置泛型委托
csharp
// 1. 自定义泛型委托:接收T类型参数,返回void
public delegate void MyAction<T>(T item);
// 2. 使用自定义泛型委托
public class DelegateDemo
{
public static void PrintInt(int num) => Console.WriteLine($"整数:{num}");
public static void PrintString(string str) => Console.WriteLine($"字符串:{str}");
}
// 3. 使用.NET内置泛型委托(Func<T, TResult>:有返回值;Action<T>:无返回值)
class Program
{
static void Main()
{
// 自定义泛型委托
MyAction<int> intAction = DelegateDemo.PrintInt;
intAction(100); // 输出:整数:100
MyAction<string> strAction = DelegateDemo.PrintString;
strAction("hello"); // 输出:字符串:hello
// 内置Func<T, TResult>:接收int,返回string
Func<int, string> intToString = num => num.ToString();
string result = intToString(123);
Console.WriteLine(result); // 输出:123
// 内置Action<T>:接收string,无返回值
Action<string> log = msg => Console.WriteLine($"日志:{msg}");
log("操作完成"); // 输出:日志:操作完成
}
}
三、泛型约束(Generic Constraints)
默认情况下,泛型类型参数 T
可以是任何类型(值类型、引用类型、null等),但有时需要限制 T
的范围(如"只能是引用类型""必须实现某个接口"),此时需使用泛型约束 (通过 where
关键字)。
泛型约束有以下6种常见类型:
1. where T : struct
(值类型约束)
限制 T
必须是非可空值类型 (如 int
、DateTime
,不能是 string
或自定义类)。
csharp
public class ValueTypeDemo<T> where T : struct // T必须是值类型
{
public T GetDefault()
{
return default(T); // 值类型的默认值(如int默认0)
}
}
// 使用
var demo = new ValueTypeDemo<int>(); // 正确:int是值类型
Console.WriteLine(demo.GetDefault()); // 输出:0
// var error = new ValueTypeDemo<string>(); // 编译报错:string是引用类型,不满足struct约束
2. where T : class
(引用类型约束)
限制 T
必须是引用类型 (如 string
、自定义类、接口)。
csharp
public class ReferenceTypeDemo<T> where T : class // T必须是引用类型
{
public void SetNull(ref T item)
{
item = null; // 引用类型可赋值为null
}
}
// 使用
var demo = new ReferenceTypeDemo<string>(); // 正确:string是引用类型
string s = "hello";
demo.SetNull(ref s);
Console.WriteLine(s == null); // 输出:True
// var error = new ReferenceTypeDemo<int>(); // 编译报错:int是值类型,不满足class约束
3. where T : new()
(无参构造函数约束)
限制 T
必须有公共无参构造函数 (配合其他约束时,new()
必须放在最后)。
csharp
public class NewConstraintDemo<T> where T : new() // T必须有公共无参构造函数
{
public T CreateInstance()
{
return new T(); // 调用无参构造函数创建实例
}
}
// 符合约束的类(有公共无参构造函数)
public class Person
{
public string Name { get; set; }
public Person() { } // 无参构造函数
}
// 使用
var demo = new NewConstraintDemo<Person>();
Person p = demo.CreateInstance(); // 成功创建Person实例
// 不符合约束的类(无无参构造函数)
public class Student
{
public Student(int id) { } // 只有带参构造函数
}
// var error = new NewConstraintDemo<Student>(); // 编译报错:Student无无参构造函数
4. where T : 基类名
(基类约束)
限制 T
必须是"指定基类"或其派生类。
csharp
// 基类
public class Animal { public string Name { get; set; } }
// 派生类
public class Dog : Animal { public void Bark() { } }
// 基类约束:T必须是Animal或其派生类
public class AnimalDemo<T> where T : Animal
{
public void PrintName(T animal)
{
Console.WriteLine(animal.Name); // 可直接访问基类的属性
}
}
// 使用
var dogDemo = new AnimalDemo<Dog>(); // 正确:Dog是Animal的派生类
dogDemo.PrintName(new Dog { Name = "旺财" }); // 输出:旺财
// var error = new AnimalDemo<string>(); // 编译报错:string不是Animal的派生类
5. where T : 接口名
(接口约束)
限制 T
必须实现"指定接口"。
csharp
// 接口
public interface IFly { void Fly(); }
// 实现接口的类
public class Bird : IFly { public void Fly() => Console.WriteLine("鸟在飞"); }
// 接口约束:T必须实现IFly接口
public class FlyDemo<T> where T : IFly
{
public void LetFly(T flyer)
{
flyer.Fly(); // 直接调用接口方法
}
}
// 使用
var demo = new FlyDemo<Bird>(); // 正确:Bird实现了IFly
demo.LetFly(new Bird()); // 输出:鸟在飞
// var error = new FlyDemo<Dog>(); // 编译报错:Dog未实现IFly接口
6. where T : U
(另一个类型参数约束)
限制 T
必须是"另一个类型参数 U
"或其派生类(用于多类型参数场景)。
csharp
public class TypeParamConstraint<T, U> where T : U // T必须是U或其派生类
{
public U Convert(T value)
{
return value; // T可隐式转换为U
}
}
// 使用
// 情况1:T=Dog,U=Animal(Dog是Animal的派生类,满足约束)
var demo1 = new TypeParamConstraint<Dog, Animal>();
Animal animal = demo1.Convert(new Dog { Name = "旺财" });
// 情况2:T=Animal,U=Dog(Animal是Dog的基类,不满足约束)
// var demo2 = new TypeParamConstraint<Animal, Dog>(); // 编译报错
四、泛型的协变与逆变(高级特性)
在泛型接口和委托中,可通过 out
(协变)和 in
(逆变)关键字,允许"派生类型向基类型"的隐式转换,提高灵活性。
1. 协变(Covariance):out T
允许将"泛型类型参数为派生类"的接口/委托,隐式转换为"参数为基类"的接口/委托(只读场景 ,只能返回 T
,不能接收 T
作为参数)。
csharp
// 协变接口:用out关键字标记T
public interface ICovariant<out T>
{
T GetItem(); // 允许返回T(只读)
// void SetItem(T item); // 错误:协变接口不能接收T作为参数(写操作)
}
// 实现协变接口
public class CovariantImplementation<T> : ICovariant<T>
{
private T _item;
public CovariantImplementation(T item) => _item = item;
public T GetItem() => _item;
}
// 使用协变
class Program
{
static void Main()
{
// 派生类实例(Dog是Animal的派生类)
ICovariant<Dog> dogCovariant = new CovariantImplementation<Dog>(new Dog { Name = "旺财" });
// 协变:ICovariant<Dog> 可隐式转换为 ICovariant<Animal>
ICovariant<Animal> animalCovariant = dogCovariant;
Animal animal = animalCovariant.GetItem(); // 正确:返回Dog(是Animal的派生类)
Console.WriteLine(animal.Name); // 输出:旺财
}
}
.NET 中典型的协变接口:IEnumerable<out T>
(可将 IEnumerable<Dog>
转换为 IEnumerable<Animal>
)。
2. 逆变(Contravariance):in T
允许将"泛型类型参数为基类"的接口/委托,隐式转换为"参数为派生类"的接口/委托(只写场景 ,只能接收 T
作为参数,不能返回 T
)。
csharp
// 逆变接口:用in关键字标记T
public interface IContravariant<in T>
{
void Process(T item); // 允许接收T作为参数(只写)
// T GetItem(); // 错误:逆变接口不能返回T(读操作)
}
// 实现逆变接口
public class ContravariantImplementation<T> : IContravariant<T>
{
public void Process(T item)
{
Console.WriteLine($"处理{typeof(T).Name}:{item.ToString()}");
}
}
// 使用逆变
class Program
{
static void Main()
{
// 基类实例(Animal是Dog的基类)
IContravariant<Animal> animalContravariant = new ContravariantImplementation<Animal>();
// 逆变:IContravariant<Animal> 可隐式转换为 IContravariant<Dog>
IContravariant<Dog> dogContravariant = animalContravariant;
dogContravariant.Process(new Dog { Name = "旺财" }); // 正确:用处理Animal的逻辑处理Dog
// 输出:处理Animal:Dog(假设Dog重写了ToString)
}
}
.NET 中典型的逆变接口:IComparer<in T>
(可将 IComparer<Animal>
转换为 IComparer<Dog>
)。
五、泛型的优点总结
- 代码复用 :一份代码支持多种类型,避免重复开发(如
List<T>
可存储任何类型,无需为每个类型写List)。 - 类型安全 :编译时检查类型,避免运行时类型转换错误(相比
object
类型)。 - 性能优化:值类型使用泛型无需装箱拆箱(直接操作栈内存),减少内存分配和类型转换开销。
- 灵活性:通过泛型约束和协变/逆变,在保证安全的同时提高代码灵活性。
总结
泛型是 C# 中核心的类型系统特性,通过类型参数实现了"通用代码+类型安全"的平衡。无论是日常开发中的集合(List<T>
、Dictionary<TKey, TValue>
)、工具方法,还是框架设计(如 EF Core、依赖注入),泛型都无处不在。掌握泛型的用法和约束,能显著提升代码质量和开发效率。