首页 > 编程语言 >《NET CLR via C#》---第十二章(泛型)

《NET CLR via C#》---第十二章(泛型)

时间:2024-09-24 16:36:39浏览次数:1  
标签:via C# List System 约束 --- 类型 泛型 CLR

泛型(generic)是CLR和编程语言提供的一种特殊机制,它支持另一种形式的代码重用,即“算法重用”。

简单来说,开发人员先定义好算法,必然排序、搜索、交换、比较或者转换等。但是,定义算法的开发人员并不设定该算法要操作什么数据类型;该算法可广泛地应用于不同类型的对象。

泛型为开发人员提供了以下优势:

  • 源代码保护:使用泛型算法的开发人员不需要访问算法的源代码。然后,使用C++模板的泛型技术时,算法的源代码必须提供给准备使用算法的用户。
  • 类型安全:将泛型算法应用于一个具体的类型时,编译器和CLR能理解开发人员的意图,并保证只有与指定数据类型兼容的对象才能用于算法。
  • 更清晰的代码:由于编译器强制类型安全性,所以减少了源代码中必须进行的强制类型转换次数,使代码更容易编写和维护。
  • 更佳的性能:没有泛型的时候,要想定义常规化的算法,它的所有成员都要定义成操作Object数据类型。要用这个算法来操作值类型的实例,CLR必须在调用算法的成员之前对值类型实例进行装箱。

为了理解性能优化,我们可以通过如下的代码进行测试:

using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;

class Program
{
    static void Main(string[] args)
    {
        ValueTypePerfTest();
        ReferenceTypePerfTest();
    }

    private static void ValueTypePerfTest()
    {
        const int count = 100000000;

        using(new OperationTimer("List<int>"))
        {
            List<int> l = new List<int>();
            for (int i = 0; i < count; i++)
            {
                l.Add(i);
                int x = l[i];
            }
            l = null;
        }

        using (new OperationTimer("ArrayList of int"))
        {
            ArrayList l = new ArrayList();
            for (int i = 0; i < count; i++)
            {
                l.Add(i);           // 装箱
                int x = (int)l[i];  // 拆箱
            }
            l = null;
        }
    }

    private static void ReferenceTypePerfTest()
    {
        const int count = 100000000;

        using (new OperationTimer("List<string>"))
        {
            List<string> l = new List<string>();
            for (int i = 0; i < count; i++)
            {
                l.Add("X");
                string x = l[i];
            }
            l = null;
        }

        using (new OperationTimer("ArrayList of string"))
        {
            ArrayList l = new ArrayList();
            for (int i = 0; i < count; i++)
            {
                l.Add("X");          
                string x = (string)l[i];  
            }
            l = null;
        }
    }
}

internal sealed class OperationTimer : IDisposable
{
    private Stopwatch stopwatch;
    private string text;
    private int collectionCollect;

    public OperationTimer(string text)
    {
        PrepareForOperation();
        this.text = text;
        collectionCollect = GC.CollectionCount(0);

        stopwatch = Stopwatch.StartNew();
    }

    public void Dispose()
    {
        Console.WriteLine($"{stopwatch.Elapsed} (GCs={GC.CollectionCount(0) - collectionCollect}) {text}");
    }

    private static void PrepareForOperation()
    {
        GC.Collect();
        GC.WaitForPendingFinalizers();
        GC.Collect();
    }
}

最后会得到如下输出:
image
很明显,在操作值类型时,泛型算法比非泛型算法快了近乎11倍了。此外用ArrayList操作值类型,会造成大量装箱,最终要进行293次垃圾回收。

不过,引用类型,差异则没有那么明显了,GC一样都是0,时间虽然泛型略快一点,但也不像值类型有这么大的差距。

开放类型和封闭类型

具有泛型类型参数的类型仍然是类型,CLR同样会为它创建内部的类型对象。这一点适合引用类型,值类型,接口类型和委托类型。然而,具有泛型类型参数的类型称为开放类型,CLR禁止构造开放开放类型的任何实例。

代码引用泛型类型时可指定一组泛型类型参数。为所有类型参数都传递了实际的数据类型,类型就称为封闭类型。CLR允许构造封闭类型的实例。例如以下例子中,我分别尝试用反射的方法去实例化一个开放类型和封闭类型:

using System;
using System.Collections.Generic;

