C# 中关于 T 泛型【C# 基础】
C# 中关于 T 泛型【C# 基础】
〇、前言
C# 里面的泛型不仅可以使用泛型函数、泛型接口,也可以使用泛型类、泛型委托等等。在使用泛型的时候,它们会自行检测你传入参数的类型,因此它可以为我们省去大量的时间,不用一个个编写方法的重载。与此同时,使用泛型会提升程序的效率。
本文将围绕泛型的各个方面,详细看下泛型到底怎么用,会给每位开发者带来什么便利。
一、泛型类型参数和运行时中的泛型
1.1 泛型类型参数
在泛型类型或方法定义中,类型参数是在其创建泛型类型的一个实例时,客户端指定的特定类型的占位符。
泛型类(例如泛型介绍中列出的 GenericList<T>)无法按原样使用,因为它不是真正的类型,它更像是类型的蓝图。
若要使用 GenericList<T>,客户端代码必须通过指定尖括号内的类型参数来声明并实例化构造类型。此特定类的类型参数可以是编译器可识别的任何类型。
可创建任意数量、使用不同的类型参数的构造类型实例。如下示例,创建 GenericList<T> 的三个不同类型的实例:
GenericList<float> list1 = new GenericList<float>(); | |
GenericList<ExampleClass> list2 = new GenericList<ExampleClass>(); | |
GenericList<ExampleStruct> list3 = new GenericList<ExampleStruct>(); |
在 GenericList<T> 的每个实例中,类中出现的每个 T 在运行时均会被替换为类型参数。通过这种替换,我们已通过使用单个类定义创建了三个单独的类型安全的有效对象。
1.2 泛型类型的命名
当泛型类型允许用任意类代替,且仅有一个泛型类型时,就可以用字符T
作为泛型类型的名称。如下示例:
public int IComparer<T>() { return 0; } | |
public delegate bool Predicate<T>(T item); | |
public struct Nullable<T> where T : struct { /*...*/ } |
如果泛型类型存在多个,为了避免混淆,建议给类型参数描述性名称加上字符T
做前缀,加以区分。如下示例:
public interface ISessionChannel<TSession> { /*...*/ } | |
public delegate TOutput Converter<TInput, TOutput>(TInput from); | |
public class List<T> { /*...*/ } |
下面是一个简单的示例,泛型 TSession 的一个实现,实际类型为 Test。
public interface ISessionChannel<TSession> // 泛型类型 TSession | |
{ | |
TSession Session { get; } | |
} | |
public class Test : ISessionChannel<Test> // 类 Test 作为泛型类型的实际类型 | |
{ | |
public int MyProperty { get; set; } | |
public Test Session => new Test() { MyProperty = 1 }; | |
} |
https://learn.microsoft.com/zh-cn/dotnet/csharp/programming-guide/generics/generic-type-parameters
1.3 运行时中的泛型
泛型类型或方法编译为 Microsoft 中间语言(MSIL)时,它包含将其标识为具有类型参数的元数据。值类型和引用类型的泛型在 MSIL 编译过程中是有区别的,下面来介绍一下区别在哪里。
当首次构造泛型类型,使用值类型作为参数时:
运行时会为泛型类型创建专用空间,MSIL 执行过程中会在合适的位置,替换传入的一个或多个参数。为每种用作参数的类型,创建专用化泛型类型。
下面例举个示例:
// 首先,声明了一个由整数构造的堆栈 | |
// 运行时生成一个专用版 Stack<T> 类,其中用整数相应地替换其参数 | |
Stack<int>? stack; | |
// 每当程序代码使用整数堆栈时,运行时都重新使用已生成的专用 Stack<T> 类 | |
// 在下面的示例中创建了两个整数堆栈实例 | |
// 由于它们【都是 int 类型,所以共用 Stack<int> 代码的一个实例】 | |
Stack<int> stackOne = new Stack<int>(); | |
Stack<int> stackTwo = new Stack<int>(); | |
// 假定在代码中另一点上再创建一个将不同值类型(例如 long 或用户定义结构)作为参数的 Stack<T> 类 | |
// 则,此时运行时在 MSIL 中,会【生成另一个版本的泛型类型】并在适当位置替换 long | |
Stack<long> stackTwo = new Stack<long>(); |
当首次构造泛型类型,使用引用类型作为参数时:
运行时创建一个专用化泛型类型,用对象引用替换 MSIL 中的参数。之后,每次使用引用类型作为参数实例化已构造的类型时,无论何种类型,运行时皆重新使用先前创建的专用版泛型类型。原因很简单,因为对实例的引用是类似的,可以存放在同一泛化类型中。
下面也例举个简单的示例:
// 先声明两个类 | |
class Customer { } | |
class Order { } | |
// 再声明一个 Customer 类型的堆栈 | |
// 此时,运行时生成一个专用的 Stack<T> 类,此类中会被填入引用类型值的引用,而不是实际数据 | |
Stack<Customer> customers; | |
// 下面再创建另一类型 Order 的堆栈 | |
// 虽然不同于 Customer 类型的堆栈,但是 MSIL 也不会再为 Order 类型的堆栈创建新的 Stack<T> 类 | |
// 而是使用之前创建的专用的 Stack<T> 类的实例,将 orders 变量的引用加入新的实例中 | |
Stack<Order> orders = new Stack<Order>(); | |
// 假定之后遇到一行创建 Customer 类型堆栈的代码 | |
customers = new Stack<Customer>(); | |
// 此时的处理方式相同,再创建一个 Stack<T> 类的一个实例 |
由于引用类型的数量因程序不同而有较大差异,因此通过将编译器为引用类型的泛型类,创建的专用类的数量减少至 1,这样泛型的 C# 实现,可极大减少代码量。
使用值类型或引用类型参数,实例化泛型 C# 类时,反射可在运行时对其进行查询,且其实际类型和类型参数皆可被确定。
https://learn.microsoft.com/zh-cn/dotnet/csharp/programming-guide/generics/generics-in-the-run-time
1.4 为什么要约束类型参数?
在没有任何约束的情况下,类型参数可以是任何类型。编译器只能假定 System.Object 的成员,它是任何 .NET 类型的最终基类。如果客户端代码使用不满足约束的类型,编译器将发出错误。
可以通过使用 where 上下文关键字指定约束。
下面列举一下总共 12 种约束类型:
约束 | 描述 |
where T : struct | 类型参数必须是不可为 null 的值类型。由于所有值类型都具有可访问的无参数构造函数,因此 struct 约束表示 new() 约束,并且不能与 new() 约束结合使用。struct 约束也不能与 unmanaged 约束结合使用。 |
where T : class | 类型参数必须是引用类型。此约束还应用于任何类、接口、委托或数组类型。在可为 null 的上下文中,T 必须是不可为 null 的引用类型。 |
where T : class? | 类比上一条,增加了可为 null 的情形。 |
where T : notnull | 类型参数必须是不可为 null 的值类型或引用类型。 |
where T : default | 重写方法或提供显式接口实现时,如果需要指定不受约束的类型参数,此约束可解决歧义。default 约束表示基方法,但不包含 class 或 struct 约束。 |
where T : unmanaged | 类型参数必须是不可为 null 的非托管类型。unmanaged 约束表示 struct 约束,且不能与 struct 约束或 new() 约束结合使用。 |
where T : new() | 类型参数必须具有公共无参数构造函数。 与其他约束一起使用时,new() 约束必须最后指定。 new() 约束不能与 struct 和 unmanaged 约束结合使用。 |
where T : <基类名> | 类型参数必须是指定的基类或派生自指定的基类。在可为 null 的上下文中,T 必须是从指定基类派生的不可为 null 的引用类型。 |
where T : <基类名>? | 类比上一条,增加了基类派生的可为 null 的引用类型。 |
where T : <接口名称> | 类型参数必须是指定的接口或实现指定的接口。可指定多个接口约束。约束接口也可以是泛型。在的可为 null 的上下文中,T 必须是实现指定接口的不可为 null 的类型。 |
where T : <接口名称>? | 类比上一条,增加了实现指定接口的可为 null 的类型。 |
where T : U | 为 T 提供的类型参数必须是为 U 提供的参数或派生自为 U 提供的参数。在可为 null 的上下文中: 如果 U 是不可为 null 的引用类型,T 必须是不可为 null 的引用类型。 如果 U 是可为 null 的引用类型,则 T 可以是可为 null 的引用类型,也可以是不可为 null 的引用类型。 |
那到底为啥要用约束呢?
首先,声明上面表中这些约束,意味着你可以放心的执行所约束类型的操作和方法。就好比你和几个朋友约饭订位置,肯定要提前说好都有谁,不然大概率会出现空座或坐不下的异常情况。
如果泛型类或方法,对泛型成员使用除简单赋值之外的其他操作,或者调用 System.Object 不支持的任何方法,则将对类型参数应用约束,不然易引发异常。
例如,基类约束告诉编译器,仅此类型的对象或派生自此类型的对象可用作类型参数。编译器有了此保证后,就能够允许在泛型类中调用该类型的方法。
下面示例代码,使用基类约束:
public class Employee // 基类声明 | |
{ | |
public Employee(string name, int id) => (Name, ID) = (name, id); | |
public string Name { get; set; } // 基类中包含 Name 属性 | |
public int ID { get; set; } | |
} | |
public class GenericList<T> where T : Employee // 约束泛型参数 T 类的基类是 Employee | |
{ | |
private class Node // 私有类 Node 中的属性包含对泛型 T 类的操作 | |
{ | |
public Node(T t) => (Next, Data) = (null, t); | |
public Node? Next { get; set; } | |
public T Data { get; set; } | |
} | |
private Node? head; | |
public void AddHead(T t) | |
{ | |
Node n = new Node(t) { Next = head }; | |
head = n; | |
} | |
public IEnumerator<T> GetEnumerator() | |
{ | |
Node? current = head; | |
while (current != null) | |
{ | |
yield return current.Data; | |
current = current.Next; | |
} | |
} | |
public T? FindFirstOccurrence(string s) | |
{ | |
Node? current = head; | |
T? t = null; | |
while (current != null) | |
{ | |
// 此处可以放心的访问基类 Employee 中的 Name 属性 | |
if (current.Data.Name == s) | |
{ | |
t = current.Data; | |
break; | |
} | |
else | |
{ | |
current = current.Next; | |
} | |
} | |
return t; | |
} | |
} |
另外,也可以对同一类型参数应用多个约束,并且约束自身可以是泛型类型。如下代码,多条件用逗号分隔:
class EmployeeList<T> where T : Employee, IEmployee, System.IComparable<T>, new() | |
{ | |
// ... | |
} |
在应用 where T : class 约束时,必须避免对类型参数使用 == 和 != 运算符,因为这些运算符仅测试引用标识而不测试值相等性。如果必须测试值相等性,建议同时应用 where T : IEquatable<T> 或 where T : IComparable<T> 约束,并在用于构造泛型类的任何类中实现该接口。
上面说了如何对一个参数应用多个约束,下面看下对多个参数都进行约束怎么写:
class Base { } | |
class Test<T, U> | |
where U : struct | |
where T : new() | |
{ } |
未绑定类型的参数,就无法进行对比,因为不知道它到底是值类型还是引用类型,但肯定都属于 System.Object。
另外再看几个示例:
// 类型参数可以作为约束,如下 | |
public class List<T> | |
{ | |
public void Add<U>(List<U> items) where U : T {/*...*/} // 仅约束添加的 U 对象,对 items 中的 U 无效 | |
} | |
// 类型参数可在泛型类定义中用作约束 | |
public class SampleClass<T, U, V> where T : V { } | |
// 约束 T 不可为空 | |
public class List<T> where T : notnull | |
{ | |
// ... | |
} | |
// 使用 unmanaged 约束来指定类型参数必须是不可为 null 的非托管类型 | |
// 通过 unmanaged 约束,用户能编写可重用例程,从而使用可作为内存块操作的类型 | |
unsafe public static byte[] ToByteArray<T>(this T argument) where T : unmanaged | |
{ | |
var size = sizeof(T); // 注:sizeof 运算符必须用在已知的内置类型上,此处前提是 where T : unmanaged | |
var result = new Byte[size]; | |
Byte* p = (byte*)&argument; | |
for (var i = 0; i < size; i++) | |
result[i] = *p++; | |
return result; | |
} | |
// 使用 System.Delegate 约束,就能以类型安全的方式编写使用委托的代码 | |
public static TDelegate? TypeSafeCombine<TDelegate>(this TDelegate source, TDelegate target) | |
where TDelegate : System.Delegate | |
=> Delegate.Combine(source, target) as TDelegate; | |
// 可指定 System.Enum 类型作为枚举类型的基类约束 | |
public static Dictionary<int, string> EnumNamedValues<T>() where T : System.Enum | |
{ | |
var result = new Dictionary<int, string>(); | |
// Enum.GetValues、Enum.GetName 使用反射,会对性能产生影响,调用 EnumNamedValues 来生成可缓存和重用的集合来避免使用反射 | |
// var values = Enum.GetValues(typeof(T)); | |
var values = EnumNamedValues<Rainbow>(); | |
foreach (int item in values) | |
result.Add(item.Key, item.Value); | |
// result.Add(item, Enum.GetName(typeof(T), item)!); | |
return result; | |
} | |
enum Rainbow | |
{ | |
Red, | |
Orange, | |
Yellow, | |
Green, | |
Blue, | |
Indigo, | |
Violet | |
} |
二、泛型类
泛型类是 C# 语言中一种强大的特性,它允许在定义类时,使用类型参数来表示其中的某些成员。通过使用泛型类,我们可以编写更通用、可复用的代码,以适应不同类型的数据。
泛型类最常见用法是用于链接列表、哈希表、堆栈、队列和树等集合。无论存储数据的类型如何,添加项和从集合删除项等操作的执行方式基本相同。
class Program | |
{ | |
static void Main(string[] args) | |
{ | |
// 创建一个整数类型的栈 | |
Stack<int> intStack = new Stack<int>(3); | |
intStack.Push(10); | |
intStack.Push(20); | |
intStack.Push(30); | |
intStack.Push(40); | |
Console.WriteLine($"出栈元素:{intStack.Pop()}"); | |
Console.WriteLine($"出栈元素:{intStack.Pop()}"); | |
Console.WriteLine(); | |
// 创建一个字符串类型的栈 | |
Stack<string> stringStack = new Stack<string>(3); | |
stringStack.Push("Hello"); | |
stringStack.Push("World"); | |
Console.WriteLine($"出栈元素:{stringStack.Pop()}"); | |
Console.WriteLine(); | |
// 创建一个自定义类型的栈 | |
Stack<Person> personStack = new Stack<Person>(2); | |
Person p1 = new Person("John", 25); | |
Person p2 = new Person("Alice", 30); | |
personStack.Push(p1); | |
personStack.Push(p2); | |
Console.WriteLine($"出栈元素:{personStack.Pop()}"); | |
Console.WriteLine($"出栈元素:{personStack.Pop()}"); | |
Console.WriteLine($"出栈元素:{personStack.Pop()}"); | |
Console.ReadLine(); | |
} | |
} | |
// 自定义一个Person类 | |
public class Person | |
{ | |
public string Name { get; set; } | |
public int Age { get; set; } | |
public Person(string name, int age) | |
{ | |
Name = name; | |
Age = age; | |
} | |
public override string ToString() | |
{ | |
return string.Format("Name: {0}, Age: {1}", Name, Age); | |
} | |
} | |
// 定义一个泛型类 | |
public class Stack<T> | |
{ | |
private T[] elements; | |
private int top; | |
public Stack(int size) | |
{ | |
elements = new T[size]; | |
top = -1; | |
} | |
public void Push(T item) | |
{ | |
if (top >= elements.Length - 1) | |
{ | |
Console.WriteLine("栈已满,无法入栈"); | |
return; | |
} | |
elements[++top] = item; | |
} | |
public T Pop() | |
{ | |
if (top < 0) | |
{ | |
Console.WriteLine("栈为空,无法出栈"); | |
return default(T); | |
} | |
T item = elements[top--]; | |
return item; | |
} | |
} |
输出结果:
在上述示例代码中,我们创建了一个泛型类 Stack<T>,它表示一个简单的栈数据结构。通过使用类型参数 T,我们可以将 Stack<T> 应用于不同类型的数据。
在 Main 方法中,我们分别创建了整数类型、字符串类型和自定义类型(Person)的栈,并对其进行了一些入栈和出栈操作。由于使用了泛型类,我们可以在编译时指定栈中存储的元素类型,并在运行时处理相应类型的数据。
这个示例代码也展示了泛型类的诸多好处,例如:
- 可以重复使用同一个泛型类来处理不同类型的数据,提高代码的复用性。
- 在编译时进行类型检查,避免了类型转换错误和运行时异常。
- 提供了更好的代码可读性和维护性,因为我们可以在泛型类中使用具有描述性的类型参数名称。
详情可参考:https://www.cnblogs.com/dotnet261010/p/9034594.html
三、泛型接口
泛型接口是 C# 语言中的另一个强大特性,它允许在定义接口时使用类型参数来表示其中的某些成员。通过使用泛型接口,可以定义通用的接口规范,以适应不同类型的实现。
以下示例代码是对泛型接口的一个简单的应用:
// 测试一下 | |
class Program | |
{ | |
static void Main(string[] args) | |
{ | |
IRepository<User> userRepository = new UserRepository(3); | |
User user1 = new User(1, "John"); | |
User user2 = new User(2, "Alice"); | |
User user3 = new User(3, "Bob"); | |
userRepository.Add(user1); // 添加用户信息 | |
userRepository.Add(user2); | |
userRepository.Add(user3); | |
User retrievedUser = userRepository.GetById(2); | |
Console.WriteLine("Retrieved user: {0}", retrievedUser.Name); | |
userRepository.Delete(user2); // 删除用户 2 | |
retrievedUser = userRepository.GetById(2); | |
if (retrievedUser != null) // 删除后再去查询就返回 null | |
Console.WriteLine("Retrieved user: {0}", retrievedUser.Name); | |
} | |
} | |
// 定义一个泛型接口 | |
public interface IRepository<T> | |
{ | |
void Add(T item); | |
void Delete(T item); | |
T GetById(int id); | |
} | |
// 实现泛型接口的具体类 | |
public class UserRepository : IRepository<User> | |
{ | |
private User[] users; | |
private int count; | |
public UserRepository(int size) | |
{ | |
users = new User[size]; | |
count = 0; | |
} | |
public void Add(User user) | |
{ | |
if (count >= users.Length) | |
{ | |
Console.WriteLine("仓库已满,无法添加用户"); | |
return; | |
} | |
users[count++] = user; | |
} | |
public void Delete(User user) | |
{ | |
int index = Array.IndexOf(users, user); | |
if (index < 0) | |
{ | |
Console.WriteLine("用户不存在"); | |
return; | |
} | |
for (int i = index; i < count - 1; i++) | |
{ | |
users[i] = users[i + 1]; | |
} | |
count--; | |
} | |
public User GetById(int id) | |
{ | |
foreach (User user in users) | |
{ | |
if (user.Id == id) | |
return user; | |
} | |
Console.WriteLine($"找不到 id 为 {id} 的用户"); | |
return null; | |
} | |
} | |
// 自定义 User 类 | |
public class User | |
{ | |
public int Id { get; set; } | |
public string Name { get; set; } | |
public User(int id, string name) | |
{ | |
Id = id; | |
Name = name; | |
} | |
} |
结果输出:
在上述示例代码中,首先定义了一个泛型接口 IRepository<T>,其中包含了一些通用的增删查等操作方法。然后实现了该接口的具体类 UserRepository,用于管理用户对象的存储和操作。
通过使用泛型接口,我们可以在编译时指定接口中的类型参数,使得 IRepository<T> 和 UserRepository 能够处理特定类型的数据(这里是 User 类),后续有新的类时,可以直接实现接口,然后基本操作类似,有助于团队协作开发。
在 Main 方法中,我们创建了一个 UserRepository 对象,并对其进行了一些添加、删除和查询操作。由于使用了泛型接口,我们可以保证在调用接口方法时传入正确的数据类型,并且在编译时进行类型检查。
由示例代码可以看到,泛型接口也具备许多好处,例如:
- 可以定义通用的接口规范,可以被多个类和方法重用,从而减少代码重复。
- 在编译时进行类型检查,避免了类型转换错误和运行时异常,也减少类型转换的开销。
- 提供了更好的代码可读性和维护性,因为我们可以在泛型接口中使用具有描述性的类型参数名称,还可以帮助开发人员更好地理解代码的用途和行为。
- 可以提高代码的灵活性和可扩展性。通过使用泛型接口,可以在不修改代码的情况下,轻松地添加新的类型或修改现有类型的属性和方法。
四、泛型方法
泛型方法是通过类型参数声明的方法。它允许在方法定义时不指定具体的数据类型,而是在调用方法时根据需要传入实际的类型。如下示例:
// 声明一个泛型方法 | |
static void Swap<T>(ref T lhs, ref T rhs) | |
{ | |
T temp; | |
temp = lhs; | |
lhs = rhs; | |
rhs = temp; | |
} | |
// 声明一个泛型方法,将输入的两个泛型实例值对调 | |
public static void TestSwap() | |
{ | |
int a = 1; | |
int b = 2; | |
Swap<int>(ref a, ref b); | |
Console.WriteLine(a + " " + b); // 输出:2 1 | |
} |
还可省略类型参数,编译器将推断类型参数。 如下 Swap 调用等效于之前的调用:
Swap(ref a, ref b); |
类型推理的相同规则适用于静态方法和实例方法。
编译器可基于传入的方法参数推断类型参数;而无法仅根据约束或返回值推断类型参数,因此,类型推理不适用于不具有参数的方法。
如果定义一个具有与当前类相同的类型参数的泛型方法,则编译器会生成警告 CS0693,因为在该方法范围内,向内 T 提供的参数会隐藏向外 T 提供的参数。如果需要使用类型参数(而不是类实例化时提供的参数)调用泛型类方法,可以考虑为此方法的类型参数提供另一标识符,如下示例中 GenericList2<T> 所示。
class GenericList<T> | |
{ | |
// CS0693. | |
void SampleMethod<T>() { } | |
} | |
class GenericList2<T> | |
{ | |
// No warning. | |
void SampleMethod<U>() { } | |
} |
使用约束在方法中的类型参数上实现更多专用操作。此版 Swap<T> 现名为 SwapIfGreater<T>,仅可用于实现 IComparable<T> 的类型参数。
void SwapIfGreater<T>(ref T lhs, ref T rhs) where T : IComparable<T> | |
{ | |
T temp; | |
if (lhs.CompareTo(rhs) > 0) | |
{ | |
temp = lhs; | |
lhs = rhs; | |
rhs = temp; | |
} | |
} |
泛型方法可重载在数个泛型参数上。 例如,以下方法可全部位于同一类中:
void DoWork() { } | |
void DoWork<T>() { } | |
void DoWork<T, U>() { } |
五、泛型与数组
下限为零的单维数组自动实现 IList<T>,这意味着可以使用相同代码,循环访问数组和其他集合类型的泛型方法。 IList<T> 接口无法用于添加元素或从数组删除元素,若尝试对数组调用 IList<T> 方法(例如 RemoveAt),则会引发异常。
static void Main() | |
{ | |
int[] arr = { 0, 1, 2, 3, 4 }; | |
List<int> list = new List<int>(); | |
for (int x = 5; x < 10; x++) | |
{ | |
list.Add(x); | |
} | |
ProcessItems<int>(arr); | |
ProcessItems<int>(list); | |
Console.ReadLine(); | |
} | |
static void ProcessItems<T>(IList<T> coll) | |
{ | |
System.Console.WriteLine("IsReadOnly : {0} .",coll.IsReadOnly); | |
//coll.RemoveAt(4); // System.NotSupportedException: 'Collection was of a fixed | |
foreach (T item in coll) | |
{ | |
System.Console.Write(item?.ToString() + " "); | |
} | |
System.Console.WriteLine(); | |
} |
六、泛型委托
委托可以定义它自己的类型参数。引用泛型委托的代码可以指定类型参数以创建封闭式构造类型,就像实例化泛型类或调用泛型方法一样,如以下示例中所示:
public delegate void Del<T>(T item); | |
public static void Notify(int i) { } | |
Del<int> m1 = new Del<int>(Notify); | |
// C# 2.0 版具有一种称为方法组转换的新功能,适用于具体委托类型和泛型委托类型,因此上一行代码可简化为: | |
Del<int> m2 = Notify; |
在泛型类中定义的委托,可以和类方法以相同方式来使用泛型类的类型参数。
class Stack<T> | |
{ | |
public delegate void StackDelegate(T[] items); | |
} |
引用委托的代码,必须指定所包含类的类型参数,如下所示:
private static void DoWork(float[] items) | |
{ | |
Console.WriteLine("执行工作"); | |
} | |
public static void TestStack() | |
{ | |
Stack<float> s = new Stack<float>(); // 泛型类型参数 float 来指定栈中存储的元素类型为浮点数 | |
Stack<float>.StackDelegate d = DoWork; | |
// 调用委托引用的方法 | |
float[] array = { 1.0f, 2.0f, 3.0f }; | |
d(array); | |
} |
如下示例代码,定义了一个泛型委托 AddDelegate<T>,它接受两个类型为T的参数并返回一个 T 类型的结果。Calculator 类中定义了三个静态方法 Add,分别用于整数、浮点数和双精度浮点数的加法运算。在主函数中,我们创建了三个不同类型的泛型委托实例,并使用这些委托引用了 Calculator 类中的相应方法。然后,我们通过调用委托实例来执行加法运算,并将结果打印到控制台上。
// 声明一个委托 | |
public delegate T AddDelegate<T>(T a, T b); | |
public class Calculator // 计算加法类 | |
{ | |
public static int Add(int a, int b) | |
{ | |
return a + b; | |
} | |
public static float Add(float a, float b) | |
{ | |
return a + b; | |
} | |
public static double Add(double a, double b) | |
{ | |
return a + b; | |
} | |
} | |
public class Program | |
{ | |
public static void Main(string[] args) | |
{ | |
// 分别以不同的类型将 Add() 方法添加到委托实例的引用 | |
AddDelegate<int> intAddDelegate = Calculator.Add; | |
int sum1 = intAddDelegate(5, 10); | |
Console.WriteLine("Sum of integers: " + sum1); | |
AddDelegate<float> floatAddDelegate = Calculator.Add; | |
float sum2 = floatAddDelegate(3.14f, 2.78f); | |
Console.WriteLine("Sum of floats: " + sum2); | |
AddDelegate<double> doubleAddDelegate = Calculator.Add; | |
double sum3 = doubleAddDelegate(2.5, 3.7); | |
Console.WriteLine("Sum of doubles: " + sum3); | |
Console.ReadLine(); | |
} | |
} |
结果输出:
详情可参考:https://learn.microsoft.com/zh-cn/dotnet/csharp/programming-guide/generics/generic-delegates
七、小小的总结
由以上的介绍,可以看到泛型在 C# 语言中是一个非常强大的特性,总体看来它有一下几点好处:
- 首先就是代码的重用。没有泛型的话,我们需要声明一个父类,然后有几个类型再继承出来几个子类,这样就非常麻烦。如果用上泛型那么就可以写一遍搞定,也提高了代码的可维护性和可扩展性。
- 然后就是类型安全。泛型提供了编译时类型检查的好处,这意味着编译器可以在编译时捕获并阻止不匹配类型的错误。这有助于减少运行时错误,并增加代码的健壮性。
- 还有可以性能优化。泛型可以提供更高的性能,因为它们在编译时生成特定类型的代码。相比于使用非泛型的代码,泛型可以避免装箱和拆箱操作,从而提高代码的执行效率。
- 可以将类型参数化。通过使用泛型,可以将类型作为参数传递给类、方法或委托,从而使代码更加灵活和可配置。这样可以实现更高级别的抽象和模块化。
- 最后还可以进行安全约束。使用泛型,可以对泛型类型进行 where 约束,限制其可以接受的类型。这可以帮助我们确保代码只能在特定类型上运行,并提供更严格的类型检查。
泛型在 C# 中提供了更加灵活、安全和高效的编程方式。它可以提高代码的可重用性、可维护性和可扩展性,同时还能够减少错误并提高性能。
因此,在合适的情况下,使用泛型是一个非常好的选择。