【科普】.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(泛型类型约束)- C# 参考 | Microsoft Docs

说白了,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 的非托管类型”。

来源:C# 泛型约束 xxx Where T:约束(二) - 赵青青 - 博客园 (cnblogs.com)

下面讲解几个比较不容易理解的约束:

引用类型约束

/// <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等类。
  • 您可以创建自己的泛型接口、类、方法、事件和委托。
  • 泛型类可能受到限制,以允许访问特定数据类型上的方法。
  • 有关泛型数据类型中使用的类型的信息可以在运行时使用反射获得。

来源:Generic classes and methods | Microsoft Docs

LICENSE

已将所有引用其他文章之内容清楚明白地标注,其他部分皆为作者劳动成果。对作者劳动成果做以下声明:

copyright © 2021 苏月晟,版权所有。


作品苏月晟采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。

版权声明:本文版权归作者所有,遵循 CC 4.0 BY-SA 许可协议, 转载请注明原文链接
https://www.cnblogs.com/sogeisetsu/p/15721731.html

C# 将PDF转为线性化PDF-编程思维

线性化PDF文件是PDF文件的一种特殊格式,可以通过Internet更快地进行查看。线性化的PDF,在页面数量很多的情况下,更能突出表现出快速浏览的优势。下面是通过后端.NET程序实现将PDF文件转为线性化PDF的方法。 程序环境 Visual Studio 2017 .NET Framework 4.6.1 Spir

容器扩展属性 IExtenderProvider 实现WinForm通用数据验证组件-编程思维

大家对如下的Tip组件使用应该不陌生,要想让窗体上的控件使用ToolTip功能,只需要拖动一个ToolTip组件到窗口,所有的控件就可以使用该功能,做信息提示。 本博文要记录的,就是通过容器扩展属性 IExtenderProvider,来实现一个数据验证组件,通过将组件拖动到窗口后,使得上面的所有控件可以实现数据验证!

WPF控件界面自适应-编程思维

之前就听说WPF流式布局,顺滑的很。但由于专业只学习了winform,工作对界面的要求并不高一直没去玩它。目前公司一些软件都是WPF布局,加上工作内容涉及Socket通讯较多,决定用WPF做一个通讯小工具。   本文章讲界面布局。 主要是想实现缩小放大界面时控件自动跟随的效果,开始用了百度上的很多方法,用Grid、Do

基于C#的多边形冲突检测-编程思维

之前在项目上碰到了一个多边形冲突检测的问题,经百度、bing、google,发现目前已有的方案,要么是场景覆盖不全,要么是通过第三方类库实现(而这些第三方类库几乎是无法逆向反编译的),而项目中禁止使用第三方类库,遂自己实现了此算法。 场景是这样的,有两个多边形,多边形A和多变型B,需要判断多边形B是否在多变型A内,即多

基于C#的机器学习--垃圾邮件过滤-编程思维

  在这一章,我们将建立一个垃圾邮件过滤分类模型。我们将使用一个包含垃圾邮件和非垃圾邮件的原始电子邮件数据集,并使用它来训练我们的ML模型。我们将开始遵循上一章讨论的开发ML模型的步骤。这将帮助我们理解工作流程。        在本章中,我们将讨论以下主题:     l  定义问题     l  准备数据     l 

c#中Array,ArrayList 与List的区别、共性与转换-编程思维

本文内容来自我写的开源电子书《WoW C#》,现在正在编写中,可以去WOW-Csharp/学习路径总结.md at master · sogeisetsu/WOW-Csharp (github.com)来查看编写进度。预计2021年年底会完成编写,2022年2月之前会完成所有的校对和转制电子书工作,争取能够在2022年

.NET6使用DOCFX自动生成开发文档-编程思维

本文内容来自我写的开源电子书《WoW C#》,现在正在编写中,可以去WOW-Csharp/学习路径总结.md at master · sogeisetsu/WOW-Csharp (github.com)来查看编写进度。预计2021年年底会完成编写,2022年2月之前会完成所有的校对和转制电子书工作,争取能够在2022年

.NET 5的System.Text.Json的JsonDocument类讲解-编程思维

本文内容来自我写的开源电子书《WoW C#》,现在正在编写中,可以去WOW-Csharp/学习路径总结.md at master · sogeisetsu/WOW-Csharp (github.com)来查看编写进度。预计2021年年底会完成编写,2022年2月之前会完成所有的校对和转制电子书工作,争取能够在2022年

javaScript(js)手写原生任务定时器源码-编程思维

javaScript(js)手写原生任务定时器 功能介绍 定时器顾名思义就是在某个特定的时间去执行一些任务,现代的应用程序早已不是以前的那些由简单的增删改查拼凑而成的程序了,高复杂性早已是标配,而任务的定时调度与执行也是对程序的基本要求了。通过时间表达式来进行调度和执行的一类任务被称为定时任务,很多业务需求的实现都离不

salesforce教程(一)管理员入门-编程思维

欢迎访问Administrator self Trailmix what Salesforce 只是一个 CRM 它可以存储客户数据,为您提供培养潜在客户的流程,以及提供与同事合作的途径。这些功能它都具备。 它的功能远不止这些。 术语 理解和数据库的关系 如下图理解 1 应用 一组对象、字段和支持业务流程的其他功能

js旋转v字俄罗斯方块_我的个趣-编程思维

实现效果如图,也就是一个图像的旋转。注意,旋转后的文字是相对应的,而且文字还是立起的。第一次点击时显示,第二次点击时开始旋转。下面是我做这个效果的记录,方法这么差,我也就不说什么了。 先上HTML/CSS部分,这部分都是相同的。JS放在 script 标签里。 <!-- Author: XiaoWen Cr