C#入门到精通——1_泛型Generic

为什么要有泛型?

我们在编写程序时,经常遇到两个模块的功能非常相似,只是一个是处理int数据,另一个是处string数据,或者其他自定义的数据类型,但我们没有办法,只能分别写多个方法处理每个数据类型,因为方法的参数类型不同。

 

 

 你可能会想到用object,来解决这个问题。但是,缺陷的:
1.会出现装箱、折箱操作,这将在托管堆上分配和回收大量的变量,若数据量大,则性能损失非常严重。
2.在处理引用类型时,虽然没有装箱和折箱操作,但将用到数据类型的强制转换操作,增加处理器的负担。 

一、介绍泛型

C#中的泛型能够将类型作为参数来传递,即在创建类型时用一个特定的符号如“T”来作为一个占位符,代替实际的类型,等待在实例化时用一个实际的类型来代替。

泛型的优点 :

一、使用泛型类型可以最大限度地重用代码、保护类型的安全以及提高性能。
二、降低了强制转换或装箱操作的成本或风险。
三、可以对泛型类进行约束以访问特定数据类型的方法。

泛型的限制:

void MyFunc<T>(T x, T y)
{
x = x + y; //报错!泛型的变量不能使用运算符’+’
}

泛型成员因类型不确定,可能是类、结构体、字符、枚举……所以不能使用算术运算符、比较运算符等进行运算!
注意,可以使用赋值运算符。

二、泛型类型参数:

在泛型类型或方法定义中,类型参数是客户端在实例化泛型类型的变量时指定的特定类型的占位符。

注意:
1.类型参数并不是只有一个,可以有多个。
2.类型参数可以是编译器识别的任何类型。
3.类型参数的名字不能随便起,不能重名。

类型参数的约束: 

在定义泛型类时,可以对客户端代码能够在实例化类时用于类型参数的类型种类施加限制。 如果客户端代码尝试使用某个约束所不允许的类型来实例化类,则会产生编译时错误。 这些限制称为约束。 约束是使用 where 上下文关键字指定的。

 

 

下表列出了各种类型的约束:

使用约束的原因:

约束指定类型参数的功能和预期。 声明这些约束意味着你可以使用约束类型的操作和方法调用。 如果泛型类或方法对泛型成员使用除简单赋值之外的任何操作或调用 System.Object 不支持的任何方法,则将对类型参数应用约束。 例如,基类约束告诉编译器,仅此类型的对象或派生自此类型的对象可用作类型参数。 编译器有了此保证后,就能够允许在泛型类中调用该类型的方法。 以下代码示例演示可通过应用基类约束添加到(泛型介绍中的)GenericList<T> 类的功能。

复制代码
 1 public class Employee
 2 {
 3     public Employee(string name, int id) => (Name, ID) = (name, id);
 4     public string Name { get; set; }
 5     public int ID { get; set; }
 6 }
 7  
 8 public class GenericList<T> where T : Employee
 9 {
10     private class Node
11     {
12         public Node(T t) => (Next, Data) = (null, t);
13  
14         public Node Next { get; set; }
15         public T Data { get; set; }
16     }
17  
18     private Node head;
19  
20     public void AddHead(T t)
21     {
22         Node n = new Node(t) { Next = head };
23         head = n;
24     }
25  
26     public IEnumerator<T> GetEnumerator()
27     {
28         Node current = head;
29  
30         while (current != null)
31         {
32             yield return current.Data;
33             current = current.Next;
34         }
35     }
36  
37     public T FindFirstOccurrence(string s)
38     {
39         Node current = head;
40         T t = null;
41  
42         while (current != null)
43         {
44             //The constraint enables access to the Name property.
45             if (current.Data.Name == s)
46             {
47                 t = current.Data;
48                 break;
49             }
50             else
51             {
52                 current = current.Next;
53             }
54         }
55         return t;
56     }
57 }
复制代码

约束使泛型类能够使用 Employee.Name 属性。 约束指定类型 T 的所有项都保证是 Employee 对象或从 Employee 继承的对象。

可以对同一类型参数应用多个约束,并且约束自身可以是泛型类型,如下所示:

 

1 class EmployeeList<T> where T : Employee, IEmployee, System.IComparable<T>, new()
2 {
3     // ...
4 }

在应用 where T : class 约束时,请避免对类型参数使用 == 和 != 运算符,因为这些运算符仅测试引用标识而不测试值相等性。 即使在用作参数的类型中重载这些运算符也会发生此行为。 下面的代码说明了这一点;即使 String 类重载 == 运算符,输出也为 false。

