【.NET Core】泛型(Generics)详解

简介: 笔记

一、概述


泛型是为所存储或使用的一个或多个类型具有占位符(类型形参)的类、结构、接口和方法。泛型集合类可以将类型形参用作其存储的对象的占位符;类型形参程序为字段的类型或其方法的参数类型。泛型方法可将其类型形参用作其返回值的类型或用作其形参之一的类型。


为了方便理解,我们用ArrayList为例,在.NET Framework1.0中,ArrayList元素属于Object类型。添加到集合的任何元素都会以静默方式转换为Object。自此过程中会发生装箱和拆箱的过程,在装箱和拆箱的类型转换过程中,会影响性能。这个是因为在编译的时候无法确认数据的类型,数据的类型只能在运行阶段确定,这个过程就导致性能消耗。为了解决这个问题微软在NET Framework 2.0中首次引入了这个泛型,它本质上是一个"代码模板",让开发人定义类型安全的数据结构,这样就避免了在装箱和拆箱过程性能损失,或在运行中的异常。


下面我们演示一下非泛型和泛型性能的差异。

List<int> ListGeneric = new List<int> { 5, 9, 1, 4 };
ArrayList ListNonGeneric = new ArrayList { 5, 9, 1, 4 };
Stopwatch s = Stopwatch.StartNew();
ListGeneric.Sort();
s.Stop();
Console.WriteLine($"Generic Sort: {ListGeneric}  \n Time taken: {s.Elapsed.TotalMilliseconds}ms");
Stopwatch s2 = Stopwatch.StartNew();
ListNonGeneric.Sort();
s2.Stop();
Console.WriteLine($"Non-Generic Sort: {ListNonGeneric}  \n Time taken: {s2.Elapsed.TotalMilliseconds}ms");
Console.ReadLine();

运行结果

