C# 泛型

日之朝矣

泛型(Generics)是类型参数化的机制,允许你在编写类、结构、接口、方法和委托时使用占位符表示的类型。这样可以提高代码的重用性、类型安全性和性能。这种特性广泛应用于类型安全的集合类、算法和数据结构中。

类型参数:泛型中的类型参数是一个占位符,它在类、接口或方法的定义中表示一种未确定的类型。在使用泛型时,可以将具体的类型传递给这些占位符。通常使用 TUV 等字母来表示类型参数。(大多数使用T这个字母是因为Type缩写)

类型安全:泛型提供了类型安全性,编译器会在编译时检查类型一致性,从而避免了许多运行时错误。泛型使得在不需要显式类型转换的情况下,可以灵活地处理不同类型的数据。

代码复用:泛型可以极大地提高代码的复用性,因为相同的泛型类或方法可以适用于不同的数据类型,而不需要为每一种类型单独编写代码。

性能提升:泛型避免了装箱和拆箱,减少了运行时的开销,从而提高了性能。

简化维护:由于泛型可以减少代码的重复性,使得代码更加简洁易读,减少了维护的难度。

基本语法

泛型类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class GenericClass<T>
{
private T data;

public GenericClass(T value)
{
data = value;
}

public T GetData()
{
return data;
}
}

GenericClass<int> intClass = new GenericClass<int>(10);
Console.WriteLine(intClass.GetData()); // 输出: 10

GenericClass<string> stringClass = new GenericClass<string>("Hello");
Console.WriteLine(stringClass.GetData()); // 输出: Hello

泛型方法

1
2
3
4
5
6
7
8
9
10
11
12
public class GenericMethodClass
{
public void Display<T>(T data)
{
Console.WriteLine(data);
}
}

GenericMethodClass genericMethodClass = new GenericMethodClass();
genericMethodClass.Display<int>(123); // 输出: 123
genericMethodClass.Display<string>("Hello"); // 输出: Hello

泛型接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public interface IGenericInterface<T>
{
T GetData();
}

public class GenericInterfaceClass<T> : IGenericInterface<T>
{
private T data;

public GenericInterfaceClass(T value)
{
data = value;
}

public T GetData()
{
return data;
}
}

IGenericInterface<int> intInterface = new GenericInterfaceClass<int>(100);
Console.WriteLine(intInterface.GetData()); // 输出: 100

泛型委托

1
2
3
4
public delegate T GenericDelegate<T>(T item);

GenericDelegate<int> square = x => x * x;
Console.WriteLine(square(5)); // 输出: 25

C# 泛型运行原理

C# 程序在编译时,由编译器将源代码编译成中间语言(IL)代码,并打包成 .exe.dll 文件。对于泛型类型和泛型方法,编译器生成的是一个泛型类型定义(Generic Type Definition),该定义中包含了未具体化的泛型类型参数。

在运行时,当 .NET 运行时中的 JIT 编译器首次遇到一个泛型类型的具体实例化(例如 List<int>),它会基于泛型类型定义生成针对该类型的机器代码。对于值类型,JIT 会为每个不同的值类型生成单独的代码;对于引用类型,JIT 会重用相同的代码。

这种双重编译机制(C# 编译器的编译和 JIT 编译器的即时编译)确保了泛型在运行时的类型安全性和高效性。

泛型约束

可以使用泛型约束来限制泛型类型参数必须满足的条件,例如必须实现某个接口或必须有一个无参数的构造函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
public class GenericClassWithConstraint<T> where T : IComparable, new()
{
public T CreateInstance()
{
return new T();
}

public bool Compare(T item1, T item2)
{
return item1.CompareTo(item2) > 0;
}
}

泛型限定条件:

  • T:struct(类型参数必须是值类型。可以指定除 Nullable 以外的任何值类型)
  • T:class(类型参数必须是引用类型,包括任何类、接口、委托或数组类型)
  • T:new() (类型参数必须具有无参数的公共构造函数。当与其他约束一起使用时new() 约束必须最后指定)
  • T:<基类名> 类型参数必须是指定的基类或派生自指定的基类
  • T:<接口名称> 类型参数必须是指定的接口或实现指定的接口。可以指定多个接口约束。约束接口也可以是泛型的。

协变与逆变

协变(Covariance)和逆变(Contravariance)是编程语言中处理泛型、委托和接口时的一种重要概念,它们决定了如何在继承关系中处理类型的转换。协变和逆变帮助我们在泛型类型中灵活地处理类型参数,使代码更具通用性和重用性。

协变

方向:从子类到父类。

应用场景:主要用于返回值类型的转换。

协变允许你在类型转换时,将一个 泛型类型的派生类型 赋值给 其基类型的泛型类型。协变主要应用于返回类型,即你可以将派生类型的泛型对象赋给基类型的泛型对象。

通过给泛型接口的 类型参数 前添加out关键字来表示T是协变的。(类型参数:即尖括号内的T)

1
2
3
4
5
6
7
public class Animal{}
public class Cat : Animal{}
public interface IConvariant<out T> // 这里,out 关键字表示 T 是协变的。
{
T GetValue();
// void SetValue(T t); 编译错误,将类型参数标记为协变后,其不能再作为方法的参数
}
1
2
IConvariant<Cat> cat = null;
IConvariant<Animal> animal = cat; // 合法,因为 Cat 是 Animal 的子类

通过上面的例子,我们可以发现,ICovariant<Car> 可以赋值给 ICovariant<Animal>,因为 CatAnimal 的子类。

协变限制

当类型参数被标记为协变时,该类型参数只能用于返回值,不能用作方法的输入参数。这是因为协变的方向是从子类到父类,而允许将 T 用作输入参数可能会破坏类型安全。

逆变

方向:从父类到子类。

应用场景:主要用于参数类型的转换。

逆变允许你在类型转换时,将一个 **泛型类型的基类型 **赋值给 其派生类型的泛型类型。逆变主要应用于参数类型,即你可以将基类型的泛型对象赋给派生类型的泛型对象。

1
2
3
4
5
6
7
public class Animal{}
public class Cat : Animal{}
public interface IConvariant<in T> // 这里,in 关键字表示 T 是逆变的。
{
// T GetValue(); 编译错误,将类型参数标记为逆变后,其不能再作为方法的返回值
void SetValue(T t);
}
1
2
IConvariant<Animal> animal = null;
IConvariant<Cat> cat = animal;

通过上面的例子,我们可以发现,IContravariant<Animal> 可以赋值给 IContravariant<Dog>,因为 AnimalDog 的基类。

逆变限制

当类型参数被标记为逆变时,该类型参数只能用作方法的输入参数,不能作为返回值使用。这是因为逆变的方向是从基类到子类,而允许将 T 用作返回值可能会破坏类型安全。

  • 标题: C# 泛型
  • 作者: 日之朝矣
  • 创建于 : 2024-08-10 11:05:14
  • 更新于 : 2024-08-18 09:25:27
  • 链接: https://blog.rzzy.fun/2024/08/10/csharp-generic-type/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论