C# 里面的泛型不仅可以使用泛型函数、泛型接口,也可以使用泛型类、泛型委托等等。在使用泛型的时候,它们会自行检测你传入参数的类型,因此它可以为我们省去大量的时间,不用一个个编写方法的重载。与此同时,使用泛型会提升程序的效率。
本文将围绕泛型的各个方面,详细看下泛型到底怎么用,会给每位开发者带来什么便利。
在泛型类型或方法定义中,类型参数是在其创建泛型类型的一个实例时,客户端指定的特定类型的占位符。
泛型类(例如泛型介绍中列出的 GenericList
若要使用 GenericList
可创建任意数量、使用不同的类型参数的构造类型实例。如下示例,创建 GenericList
GenericList<float> list1 = new GenericList<float>();
GenericList<ExampleClass> list2 = new GenericList<ExampleClass>();
GenericList<ExampleStruct> list3 = new GenericList<ExampleStruct>();
在 GenericList
当泛型类型允许用任意类代替,且仅有一个泛型类型时,就可以用字符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
泛型类型或方法编译为 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
在没有任何约束的情况下,类型参数可以是任何类型。编译器只能假定 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
上面说了如何对一个参数应用多个约束,下面看下对多个参数都进行约束怎么写:
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
在 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
通过使用泛型接口,我们可以在编译时指定接口中的类型参数,使得 IRepository
在 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
class GenericList<T>
{
// CS0693.
void SampleMethod<T>() { }
}
class GenericList2<T>
{
// No warning.
void SampleMethod<U>() { }
}
使用约束在方法中的类型参数上实现更多专用操作。此版 Swap
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
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
// 声明一个委托
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# 语言中是一个非常强大的特性,总体看来它有一下几点好处:
泛型在 C# 中提供了更加灵活、安全和高效的编程方式。它可以提高代码的可重用性、可维护性和可扩展性,同时还能够减少错误并提高性能。
因此,在合适的情况下,使用泛型是一个非常好的选择。
手机扫一扫
移动阅读更方便
你可能感兴趣的文章