Generic Sort: System.Collections.Generic.List`1[System.Int32]
Time taken: 0.0119ms
Non-Generic Sort: System.Collections.ArrayList
Time taken: 0.2944ms

从运行结果我们可以看出装箱和拆箱的过程中性能损失挺大。


二、泛型类型参数


类型参数是在其创建泛型类型的一个实例时,客户端指定的特定类型的占位符。泛型类在定义以后,无法直接使用,必须指定真正的类型后,才能使用。每个类型必须通过指定尖括号内的类型参数来声明并实例化构造类型。此特定的类型一定是编译器可识别的任何类型。


实例如下:

GenericList<float> list1 = new GenericList<float>();
GenericList<ExampleClass> list2 = new GenericList<ExampleClass>();
GenericList<ExampleStruct> list3 = new GenericList<ExampleStruct>();

在GenericList的每个实例中,类中出现的每个T在运行时均会被替换为类型参数。通过这种替换,通过使用单个类定义创建了三个单独的类型安全的有效对象


三、泛型中类型参数的约束


约束告知编译器类型参数必须具备的功能。在没有任何约束的情况下,类型参数可以是任何类型。编译器只能预设为System.Object的成员,System.Object类型是任何类型的基类。如果在使用泛型时,不能满足约束的类型,编译器将会发生错误。通过使用where关键字指定约束。


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

3.1 where T:struct

类型参数必须是不可为null的值类型,由于所有值类型都具有可访问的无参数构造函数,因此struct约束表示 new()约束,并且不能与new()约束一起使用。struct 约束也不能与 unmanaged 约束结合使用。

public class GenericsStructCLS<T> where T : struct
{
}

3.2 where T:class

类型参数必须是引用类型,此约束还应用于任何类、接口、委托或数组类型。在可为null的上下文中,T必须是不可为null的引用类型。

public class GenericsClassCLS<T> where T : class
{
}

3.3 where T:class?

类型参数必须是为null或不可为null的引用类型。此约束应该用于任何类,接口、委托或数组类型。

public class GenericsClassNullCLS<T> where T : class?
{
}

3.4 where T:notnull

类型参数必须是不可为null的类型。参数可以是不可为null的引用类型,也可以是不可为null的值类型。

public class GenericsClassNotNullCLS<T> where T : notnull
{
}

3.5 where T:default

重写方法或提供显示接口实现时,如果需要指定不受约束的类型参数,此约束可解决歧义。default约束表示基方法,但不包含class或struct约束。

public class GenericsClassDefault<T> where T:default
{
}

default 约束表示基方法,但不包含 class 或 struct 约束

3.6 where T:unmanaged

类型参数必须是不可为null的非托管类型。unmanaged约束表示structe约束,且不能与struct约束或new()约束结合使用。

public class GenericsClassUnmanagedCLS<T> where T : unmanaged
{
}

3.7 where T:new()

类型参数必须具有公共无参数构造函数。 与其他约束一起使用时,new() 约束必须最后指定。 new() 约束不能与 struct 和 unmanaged 约束结合使用。

public class GenericsClassNewCLS<T> where T : new()
{
    //类方法
}

3.8 where T:<基类名>

类型参数必须是指定的基类或派生自指定的基类。在可为null的上下文中,T必须是从指定基类派生的不可为null的引用类型。

public class Base{}
public class GenericsClassBaseCLS<T> where T : Base 
{
    //类方法
}

3.9 where T:<基类名>?

类型参数必须是指定的基类或派生自指定的基类。 在可为 null 的上下文中,T 可以是从指定基类派生的可为 null 或不可为 null 的类型。

public class Base{}
public class GenericsClassBaseCLS<T> where T : Base? 
{
    //类方法
}

3.10 where T:<接口名称>

类型参数必须是指定的接口或实现指定的接口。 可指定多个接口约束。 约束接口也可以是泛型。 在的可为 null 的上下文中,T 必须是实现指定接口的不可为 null 的类型。

public interface IBase { }
public class GenericsClass<T> where T : IBase 
{
    //类方法
}

3.11 where T:<接口名称>?

类型参数必须是指定的接口或实现指定的接口。 可指定多个接口约束。 约束接口也可以是泛型。 在可为 null 的上下文中,T 可以是可为 null 的引用类型、不可为 null 的引用类型或值类型。 T 不能是可为 null 的值类型。

public interface IBase { }
public class GenericsClass<T> where T : IBase? 
{
    //类方法
}

3.12 where T:U

为 T 提供的类型参数必须是为 U 提供的参数或派生自为 U 提供的参数。 在可为 null 的上下文中,如果 U 是不可为 null 的引用类型,T 必须是不可为 null 的引用类型。 如果 U 是可为 null 的引用类型,则 T 可以是可为 null 的引用类型,也可以是不可为 null 的引用类型。

public class BaseClass<T> { }
public class UTClass<T> where T: BaseClass<T> 
{
    //类方法
}

3.13 对参数应用多个约束

public class UTClass<T> where T: BaseClass<T> ,IBase,new()
{
}

3.14 约束多个参数

不但可以对参数应用多个约束,也可以对多个参数应用多个约束。

class Base { }
class Test<T, U>
    where U : struct
    where T : Base, new()
{
    //类方法
}


四、泛型类


泛型类封装不特定于特定类型的操作。泛型类最常见用法是用于链接列表,哈希表、堆栈、队列和树等集合。无论存储数据的类型如何,添加项和从集合删除项等操作的执行方式基本相同。


创建自己的泛型类时,需要考虑以下重要注意事项:


  • 要将哪些类型泛化为类型参数
  • 如何给泛型类添加参数约束
  • 是否将泛型行为分解基类和子类
  • 实现一个泛型接口还是多个泛型接口
class BaseNode { }
class BaseNodeGeneric<T> { }
class NodeConcrete<T> : BaseNode { }
class NodeClosed<T> : BaseNodeGeneric<int> { }
class NodeOpen<T> : BaseNodeGeneric<T> { }

泛型类的特性

1.非泛型类可继承自封闭式构造基类,但不可继承自开放式构造类或类型参数;

2.泛型类继承自开放构造类型的泛型类必须保持参数相同;

3.泛型类型可使用多个类型参数和约束;

4.开放式构造和封闭式构造类型可用作方法参数;


五、泛型接口


为避免对值类型执行装箱和拆箱操作,最好对泛型类使用泛型接口。.NET类库定义多个泛型接口,以便用于System.Collections.Generic命名空间中的集合类。


泛型接口提供与非泛型接口对应的类型安全接口,用于实现排序比较,相等比较以及泛型集合类型所共享的功能。

public class GenericList<T>:System.Collections.Generic.IEnumerable<T>
{
    //方法体
}

泛型接口可将多个接口指定微单个类型上的约束。

class Stack<T> where T:System.IComparable<T>,IEnumerable<T>
{
    //方法体
}

一个接口可定义多个类型参数:

interface IDictionary<K,V>
{
    //方法体
}

泛型类既可实现泛型接口或封闭式构造接口。

interface IBaseInterface1<T> { }
interface IBaseInterface2<T,U> { }
class SampleClass1<T> : IBaseInterface1<T> { }
class SampleClass2<T> : IBaseInterface2<T, string> { }


从C# 11开始,接口可以声明static abstract或static virtual成员。声明任一static abstract或static virtual成员的接口几乎始终是泛型接口。编译器必须在编译时解析对 static virtual 和 static abstract 方法的调用。 接口中声明的 static virtual 和 static abstract 方法没有类似于类中声明的 virtual 或 abstract 方法的运行时调度机制。 相反,编译器使用编译时可用的类型信息。 这些成员通常是在泛型接口中声明的。


六、泛型方法


泛型方法是通过类型参数声明的方法。如下所示:

public  void Swap<T>(ref T ins, ref T ot) 
{
    T temp;
    temp=ins;
    ins = ot;
    ot=temp;
}

如果定义一个具有与包含类相同的类型参数的泛型方法,编译器会生成警告CS0693( 警CS0693类型参数“T”与外部类型“GenericsMethodClass”中的类型参数同名 )。如果需要使用类型参数调用泛型类方法所具备的灵活性,可以考虑为此方法的类型参数提供另一标识符。

class GenericList<T>
{
    //CS0693.
    void SampleMethod<T>(){}
}
class GenericList2<T>
{
    //No warning.
    void SampleMethod<U>(){}
}

6.1 泛型方法约束

void SwapIfGreater<T>(ref T lhs, ref T rhs) where T : System.IComparable<T>
{
    T temp;
    if (lhs.CompareTo(rhs) > 0)
    {
        temp = lhs;
        lhs = rhs;
        rhs = temp;
    }
}

6.2 泛型方法重载

void DoWork(){}
void DoWork<T>(){}
void DoWork<T,U>(){}


七、泛型委托


委托可以定义它自己的类型参数。引用泛型委托的代码可以指定类型参数以创建封闭式构造类型。

示例如下:

public delegate T DelMetho<T>(T item);
public static int Notify(int a) { return a; }
public static string Notify1(string b) { return b; }
DelMetho<int> metho = new DelMetho<int>(Notify);
DelMetho<string> method = new DelMetho<string>(Notify1);

C#2.0版具有一种称为方法组转换的新功能,使用于具体委托类型和泛型委托类型,能简化语法编写:

public delegate T DelMetho<T>(T item);
public static int Notify(int a) { return a; }
public static string Notify1(string b) { return b; }
DelMetho<int> metho =Notify;


八、运行时中的泛型


泛型类型或方法编译为MSIL时,它包含将其标识为具有类型参数的元数据。MSIL根据所提供的类型参数是值类型还是引用类型而有不同。


8.1 值类型

使用值类型作为参数首次构造泛型类型时,运行时创建专用的泛型类型,MSIL内的适当位置替换提供的一个或多个参数。为每个用参数的唯一值类型一次创建专用化泛型类型。


8.2 引用类型

引用类型,泛型的作用方式略有不同。首先使用任意引用类型构造泛型类型时,运行时创建一个专用化泛型类型,用对象引用替换 MSIL 中的参数。 之后,每次使用引用类型作为参数实例化已构造的类型时,无论何种类型,运行时皆重新使用先前创建的专用版泛型类型。 原因可能在于所有引用大小相同。

目录
相关文章
|
13天前
|
数据可视化 网络协议 C#
C#/.NET/.NET Core优秀项目和框架2024年3月简报
公众号每月定期推广和分享的C#/.NET/.NET Core优秀项目和框架(每周至少会推荐两个优秀的项目和框架当然节假日除外),公众号推文中有项目和框架的介绍、功能特点、使用方式以及部分功能截图等(打不开或者打开GitHub很慢的同学可以优先查看公众号推文,文末一定会附带项目和框架源码地址)。注意:排名不分先后,都是十分优秀的开源项目和框架,每周定期更新分享(欢迎关注公众号:追逐时光者,第一时间获取每周精选分享资讯🔔)。
|
3月前
|
开发框架 前端开发 JavaScript
盘点72个ASP.NET Core源码Net爱好者不容错过
盘点72个ASP.NET Core源码Net爱好者不容错过
71 0
|
3月前
|
开发框架 .NET
ASP.NET Core NET7 增加session的方法
ASP.NET Core NET7 增加session的方法
37 0
|
3月前
|
开发框架 JavaScript .NET
ASP.NET Core的超级大BUG
ASP.NET Core的超级大BUG
41 0
|
3月前
|
开发框架 前端开发 .NET
ASP.NET CORE 3.1 MVC“指定的网络名不再可用\企图在不存在的网络连接上进行操作”的问题解决过程
ASP.NET CORE 3.1 MVC“指定的网络名不再可用\企图在不存在的网络连接上进行操作”的问题解决过程
40 0
|
1月前
|
开发框架 人工智能 .NET
C#/.NET/.NET Core拾遗补漏合集(持续更新)
C#/.NET/.NET Core拾遗补漏合集(持续更新)
|
1月前
|
开发框架 中间件 .NET
C# .NET面试系列七:ASP.NET Core
## 第一部分:ASP.NET Core #### 1. 如何在 controller 中注入 service? 在.NET中,在ASP.NET Core应用程序中的Controller中注入服务通常使用<u>依赖注入(Dependency Injection)</u>来实现。以下是一些步骤,说明如何在Controller中注入服务: 1、创建服务 首先,确保你已经在应用程序中注册了服务。这通常在Startup.cs文件的ConfigureServices方法中完成。例如: ```c# services.AddScoped<IMyService, MyService>(); //
61 0
|
1月前
|
开发框架 安全 .NET
C# .NET面试系列三:集合、异常、泛型、LINQ、委托、EF!
<h2>集合、异常、泛型、LINQ、委托、EF! #### 1. IList 接口与 List 的区别是什么? IList 接口和 List 类是C#中集合的两个相关但不同的概念。下面是它们的主要区别: <b>IList 接口</b> IList 接口是C#中定义的一个泛型接口,位于 System.Collections 命名空间。它派生自 ICollection 接口,定义了一个可以通过索引访问的有序集合。 ```c# IList 接口包含一系列索引化的属性和方法,允许按索引访问、插入、移除元素等。 由于是接口,它只定义了成员的契约,而不提供具体的实现。类似于 IEnumera
151 2
|
2月前
|
开发框架 前端开发 .NET
福利来袭,.NET Core开发5大案例,30w字PDF文档大放送!!!
为了便于大家查找,特将之前开发的.Net Core相关的五大案例整理成文,共计440页,32w字,免费提供给大家,文章底部有PDF下载链接。
33 1
福利来袭,.NET Core开发5大案例,30w字PDF文档大放送!!!
|
2月前
|
算法 BI API
C#/.NET/.NET Core优秀项目和框架2024年1月简报
C#/.NET/.NET Core优秀项目和框架2024年1月简报