c# 泛型的详细介绍
在 C# 中,泛型(Generics) 是一种允许在定义类、接口、方法、委托时使用“未指定类型”(类型参数),在使用时再指定具体类型的技术。它的核心价值是:用一份代码支持多种数据类型,同时保证类型安全(编译时检查),避免传统方法中因使用 object
类型导致的装箱拆箱性能损耗和类型转换错误。
一、为什么需要泛型?(传统方法的问题)
在泛型出现前,若要实现“支持多种类型的通用逻辑”(如集合、工具方法),通常有两种方案,但都存在明显缺陷:
1. 为每种类型写重复代码(代码冗余)
例如,实现一个“整数栈”和“字符串栈”,逻辑完全相同但类型不同,必须重复编码:
// 整数栈
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
会装箱(堆内存分配),取出时拆箱(类型转换),损耗性能。
// 用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)
泛型类是最常用的泛型形式,在类定义时声明类型参数,实例化时指定具体类型。
示例:用泛型实现通用栈(解决上述两个问题)
// 泛型栈类:T是类型参数(表示“待指定的类型”)
public class GenericStack<T> // <T> 声明类型参数
{private T[] _items; // 用T作为数组元素类型private int _index = 0;// 构造函数:初始化数组大小public GenericStack(int capacity){_items = new T[capacity]; // 创建T类型的数组}// 入栈:参数类型为Tpublic void Push(T item){if (_index < _items.Length)_items[_index++] = item;elsethrow new StackOverflowException("栈已满");}// 出栈:返回类型为Tpublic T Pop(){if (_index > 0)return _items[--_index];elsethrow 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(); // 无需类型转换,直接返回intConsole.WriteLine(num); // 输出:20// 2. 字符串栈(指定T为string)var strStack = new GenericStack<string>(5);strStack.Push("hello");strStack.Push("world");string str = strStack.Pop(); // 直接返回stringConsole.WriteLine(str); // 输出:world// 3. 类型安全:编译时检查,不允许混合类型intStack.Push("not int"); // 编译报错!无法将string转换为int}
}
优势:
- 一份代码支持任意类型(
int
、string
、自定义类等); - 编译时检查类型,避免运行时错误;
- 值类型(如
int
)无需装箱拆箱,性能更优。
2. 泛型方法(Generic Method)
泛型方法是在方法级别声明类型参数,可独立于泛型类使用(即非泛型类中也可定义泛型方法)。
示例:实现通用的“交换两个变量”方法
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(){// 交换intint x = 1, y = 2;GenericMethodDemo.Swap(ref x, ref y);Console.WriteLine($"x={x}, y={y}"); // 输出:x=2, y=1// 交换stringstring 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>
)。
示例:定义通用的“比较”接口
// 泛型比较接口:比较两个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>
)。
示例:自定义泛型委托和使用内置泛型委托
// 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); // 输出:整数:100MyAction<string> strAction = DelegateDemo.PrintString;strAction("hello"); // 输出:字符串:hello// 内置Func<T, TResult>:接收int,返回stringFunc<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
或自定义类)。
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
、自定义类、接口)。
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()
必须放在最后)。
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
必须是“指定基类”或其派生类。
// 基类
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
必须实现“指定接口”。
// 接口
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
”或其派生类(用于多类型参数场景)。
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
作为参数)。
// 协变接口:用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
)。
// 逆变接口:用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、依赖注入),泛型都无处不在。掌握泛型的用法和约束,能显著提升代码质量和开发效率。