【.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 中的参数。 之后,每次使用引用类型作为参数实例化已构造的类型时,无论何种类型,运行时皆重新使用先前创建的专用版泛型类型。 原因可能在于所有引用大小相同。

目录
相关文章
|
6天前
|
开发框架 .NET 开发者
简化 ASP.NET Core 依赖注入(DI)注册-Scrutor
Scrutor 是一个简化 ASP.NET Core 应用程序中依赖注入(DI)注册过程的开源库,支持自动扫描和注册服务。通过简单的配置,开发者可以轻松地从指定程序集中筛选、注册服务,并设置其生命周期,同时支持服务装饰等高级功能。适用于大型项目,提高代码的可维护性和简洁性。仓库地址:&lt;https://github.com/khellang/Scrutor&gt;
24 5
|
2月前
|
存储 开发框架 JSON
ASP.NET Core OData 9 正式发布
【10月更文挑战第8天】Microsoft 在 2024 年 8 月 30 日宣布推出 ASP.NET Core OData 9,此版本与 .NET 8 的 OData 库保持一致,改进了数据编码以符合 OData 规范,并放弃了对旧版 .NET Framework 的支持,仅支持 .NET 8 及更高版本。新版本引入了更快的 JSON 编写器 `System.Text.UTF8JsonWriter`,优化了内存使用和序列化速度。
|
24天前
|
开发框架 .NET C#
在 ASP.NET Core 中创建 gRPC 客户端和服务器
本文介绍了如何使用 gRPC 框架搭建一个简单的“Hello World”示例。首先创建了一个名为 GrpcDemo 的解决方案,其中包含一个 gRPC 服务端项目 GrpcServer 和一个客户端项目 GrpcClient。服务端通过定义 `greeter.proto` 文件中的服务和消息类型,实现了一个简单的问候服务 `GreeterService`。客户端则通过 gRPC 客户端库连接到服务端并调用其 `SayHello` 方法,展示了 gRPC 在 C# 中的基本使用方法。
35 5
在 ASP.NET Core 中创建 gRPC 客户端和服务器
|
14天前
|
开发框架 缓存 .NET
GraphQL 与 ASP.NET Core 集成:从入门到精通
本文详细介绍了如何在ASP.NET Core中集成GraphQL,包括安装必要的NuGet包、创建GraphQL Schema、配置GraphQL服务等步骤。同时,文章还探讨了常见问题及其解决方法,如处理复杂查询、错误处理、性能优化和实现认证授权等,旨在帮助开发者构建灵活且高效的API。
23 3
|
3月前
|
开发框架 监控 前端开发
在 ASP.NET Core Web API 中使用操作筛选器统一处理通用操作
【9月更文挑战第27天】操作筛选器是ASP.NET Core MVC和Web API中的一种过滤器,可在操作方法执行前后运行代码,适用于日志记录、性能监控和验证等场景。通过实现`IActionFilter`接口的`OnActionExecuting`和`OnActionExecuted`方法,可以统一处理日志、验证及异常。创建并注册自定义筛选器类,能提升代码的可维护性和复用性。
|
3月前
|
开发框架 .NET 中间件
ASP.NET Core Web 开发浅谈
本文介绍ASP.NET Core,一个轻量级、开源的跨平台框架,专为构建高性能Web应用设计。通过简单步骤,你将学会创建首个Web应用。文章还深入探讨了路由配置、依赖注入及安全性配置等常见问题,并提供了实用示例代码以助于理解与避免错误,帮助开发者更好地掌握ASP.NET Core的核心概念。
111 3
|
2月前
|
开发框架 JavaScript 前端开发
一个适用于 ASP.NET Core 的轻量级插件框架
一个适用于 ASP.NET Core 的轻量级插件框架
|
3月前
|
开发框架 NoSQL .NET
利用分布式锁在ASP.NET Core中实现防抖
【9月更文挑战第5天】在 ASP.NET Core 中,可通过分布式锁实现防抖功能,仅处理连续相同请求中的首个请求,其余请求返回 204 No Content,直至锁释放。具体步骤包括:安装分布式锁库如 `StackExchange.Redis`;创建分布式锁服务接口及其实现;构建防抖中间件;并在 `Startup.cs` 中注册相关服务和中间件。这一机制有效避免了短时间内重复操作的问题。
|
4月前
|
开发框架 监控 .NET
开发者的革新利器:ASP.NET Core实战指南,构建未来Web应用的高效之道
【8月更文挑战第28天】本文探讨了如何利用ASP.NET Core构建高效、可扩展的Web应用。ASP.NET Core是一个开源、跨平台的框架,具有依赖注入、配置管理等特性。文章详细介绍了项目结构规划、依赖注入配置、中间件使用及性能优化方法,并讨论了安全性、可扩展性以及容器化的重要性。通过这些技术要点,开发者能够快速构建出符合现代Web应用需求的应用程序。
68 0
|
4月前
|
缓存 数据库连接 API
Entity Framework Core——.NET 领域的 ORM 利器,深度剖析其最佳实践之路
【8月更文挑战第28天】在软件开发领域,高效的数据访问与管理至关重要。Entity Framework Core(EF Core)作为一款强大的对象关系映射(ORM)工具,在 .NET 开发中扮演着重要角色。本文通过在线书店应用案例,展示了 EF Core 的核心特性和优势。我们定义了 `Book` 实体类及其属性,并通过 `BookStoreContext` 数据库上下文配置了数据库连接。EF Core 提供了简洁的 API,支持数据的查询、插入、更新和删除操作。
127 0