复制代码
 1 public static void OpEqualsTest<T>(T s, T t) where T : class
 2 {
 3     System.Console.WriteLine(s == t);
 4 }
 5  
 6 private static void TestStringEquality()
 7 {
 8     string s1 = "target";
 9     System.Text.StringBuilder sb = new System.Text.StringBuilder("target");
10     string s2 = sb.ToString();
11     OpEqualsTest<string>(s1, s2);
12 }
复制代码

编译器只知道 T 在编译时是引用类型,并且必须使用对所有引用类型都有效的默认运算符。 如果必须测试值相等性,建议同时应用 where T : IEquatable<T> 或 where T : IComparable<T> 约束,并在用于构造泛型类的任何类中实现该接口。

约束多个参数 :

可以对多个参数应用多个约束,对一个参数应用多个约束,如下例所示:

1 class Base { }
2 class Test<T, U>
3     where U : struct
4     where T : Base, new()
5 { }

未绑定的类型参数:

没有约束的类型参数(如公共类 SampleClass<T>{} 中的 T)称为未绑定的类型参数。 未绑定的类型参数具有以下规则:

不能使用 != 和 == 运算符,因为无法保证具体的类型参数能支持这些运算符。
可以在它们与 System.Object 之间来回转换,或将它们显式转换为任何接口类型。
可以将它们与 null 进行比较。 将未绑定的参数与 null 进行比较时,如果类型参数为值类型,则该比较将始终返回 false。

类型参数作为约束:

在具有自己类型参数的成员函数必须将该参数约束为包含类型的类型参数时,将泛型类型参数用作约束非常有用,如下例所示:

1 public class List<T>
2 {
3     public void Add<U>(List<U> items) where U : T {/*...*/}
4 }

在上述示例中,T 在 Add 方法的上下文中是一个类型约束,而在 List 类的上下文中是一个未绑定的类型参数。

类型参数还可在泛型类定义中用作约束。 必须在尖括号中声明该类型参数以及任何其他类型参数:

  1 //Type parameter V is used as a type constraint.

2 public class SampleClass<T, U, V> where T : V { } 

 类型参数作为泛型类的约束的作用非常有限,因为编译器除了假设类型参数派生自 System.Object 以外,不会做其他任何假设。 如果要在两个类型参数之间强制继承关系,可以将类型参数用作泛型类的约束。

notnull 约束:
从 C# 8.0 开始,可以使用 notnull 约束指定类型参数必须是不可为 null 的值类型或不可为 null 的引用类型。 与大多数其他约束不同,如果类型参数违反 notnull 约束,编译器会生成警告而不是错误。

notnull 约束仅在可为 null 上下文中使用时才有效。 如果在过时的可为 null 上下文中添加 notnull 约束,编译器不会针对违反约束的情况生成任何警告或错误。

class 约束:
从 C# 8.0 开始,可为 null 上下文中的 class 约束指定类型参数必须是不可为 null 的引用类型。 在可为 null 上下文中,当类型参数是可为 null 的引用类型时,编译器会生成警告。

default 约束:
添加可为空引用类型会使泛型类型或方法中的 T? 使用复杂化。 在 C# 8 之前,只能在向 T 应用了 struct 约束的情况下使用 T?。 在该上下文中,T? 引用 T 的 Nullable<T> 类型。 从 C# 8 开始,T? 可以与 struct 或 class 约束一起使用,但必须存在其中一项。 使用 class 约束时,T? 引用了 T 的可为空引用类型。 从 C# 9 开始,可在这两个约束均未应用时使用 T?。 在这种情况下,T? 的解读与在 C# 8 中对值类型和引用类型的解读相同。 但是,如果 T 是 Nullable<T>的实例,则 T? 与 T 相同。 换句话说,它不会成为 T??。

由于现在可在没有 class 或 struct 约束的情况下使用 T?,因此在重写或显式接口实现中可能会出现歧义。 在这两种情况下,重写不包含约束,但从基类继承。 当基类不应用 class 或 struct 约束时,派生类需要通过某种方式在不使用任一种约束的情况下指定应用于基方法的重写。 此时派生方法将应用 default 约束。 default 约束不阐明 class 和 struct 约束。

非托管约束:

从 C# 7.3 开始,可使用 unmanaged 约束来指定类型参数必须是不可为 null 的非托管类型。 通过 unmanaged 约束,用户能编写可重用例程,从而使用可作为内存块操作的类型,如以下示例所示: 

复制代码
unsafe public static byte[] ToByteArray<T>(this T argument) where T : unmanaged
{
    var size = sizeof(T);
    var result = new Byte[size];
    Byte* p = (byte*)&argument;
    for (var i = 0; i < size; i++)
        result[i] = *p++;
    return result;
} 
复制代码

版权声明:本文为CSDN博主「此生不悔入海贼」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。

原文链接:https://blog.csdn.net/LiKe11807/article/details/120892392

posted @   Zhongxingxing  阅读(32)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?
点击右上角即可分享
微信分享提示