【科普】.NET 泛型

本文内容来自我写的开源电子书《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 国际许可协议进行许可。

posted @ 2021-12-23 00:09  sogeisetsu  阅读(1364)  评论(3编辑  收藏  举报