【科普】.NET6 泛型
本文内容来自我写的开源电子书《WoW C#》,现在正在编写中,可以去WOW-Csharp/学习路径总结.md at master · sogeisetsu/WOW-Csharp (github.com)来查看编写进度。预计2021年年底会完成编写,2022年2月之前会完成所有的校对和转制电子书工作,争取能够在2022年将此书上架亚马逊。编写此书的目的是因为目前.NET市场相对低迷,很多优秀的书都是基于.NET framework框架编写的,与现在的.NET 6相差太大,正规的.NET 5学习教程现在几乎只有MSDN,可是MSDN虽然准确优美但是太过琐碎,没有过阅读开发文档的同学容易一头雾水,于是,我就编写了基于.NET 5的《WoW C#》。本人水平有限,欢迎大家去本书的开源仓库sogeisetsu/WOW-Csharp关注、批评、建议和指导。
泛型(Generic) 允许您延迟编写类或方法中的编程元素的数据类型的规范,直到实际在程序中使用它的时候。换句话说,泛型允许您编写一个可以与任何数据类型一起工作的类或方法。
Why?
泛型将类型参数的概念引入 .NET,这样可以设计一个或多个类型的规范。泛型有更好的性能,并且可以达到类型安全,还能够提升代码复用率。
性能
比如说两个数组的实现方式,一个是ArrayList
,一个是List<T>
。
看一下MSDN的ArrayList
的一段反编译代码:
public virtual int Add(object? value)
{
throw null;
}
可以看到ArrayList
的Add方法的参数是默认值为null的object类型。当我们从ArrayList
中取出数据时,
ArrayList arrayList = new ArrayList();
for (int i = 0; i < 10000; i++)
{
arrayList.Add(i);
}
Console.WriteLine(arrayList[1].GetType());// System.Int32
我们可以清楚的看到存入的数据和取出的数据都是设置好的数据类型(System.Int32),也就是说在存入和取出数据的时候会存在装箱和拆箱的操作,这势必会使性能下降。
类型安全
一个ArrayList
实例化对象可以接受任何的数据类型,可是List<T>
的实例化对象只能够接受指定好的数据类型。这样就保证了传入数据类型的一致,这就是所谓类型安全。
List<int> list = new List<int>();
list.Add(12);
//list.Add("12") error
泛型提升代码复用率
如果没有泛型,那么一个普通类类每涉及一个类型,就要重写类。这个可能说起来比较抽象,可以看一下下面这个demo:
class A
{
public void GetTAndTest(int value)
{
Console.WriteLine(value.GetType());
}
}
类型A的GetTAndTest()
的参数类型仅仅是int类型,如果想要参数为string类型,方法的主体不变,如果没有泛型的话就只能重新写一个方法,如果想参数类型为double呢?那么就必须再重写一个方法……,方法主体没有改变,却因为参数类型的不同而一遍又一遍的重写,这是不合理的。所以要使用泛型,使用了泛型之后就不用再重写这么多次,demo如下:
class A<T>
{
public void GetTAndTest(T value)
{
Console.WriteLine(value.GetType());
}
}
有了泛型之后,当面对不同的参数类型有无限多,方法主体不变的情况时,使用泛型能够有效的提升代码复用率。
泛型类
泛型类封装不特定于特定数据类型的操作。 所谓泛型类就是在创建一个类的时候在后面加一个类似于<T>
的标志。T就是该泛型类能够接受的数据类型。
下面定义一个泛型类:
class A<T>
{
public void GetTAndTest(T value)
{
Console.WriteLine(value.GetType());
Console.WriteLine(typeof(T) == value.GetType());
// System.Int32
// True
}
}
采取类似下面的方法来实例化泛型类A<T>
:
A<int> a = new A<int>();
a.GetTAndTest(12);
继承规则
在将泛型类的继承之前,先说几个名词:
中文 | 英文 | 形式 |
---|---|---|
具体类 | concrete type | BaseNode |
封闭构造类型 | closed constructed type | BaseNodeGeneric<int> |
开方式构造类型 | open constructed type | BaseNodeGeneric<T> |
泛型类可继承自具体的封闭式构造或开放式构造基类。
下面这些都是正确泛型类继承自基类的方式:
class BaseNode { }
class BaseNodeGeneric<T> { }
// concrete type
class NodeConcrete<T> : BaseNode { }
//closed constructed type
class NodeClosed<T> : BaseNodeGeneric<int> { }
//open constructed type
class NodeOpen<T> : BaseNodeGeneric<T> { }
非泛型类(即,具体类)可继承自封闭式构造基类,但不可继承自开放式构造类或类型参数,因为运行时客户端代码无法提供实例化基类所需的类型参数。
//正确
class Node1 : BaseNodeGeneric<int> { }
//错误
//class Node2 : BaseNodeGeneric<T> {}
//错误
//class Node3 : T {}
继承自开放式构造类型的泛型类必须对非此继承类共享的任何基类类型参数提供类型参数。关于泛型的描述总是十分抽象,不易于理解,这里用不严谨的方式来进行解释:在泛型类继承的过程中,基类不能出现不包含在继承类且无具体意义的类型参数。
class BaseNodeMultiple<T, U> { }
//正确
class Node4<T> : BaseNodeMultiple<T, int> { }
//正确
class Node5<T, U> : BaseNodeMultiple<T, U> { }
//错误,U既不是泛型类的泛型参数,也无具体指向某一个类
//class Node6<T> : BaseNodeMultiple<T, U> {}
泛型方法
泛型方法是通过类型参数声明的方法,demo如下:
class FanXing
{
public List<Object> ListObj { get; set; }
/// <summary>
/// 泛型方法
/// </summary>
/// <typeparam name="T">类型参数,示意任意类型</typeparam>
/// <param name="value">类型参数的实例化对象</param>
public void A<T>(T value)
{
ListObj.Add(value);
}
}
下面显式当类型参数为string时,调用泛型方法A<T>(T value)
:
// 实例化类
FanXing fanXing = new FanXing()
{
ListObj = new List<object>()
};
// 调用泛型方法
fanXing.A<string>("1234");
// 打印
fanXing.ListObj.ForEach(item =>
{
Console.WriteLine(item);
});
还可省略类型参数,编译器将推断类型参数。比如fanXing.A("1234")
和fanXing.A<string>("1234")
是等效的。
如果泛型类的类型参数和泛型方法的类型参数是同一个字母,也就是说如果定义一个具有与包含类相同的类型参数的泛型方法,则编译器会生成警告 CS0693,请考虑为此方法的类型参数提供另一标识符。
class GenericList<T>
{
// CS0693
void SampleMethod<T>() { }
}
class GenericList2<T>
{
//No warning
void SampleMethod<U>() { }
}
泛型接口
泛型也可以用于接口:
public interface IJK<T>
{
void One(T value);
T Two();
public int MyProperty { get; set; }
}
可以用和接口有相同泛型参数的类来实现接口:
/// <summary>
/// 实现泛型接口
/// </summary>
/// <typeparam name="T">泛型参数</typeparam>
public class Jk<T> : IJK<T>
{
public int MyProperty { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
public void One(T value)
{
throw new NotImplementedException();
}
public T Two()
{
throw new NotImplementedException();
}
}
实现规则
具体类可实现封闭式构造接口。
interface IBaseInterface<T> { }
class SampleClass : IBaseInterface<string> { }
只要类形参列表提供接口所需的所有实参,泛型类即可实现泛型接口或封闭式构造接口。
interface IBaseInterface1<T> { }
interface IBaseInterface2<T, U> { }
class SampleClass1<T> : IBaseInterface1<T> { } //正确
class SampleClass2<T> : IBaseInterface2<T, string> { } //正确
错误实现:
// 错误
public class Jk: IJK<T>
{
}
//错误
class SampleClass2<T> : IBaseInterface2<T, U> { }
继承规则
泛型类的继承规则也适用于接口。
interface IMonth<T> { }
interface IJanuary : IMonth<int> { } //正确
interface IFebruary<T> : IMonth<int> { } //正确
interface IMarch<T> : IMonth<T> { } //正确
//interface IApril<T> : IMonth<T, U> {} //错误,U既不是派生接口IApril的泛型参数,也没有具体指向哪一个类型
泛型约束关键字
在定义泛型类时,可以对代码能够在实例化类时用于类型参数的类型种类施加限制。如果代码尝试使用某个约束所不允许的类型来实例化类,则会产生编译时错误。这些限制称为约束。约束是使用 where
上下文关键字指定的。
new
new
约束指定泛型类声明中的类型实参必须有公共的无参数构造函数。 也就是说若要使用 new
约束,则该类型不能为抽象类型。
使用方式如下:
class B<T> where T : new()
{
public B()
{
}
public B(T value)
{
Console.WriteLine(value);
}
}
假设现在有一个接口类AbB
和接口类的实现类AbBExtend
,如果某泛型类使用了new
约束,则AbB
无法作为该泛型类的类型参数。
public void Four()
{
// AdB为抽象类
//B<AbB>(); error
// AbBExtend为AdB抽象类的实现类
new B<AbBExtend>(); // right
// C为接口
//new B<C>(); error
}
where
泛型定义中的
where
子句指定对用作泛型类型、方法、委托或本地函数中类型参数的参数类型的约束。 约束可指定接口、基类或要求泛型类型为引用、值或非托管类型。 它们声明类型参数必须具备的功能。
说白了,where约束泛型参数是谁的派生类,是谁的实现类,即约束泛型参数来自哪里。
比如说,现在有一个抽象类AdB
,想要泛型类D<T>
的类型参数T
必须是AdB
的抽象类,可以这样做:
/// <summary>
/// 类型参数必须来自AbB
/// </summary>
/// <typeparam name="T">抽象类AbB的派生类</typeparam>
public class D<T> where T : AbB
{
}
where的用法是where T: 约束
,下表列出了5种类型的约束:
约束 说明 T:struct
类型参数必须是值类型。可以指定除 Nullable 以外的任何值类型。 T:class
类型参数必须是引用类型,包括任何类、接口、委托或数组类型。 T:new ()
类型参数必须具有无参数的公共构造函数。当与其他约束一起使用时,new() 约束必须最后指定。 T:<基类名>
类型参数必须是指定的基类或派生自指定的基类。 T:<接口名称>
类型参数必须是指定的接口或实现指定的接口。可以指定多个接口约束。约束接口也可以是泛型的。 T:U
为 T 提供的类型参数必须是为 U 提供的参数或派生自为 U 提供的参数。这称为裸类型约束. T:notnull
约束将类型参数限制为不可为 null 的类型。 T : default
泛型方法的override或泛型接口的实现中使用 default
表明没有泛型约束,即使用default
约束来指定派生类在派生类中没有约束的情况下重写方法,或指定显式接口实现。此约束极少用到。T : unmanaged
类型参数为“非指针、不可为 null 的非托管类型”。
下面讲解几个比较不容易理解的约束:
引用类型约束
/// <summary>
/// 泛型参数必须为引用数据类型
/// </summary>
/// <typeparam name="T">引用数据类型</typeparam>
public class D<T> where T : class
{
}
裸类型约束
用作约束的泛型类型参数称为裸类型约束。当具有自己的类型参数的成员函数需要将该参数约束为包含类型的类型参数时,裸类型约束很有用。
class List<T>
{
void Add<U>(List<U> items) where U : T {/*...*/}
}
泛型类的裸类型约束的作用非常有限,因为编译器除了假设某个裸类型约束派生自 System.Object 以外,不会做其他任何假设。在希望强制两个类型参数之间的继承关系的情况下,可对泛型类使用裸类型约束。
new 组合约束
可以将其他的约束类型和new约束进行组合约束。
public class D<T> where T : class, new()
{
}
用途
泛型作为一个概念,是一个不指定特定类型的规范,在日常开发中的用途会因为开发者的需求不同而创造出不同的用途。在.NET BCL(基本类库)中,常见的用途是创建集合类。
关于如何创建集合类,请参考Generic classes and methods | Microsoft Docs,笔者不否认实现一个集合类的作用,但是在笔者并不丰富的开发经验中,极少自己创建一个集合类,原因是对集合没有过高的性能要求,认为.NET BCL所提供的泛型集合类已经满足了性能需求,关于对功能的需求,更多的是创造拓展方法。
使用泛型类型可最大限度地提高代码重用率、类型安全性和性能。
- 泛型最常见的用途是创建集合类。
- .NET 类库包含System.Collections.Generic命名空间中的多个泛型集合类。应尽可能使用泛型集合,而不是System.Collections命名空间中的ArrayList等类。
- 您可以创建自己的泛型接口、类、方法、事件和委托。
- 泛型类可能受到限制,以允许访问特定数据类型上的方法。
- 有关泛型数据类型中使用的类型的信息可以在运行时使用反射获得。
LICENSE
已将所有引用其他文章之内容清楚明白地标注,其他部分皆为作者劳动成果。对作者劳动成果做以下声明:
copyright 2021 苏月晟,版权所有。
本作品由苏月晟采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。
【科普】.NET6 泛型的更多相关文章
- 深入谷歌和甲骨文旷日持久的版权战争,趣味科普当前最火的编程语言JAVA的前世今生
这篇文章是我在B站上投稿的一个科普java的视频文案,内容如标题,感兴趣的码农朋友可以移步观看https://www.bilibili.com/video/av81171108/ 在过去短短几十年间, ...
- 人生靠反省,Java靠泛型
昨天有同事问 UserService.XxxService 都会调用 Dao 的 insert.update ... ...,这些重复的代码,有没有办法变得灵活一些? 巧了,和咱们分享的主题刚好碰上, ...
- 简单聊下.NET6 Minimal API的使用方式
前言 随着.Net6的发布,微软也改进了对之前ASP.NET Core构建方式,使用了新的Minimal API模式.之前默认的方式是需要在Startup中注册IOC和中间件相关,但是在Minimal ...
- Net6 DI源码分析Part4 CallSiteFactory ServiceCallSite
Net6 CallSiteFactory ServiceCallSite, CallSiteChain abstract class ServiceCallSite ServiceCallSite是个 ...
- Net6 Configuration & Options 源码分析 Part2 Options
Net6 Configuration & Options 源码分析 Part2 Options 第二部分主要记录Options 模型 OptionsConfigurationServiceCo ...
- 一起学 Java(三) 集合框架、数据结构、泛型
一.Java 集合框架 集合框架是一个用来代表和操纵集合的统一架构.所有的集合框架都包含如下内容: 接口:是代表集合的抽象数据类型.接口允许集合独立操纵其代表的细节.在面向对象的语言,接口通常形成一个 ...
- .NET面试题系列[8] - 泛型
“可变性是以一种类型安全的方式,将一个对象作为另一个对象来使用.“ - Jon Skeet .NET面试题系列目录 .NET面试题系列[1] - .NET框架基础知识(1) .NET面试题系列[2] ...
- C#4.0泛型的协变,逆变深入剖析
C#4.0中有一个新特性:协变与逆变.可能很多人在开发过程中不常用到,但是深入的了解他们,肯定是有好处的. 协变和逆变体现在泛型的接口和委托上面,也就是对泛型参数的声明,可以声明为协变,或者逆变.什么 ...
- 编写高质量代码:改善Java程序的151个建议(第7章:泛型和反射___建议106~109)
建议106:动态代理可以使代理模式更加灵活 Java的反射框架提供了动态代理(Dynamic Proxy)机制,允许在运行期对目标类生成代理,避免重复开发.我们知道一个静态代理是通过主题角色(Prox ...
随机推荐
- 快速沃尔什变换&快速莫比乌斯变换小记
u1s1 距离省选只剩 5 days 了,现在学新算法真的合适吗(( 位运算卷积 众所周知,对于最普通的卷积 \(c_i=\sum\limits_{j+k=i}a_jb_k\),\(a_jb_k\) ...
- AtCoder Beginner Contest 188题解
A 题意 问\(x,y\)相差是否小于\(3\) #include<iostream> #include<cstdio> #include<cmath> #defi ...
- nginx_update
软件下载 预编译 编译 配置 [root@MiWiFi-R1CM-srv ~]#wget -c https://nginx.org/download/nginx-1.15.0.tar.gz 通过-V查 ...
- 40-3Sum Closest
3Sum Closest My Submissions QuestionEditorial Solution Total Accepted: 76185 Total Submissions: 2621 ...
- Shell 输出第五行的内容
目录 Shell 输出第五行的内容 题目 题解-awk 题解-sed Shell 输出第五行的内容 题目 写一个 bash脚本以输出一个文本文件 nowcoder.txt 中第5行的内容. 示例: 假 ...
- 日常Java 2021/10/28
Java lterator Java lterator(迭代器)不是一个集合,它是一种用于访问集合的方法,可用于迭代 ArrayList和HashSet等集合.lterator是Java迭代器最简单的 ...
- django数据库增删改查
django中数据库基本操作: 1.同步数据库 python manage.py makemigrations #生成migrations python manage.py migrate #应用mi ...
- RocketMQ集群搭建方式
各角色介绍 Producer:消息的发送者:举例:发信者 Consumer:消息接收者:举例:收信者 Broker:暂存和传输消息:举例:邮局 NameServer:管理Broker:举例:各个邮局的 ...
- 【Reverse】每日必逆0x02
BUU SimpleRev 附件 https://files.buuoj.cn/files/7458c5c0ce999ac491df13cf7a7ed9f1/SimpleRev 题解 查壳 拖入iad ...
- 容器之分类与各种测试(三)——list部分用法
list是一个双向链表 例程 #include<stdexcept> #include<memory.h> #include<string> #include< ...