class Program
{
    static void Main(string[] args)
    {
        try
        {
            Type t1 = typeof(Dictionary<,>);
            var o1 = Activator.CreateInstance(t1);
            Console.WriteLine($"{t1.ToString()}实例化传功");
        }
        catch(ArgumentException e)
        {
            Console.WriteLine(e);
        }

        try
        {
            Type t2 = typeof(Dictionary<int, int>);
            var o2 = Activator.CreateInstance(t2);
            Console.WriteLine($"{t2.ToString()}实例化传功");
        }
        catch(ArgumentException e)
        {
            Console.WriteLine(e);
        }
    }
}

--------输出结果------
ystem.ArgumentException: Cannot create an instance of System.Collections.Generic.Dictionary`2[TKey,TValue] because Type.ContainsGenericParameters is true.
   at System.RuntimeType.CreateInstanceCheckThis()
   at System.RuntimeType.CreateInstanceDefaultCtor(Boolean publicOnly, Boolean skipCheckThis, Boolean fillCache, Boolean wrapExceptions)
   at System.Activator.CreateInstance(Type type, Boolean nonPublic, Boolean wrapExceptions)
   at System.Activator.CreateInstance(Type type)
   at Program.Main(String[] args) in C:\Users\LH89\source\repos\ConsoleApp3\Program.cs:line 13
System.Collections.Generic.Dictionary`2[System.Int32,System.Int32]实例化传功

