c# 泛型的详细介绍

在 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
    }
}

优势

  • 一份代码支持任意类型(intstring、自定义类等);
  • 编译时检查类型,避免运行时错误;
  • 值类型(如 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) 自动推断 Tint)。
3. 泛型接口(Generic Interface)

泛型接口允许接口中的方法、属性使用类型参数,解决非泛型接口的类型安全问题(如 IEnumerableIEnumerable<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 必须是非可空值类型 (如 intDateTime,不能是 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>)。

五、泛型的优点总结

  1. 代码复用 :一份代码支持多种类型,避免重复开发(如 List<T> 可存储任何类型,无需为每个类型写List)。
  2. 类型安全 :编译时检查类型,避免运行时类型转换错误(相比 object 类型)。
  3. 性能优化:值类型使用泛型无需装箱拆箱(直接操作栈内存),减少内存分配和类型转换开销。
  4. 灵活性:通过泛型约束和协变/逆变,在保证安全的同时提高代码灵活性。

总结

泛型是 C# 中核心的类型系统特性,通过类型参数实现了"通用代码+类型安全"的平衡。无论是日常开发中的集合(List<T>Dictionary<TKey, TValue>)、工具方法,还是框架设计(如 EF Core、依赖注入),泛型都无处不在。掌握泛型的用法和约束,能显著提升代码质量和开发效率。

相关推荐
嵌入式学习和实践6 小时前
C# WinForms 多窗口交互通信的示例-主窗口子窗口交互通信
c#·交互·主窗口-子窗口通信
专注VB编程开发20年6 小时前
C#,VB.NET数组去重复,提取键名和重复键和非重复键
c#·.net·linq·取唯一键·去重复·重复数量
YuanlongWang6 小时前
Entity Framework Core和SqlSugar的区别,详细介绍
c#
unicrom_深圳市由你创科技9 小时前
工业上位机,用Python+Qt还是C#+WPF?
python·qt·c#
偶尔的鼠标人20 小时前
Avalonia DataGrid 控件的LostFocus事件会多次触发
开发语言·c#
ytttr87320 小时前
C# 仿QQ聊天功能实现 (SQL Server数据库)
数据库·oracle·c#
future_studio1 天前
聊聊 Unity(小白专享、C# 小程序 之 图片播放器)
unity·小程序·c#
c#上位机1 天前
wpf中Grid的MouseDown 事件无法触发的原因
c#·wpf
CodeCraft Studio1 天前
国产化PDF处理控件Spire.PDF教程:如何在 C# 中从 HTML 和 PDF 模板生成 PDF
pdf·c#·html·.net·spire.pdf·pdf文档开发·html创建模板pdf