可以看出,只有封闭类型才能实例化成功。从输出类型可以看出,类型名以“`”字符和一个数字结尾。数字代表类型的元数,也就是类型要求的类型参数个数。

还要注意,CLR会在类型对象内部分配类型的静态字段。因此,每个封闭类型都有自己的静态字段。换言之,假如List<T>定义了任何静态字段,则List<int>和List<string>不会共享这些静态字段。另外,假如泛型类型定义了静态构造器,那么针对每个封闭类型,这个构造器都会执行一次。

泛型类型和继承

泛型类型仍然是类型,所以能从其他任何类型派生。使用泛型类型并指定类型实参时,实际是在CLR中定义了一个新的类型对象,新的类型对象从泛型类型派生自的那个类型派生。

class Program
{
    static void Main(string[] args)
    {
        TypeNode<int> a = new TypeNode<int>();
        TypeNode<string> b = new TypeNode<string>();

        Node start = new Node();
        start.next = a;
        start.next.next = b;
    }
}

public class Node
{
    public Node next;
}

public class TypeNode<T> : Node { }

例如上例中,Node<int>和Node<string>都继承Node基类,我们也可以利用到多态的特点,将值类型和引用类型装入同一个链表中,同时避免了值类型装箱拆箱的特点。

同一性

为了对语法进行增强,有的开发人员定义了一个新的非泛型类类型,它从一个泛型类型派生,并指定了所有类型实参,例如:

public class DateTimeList : List<DateTime> { }

此时就可以简化创造列表代码:

List<DateTime> list1 = new List<DateTime>(); ->
DateTimeList list2 = new DateTimeList();

这样做表面上是简化了代码书写,但其实不妥!绝对不要出于增强源码可读性的目的来定义一个新类。这样会散失同一性(identity)和相等性(equivalence),例如我们此时比较 list1.GetType() == list2.GetType(),会返回一个false,因为比较的是不同类型的两个对象。这也意味着如果方法的原型接受一个DateTimeList,我们无法把List<DateTime>传给他,这会导致开发非常混乱。

C#也考虑到了泛型的书写困难,所以他提供了简化的语法来引用泛型封闭类型,例如我们可以在源文件顶部这样声明,就不会丧失同一性也能保证代码可读性:

using DateTimeList = System.Collections.Generic.List<System.DateTime>;

代码爆炸

使用泛型类型参数的方法在进行JIT编译时,CLR获取方法的IL,用指定的类型实参替换,然后创建恰当的本机代码。这样有个缺点:CLR要为每种不同的方法/类型组合生成本机代码,这种现象称为代码爆炸。它会使得应用程序的工作集显著增大,从而损害性能。

幸好,CLR采用了一些优化措施缓解了代码爆炸:

  1. 假如为特定的类型实参调用了一个方法,以后再用相同的类型实参调用这个方法,CLR只会为这个方法/类型组合编译一次代码。
  2. CLR认为所有引用类型实参都完全相同,所以代码能够共享。例如,CLR为List<String>的方法编译的代码可直接用于List<Stream>的方法,因为String和Stream均为引用类型。CLR之所以能执行这个优化,是因为所有引用类型的实参或变量实际只是指向堆上对象的指针,而所有对象指针都以相同的方式操纵。
    但如果类型实参是值类型,CLR就必须专门为那个值类型生成本机代码。这是因为值类型的大小不定。即使2个值类型大小一样(比如int32和uint32,都是32位),CLR仍然无法共享代码,因为可能要用不同的本机CPU指令来操纵这些值。

泛型接口

没有泛型接口,每次用非泛型接口(入IComparable)来操纵值类型都会发生装箱,而且会失去编译时的类型安全性。因此,CLR提供了对泛型接口的支持,例如:

public interface IAnimal<T>
{
    T animal { get; }
}

public class Dog : IAnimal<Dog>
{
    public Dog animal => new Dog();
}

public class Number : IAnimal<int>
{
    public int animal => 0;
}

泛型委托

CLR支持泛型委托,目的是保证任何类型的对象都能以类型安全的方式传给回调方法。此外,泛型委托允许值类型实例在传给回调方法时不进行任何装箱。
具体例子先暂时跳过,看完17章泛型再来补充

泛型方法

泛型方法的存在,为开发人员提供了极大的灵活性。例如:

    private void Swap<T>(ref T o1, ref T o2)
    {
        T temp = o1;
        o1 = o2;
        o2 = temp;
    }

有一点要注意的是,作为out/ref实参传递的变量必须具有与方法参数相同的类型,以防止损坏类型安全性。

可验证性和约束

约束的作用是限制能指定成泛型实参的类型数量。通过限制类型的数量,可以对那些类型指向更多操作。例如:

    public static T Min<T>(T o1, T o2) where T : IComparable<T>
    {
        if(o1.CompareTo(o2) < 0)
        {
            return o1;
        }

        return o2;
    }

C#的where关键字告诉编译器,为T指定的任何类型都必须实现同类型(T)的泛型IComparable接口。有了这个约束,就可以在方法中调用CompareTo,因为已知IComparable<T>接口定义了CompareTo。

约束可应用于泛型类型的类型参数,也可应用于泛型方法的类型参数。CLR不允许基于类型参数名称或约束来进行重载;只能基于元数(类型参数个数)对类型或方法进行重载。

重写虚泛型方法时,重写的方法必须指定相同数量的类型参数,而且这些类型参数会继承在基类方法指定的约束上,事实上,根本不允许为重写的方法的类型参数指定任何约束。单类型参数的名称是可以改变的。

主要约束

类型参数可以指定零个或者一个主要约束。主要约束可以是代表非密封类的一个引用类型。不能指定以下特殊引用类型:System.Object,System.Array,System.Delegate,System.MulticastDelegate,System.ValueType,System.Enum或者System.Void。

指定引用类型约束时,相当于向编译器承诺:一个指定的类型实参要么是与约束类型相同的类型,要么是从约束类型派生的类型。例如:

    public static T Min<T>(T o1, T o2) where T : List<int>
    {
        return o1.Count < o2.Count ? o1 : o2;
    }

有两个特殊的主要约束:class和struct。
class约束:向编译器承诺类型实参是引用类型。(任何类类型、接口类型、委托类型或者数组类型都满足这个约束)
struct约束:向编译器承诺类型实参是值类型。(包括枚举在内的任何值类型都满足这个约束,但编译器和CLR将任何System.Nullable<T>值类型视为特殊类型,不满足这个struct约束)

原因是Nullable<T>类型将它的类型参数约束为struct,而CLR希望禁止像Nullable<Nullable<T>>这样的递归类型。

次要约束

类型参数可以指定零个或者多个次要约束,次要约束代表接口类型。这种约束向编译器承诺,类型实参实现了接口,由于能指定多个接口约束,所以类型实参必须实现了所有接口约束。

还有一种次要约束称为类型参数约束,有时也称为裸类型约束。它允许一个泛型类型或方法规定:指定的类型实参要么是约束的类型,要么是约束的类型的派生类。例如:

    private static List<TBase> ConvertIList<T, TBase>(IList<T> list) where T : TBase
    {
        List<TBase> baseList = new List<TBase>(list.Count);
        foreach(var item in list)
        {
            baseList.Add(item);
        }
        return baseList;
    }

构造器约束

类型参数可指定零个或一个构造器约束,它向编译器承诺类型实参是实现了公共无参构造器的非抽象类型。例如:

    private class ConstructorConstaint<T> where T : new() 
    {
        public static T Factory()
        {
            return new T();
        }
    }

标签:via,C#,List,System,约束,---,类型,泛型,CLR
From: https://www.cnblogs.com/chenxiayun/p/18396580

相关文章

  • csp2024赛前集训
    2024-09-24开题顺序:ABDC时间分配:A:20min,B:30min,C:1.5h,D:30min,其余时间打摆。主观难度:绿蓝紫蓝set设\(f_{i,j}\)表示前\(i\)个数和为\(j\)的方案数,然后直接01背包,最后用快速幂把每种和的数量次方乘起来就行了。由于\(f\)最后要当指数,所以要\(mod(kM-1)\)。hire......
  • Visual Instruction Tuning
    使用机器生成的指令跟踪数据对大型语言模型(LLM)进行指令调整已被证明可以提高新任务的零样本能力,但这个想法在多模态领域的探索较少。我们首次尝试使用纯语言GPT-4生成多模态语言图像指令跟踪数据。通过对此类生成的数据进行指令调整,我们引入了LLaVA:大型语言和视觉助手,这......
  • C#在Winform中截图指定控件中的内容生成图像
    开发上位机过程中,收到需求:在软件跑完数据之后保存报告和图表截图。因为界面控件都做了大小拉伸缩放的适配,所以简单的设置截图起始点和长宽时无法满足需求的。所以要做一个根据控件本身大小来做截取动作的功能,所以我写了一个截取指定控件内图像的函数。 函数如下,只需传入控件,和存......
  • 存算分离+双集群容灾丨云和恩墨与华为共同发布 MogDB × OceanStor Dorado 联合解决方
    引言为期三天的第九届华为全联接大会(HUAWEICONNECT2024)于9月19日在上海世博中心&展览馆盛大召开。本次大会以“共赢行业智能化”为主题,邀请思想领袖、商业精英、技术专家、合作伙伴、开发者等业界同仁,从战略、产业、生态等方面探讨如何通过智能化、数字化技术,赋能千行万业,把握新......
  • C# 线程(Thread)
    一、基本概念1、进程首先打开任务管理器,查看当前运行的进程:从任务管理器里面可以看到当前所有正在运行的进程。那么究竟什么是进程呢?进程(Process)是Windows系统中的一个基本概念,它包含着一个运行程序所需要的资源。一个正在运行的应用程序在操作系统中被视为一个进程,进程可......
  • PbootCMS 运行环境要求
    PbootCMS的运行环境要求如下:1.PHP版本要求PbootCMS需要PHP5.4或更高版本。支持最新的PHP7.0、7.1、7.2版本(截至2018年9月30日的信息,实际上现在可能支持更高版本的PHP)。2.需要开启的PHP扩展为了保证PbootCMS的所有功能都能正常使用,以下PHP扩展应该被启用:php_curl.dll:......
  • F - Takahashi in Narrow Road
    F-TakahashiinNarrowRoadProblemStatementThereisaroadextendingeastandwest,and$N$personsareontheroad.Theroadextendsinfinitelylongtotheeastandwestfromapointcalledtheorigin.The$i$-thperson$(1\leqi\leqN)$isinitial......
  • COMTRADE 录波文件 | 可视化工具 | 电能质量查看软件
    COMTRADE录波文件|可视化工具|电能质量查看软件主要功能介绍支持IEEEStdC37.111-1991/1999/2013规范。读取ASCII或二进制COMTRADE文件。查看来自COMTRADE配置文件的模拟和数字通道列表。将图表导出为SVG、BMP、JPEG和PNG图形格式。将显示的观察结果以CSV文件......
  • Ubuntu 上安装 Miniconda
    一、下载Miniconda打开终端。访问Anaconda官方仓库下载页面https://repo.anaconda.com/miniconda/选择Miniconda3-py310_24.7.1-0-Linux-x86_64.sh,进行下载。文件名当中的py310_24.7.1表示,在conda的默认的base环境中的Python版本是3.10,Miniconda版本是24.7.1。二......
  • ABC245G Foreign Friends 题解 / 二进制分组
    ABC245GForeignFriends题解回顾一下二进制分组。题目大意给定一张\(N\)个点\(M\)条边的无向图,及\(L\)个特殊点。每个点有颜色\(C_i\)。求每个点到离他最近的与他颜色不同特殊点的距离。Solve两个点颜色不同,等价于他们的颜色在二进制下至少有一位不同。所以我们考......