C# 中的泛型与非泛型性能

     2023-03-31     264

关键词:

【中文标题】C# 中的泛型与非泛型性能【英文标题】:Generic vs not-generic performance in C# 【发布时间】:2013-06-25 20:53:27 【问题描述】:

我写了两个等价的方法:

static bool F<T>(T a, T b) where T : class

    return a == b;


static bool F2(A a, A b)

    return a == b;

时差: 00:00:00.0380022 00:00:00.0170009

测试代码:

var a = new A();
for (int i = 0; i < 100000000; i++)
    F<A>(a, a);
Console.WriteLine(DateTime.Now - dt);

dt = DateTime.Now;
for (int i = 0; i < 100000000; i++)
    F2(a, a);
Console.WriteLine(DateTime.Now - dt);

有人知道为什么吗?

在下面的评论中,dtb* 显示CIL:

IL for F2: ldarg.0, ldarg.1, ceq, ret. IL for F<T>: ldarg.0, box !!T, ldarg.1, box !!T, ceq, ret.

我认为这是我问题的答案,但我可以使用什么魔法来拒绝拳击?

接下来我使用 Psilon 的代码:

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

namespace ConsoleApplication58

    internal class Program
    
        private class A
        

        

        private static bool F<T>(T a, T b) where T : class
        
            return a == b;
        

        private static bool F2(A a, A b)
        
            return a == b;
        

        private static void Main()
        
            const int rounds = 100, n = 10000000;
            var a = new A();
            var fList = new List<TimeSpan>();
            var f2List = new List<TimeSpan>();
            for (int i = 0; i < rounds; i++)
            
                // Test generic
                GCClear();
                bool res;
                var sw = new Stopwatch();
                sw.Start();
                for (int j = 0; j < n; j++)
                
                    res = F(a, a);
                
                sw.Stop();
                fList.Add(sw.Elapsed);

                // Test not-generic
                GCClear();
                bool res2;
                var sw2 = new Stopwatch();
                sw2.Start();
                for (int j = 0; j < n; j++)
                
                    res2 = F2(a, a);
                
                sw2.Stop();
                f2List.Add(sw2.Elapsed);
            
            double f1AverageTicks = fList.Average(ts => ts.Ticks);
            Console.WriteLine("Elapsed for F = 0 \t ticks = 1", fList.Average(ts => ts.TotalMilliseconds),
                              f1AverageTicks);
            double f2AverageTicks = f2List.Average(ts => ts.Ticks);
            Console.WriteLine("Elapsed for F2 = 0 \t ticks = 1", f2List.Average(ts => ts.TotalMilliseconds),
                  f2AverageTicks);
            Console.WriteLine("Not-generic method is 0 times faster, or on 1%", f1AverageTicks/f2AverageTicks,
                              (f1AverageTicks/f2AverageTicks - 1)*100);
            Console.ReadKey();
        

        private static void GCClear()
        
            GC.Collect();
            GC.WaitForPendingFinalizers();
            GC.Collect();
        
    

Windows 7、.NET 4.5、Visual Studio 2012,发布,优化,无需附加。

x64

Elapsed for F = 23.68157         ticks = 236815.7
Elapsed for F2 = 1.701638        ticks = 17016.38
Not-generic method is 13.916925926666 times faster, or on 1291.6925926666%

x86

Elapsed for F = 6.713223         ticks = 67132.23
Elapsed for F2 = 6.729897        ticks = 67298.97
Not-generic method is 0.997522398931217 times faster, or on -0.247760106878314%

而且我有了新的魔力:x64 速度快了三倍...

PS:我的目标平台是 x64。

【问题讨论】:

我得到 00:00:00.2080244 和 00:00:00.0071957 在没有调试器的发布版本中使用秒表。 你检查过 IL 吗?第二个示例可能由编译器内联。 我用过.net 4.5,它内联了这两种方法; dtb,我想如果你添加 [MethodImpl(MethodImplOptions.AggressiveInlining)] - 你会得到相同的结果 F2 的 IL:ldarg.0, ldarg.1, ceq, ret。 F 的 IL:ldarg.0, box !!T, ldarg.1, box !!T, ceq, ret。所以答案是因为参数被装箱了。但是为什么它们会被装箱呢? @dtb:你在看 IL,这没关系。您需要查看 JIT 生成的本机代码。 【参考方案1】:

我确实对您的代码进行了一些更改以正确测量性能。

    使用秒表 执行发布模式 防止内联。 使用 GetHashCode() 做一些实际的工作 查看生成的汇编代码

代码如下:

class A



[MethodImpl(MethodImplOptions.NoInlining)]
static bool F<T>(T a, T b) where T : class

    return a.GetHashCode() == b.GetHashCode();


[MethodImpl(MethodImplOptions.NoInlining)]
static bool F2(A a, A b)

    return a.GetHashCode() == b.GetHashCode();


static int Main(string[] args)

    const int Runs = 100 * 1000 * 1000;
    var a = new A();
    bool lret = F<A>(a, a);
    var sw = Stopwatch.StartNew();
    for (int i = 0; i < Runs; i++)
    
        F<A>(a, a);
    
    sw.Stop();
    Console.WriteLine("Generic: 0:F2s", sw.Elapsed.TotalSeconds);

    lret = F2(a, a);
    sw = Stopwatch.StartNew();
    for (int i = 0; i < Runs; i++)
    
        F2(a, a);
    
    sw.Stop();
    Console.WriteLine("Non Generic: 0:F2s", sw.Elapsed.TotalSeconds);

    return lret ? 1 : 0;

在我的测试中,非通用版本稍微快一些(.NET 4.5 x32 Windows 7)。 但在速度上几乎没有可测量的差异。我会说两者都是平等的。 为了完整起见,这里是通用版本的汇编代码: 我在发布模式下通过调试器获得了汇编代码,并启用了 JIT 优化。默认是在调试期间禁用 JIT 优化,以便更轻松地设置断点和变量检查。

通用

static bool F<T>(T a, T b) where T : class

        return a.GetHashCode() == b.GetHashCode();


push        ebp 
mov         ebp,esp 
push        ebx 
sub         esp,8 // reserve stack for two locals 
mov         dword ptr [ebp-8],ecx // store first arg on stack
mov         dword ptr [ebp-0Ch],edx // store second arg on stack
mov         ecx,dword ptr [ebp-8] // get first arg from stack --> stupid!
mov         eax,dword ptr [ecx]   // load MT pointer from a instance
mov         eax,dword ptr [eax+28h] // Locate method table start
call        dword ptr [eax+8] //GetHashCode // call GetHashCode function pointer which is the second method starting from the method table
mov         ebx,eax           // store result in ebx
mov         ecx,dword ptr [ebp-0Ch] // get second arg
mov         eax,dword ptr [ecx]     // call method as usual ...
mov         eax,dword ptr [eax+28h] 
call        dword ptr [eax+8] //GetHashCode
cmp         ebx,eax 
sete        al 
movzx       eax,al 
lea         esp,[ebp-4] 
pop         ebx 
pop         ebp 
ret         4 

非通用

static bool F2(A a, A b)

  return a.GetHashCode() == b.GetHashCode();


push        ebp 
mov         ebp,esp 
push        esi 
push        ebx 
mov         esi,edx 
mov         eax,dword ptr [ecx] 
mov         eax,dword ptr [eax+28h] 
call        dword ptr [eax+8] //GetHashCode
mov         ebx,eax 
mov         ecx,esi 
mov         eax,dword ptr [ecx] 
mov         eax,dword ptr [eax+28h] 
call        dword ptr [eax+8] //GetHashCode
cmp         ebx,eax 
sete        al 
movzx       eax,al 
pop         ebx 
pop         esi 
pop         ebp 
ret 

正如您所见,由于更多的堆栈内存操作并不完美,通用版本看起来效率略低,但实际上差异无法衡量,因为所有内容都适合处理器的 L1 缓存,这使得内存操作成本更低与非通用版本的纯寄存器操作相比。如果您需要为不来自任何 CPU 缓存的实际内存访问付费,我怀疑非通用版本在现实世界中的性能应该会更好。

出于所有实际目的,这两种方法都是相同的。您应该查看其他地方以获得真实世界的性能提升。我将首先查看数据访问模式和使用的数据结构。算法变化往往比这种低级的东西带来更多的性能增益。

Edit1:如果你想使用 == 那么你会发现

00000000  push        ebp 
00000001  mov         ebp,esp 
00000003  cmp         ecx,edx // Check for reference equality 
00000005  sete        al 
00000008  movzx       eax,al 
0000000b  pop         ebp 
0000000c  ret         4 

这两种方法都产生完全相同的机器代码。您测量的任何差异都是您的测量误差。

【讨论】:

谢谢,但我对如何加快我的应用程序很感兴趣,我不想使用 GetHashCode,在这种情况下它很慢 查看我的编辑。两种方法生成的汇编代码是相同的。之前所有猜测 IL 代码差异和其他影响因素的海报都没有看实际执行的代码。组装级别没有区别!任何性能差异都只是测量误差。 有趣,我有 IL 差异,请参阅主帖中的 UPD【参考方案2】:

您的测试方法有缺陷。您的操作方式存在一些大问题。

首先,您没有提供“warm-up”。在 .NET 中,您第一次访问某些内容时会比后续调用慢,因此它可以加载任何需要的程序集。如果您要执行这样的测试,您必须至少执行每个功能一次,否则第一次运行的测试将受到很大的惩罚。继续交换订单,您可能会看到相反的结果。

第二个DateTime is only accurate to 16ms,因此在比较两次时,您的 +/- 误差为 32 毫秒。两个结果之间的差异为 21 毫秒,完全在实验误差范围内。您必须使用更准确的计时器,例如 Stopwatch 类。

最后,不要做这样的人为测试。除了吹嘘某一类或另一类的权利之外,它们不会向您显示任何有用的信息。而是学习使用Code Profiler。这将向您展示导致代码变慢的原因,您可以就如何解决问题做出明智的决定,而不是“猜测”不使用模板化类会使您的代码更快。

这是一个示例代码,显示了它“应该”如何完成:

using System;
using System.Diagnostics;

namespace Sandbox_Console

    class A
    
    

    internal static class Program
    
        static bool F<T>(T a, T b) where T : class
        
            return a == b;
        

        static bool F2(A a, A b)
        
            return a == b;
        

        private static void Main()
        
            var a = new A();
            Stopwatch st = new Stopwatch();

            Console.WriteLine("warmup");
            st.Start();
            for (int i = 0; i < 100000000; i++)
                F<A>(a, a);
            Console.WriteLine(st.Elapsed);

            st.Restart();
            for (int i = 0; i < 100000000; i++)
                F2(a, a);
            Console.WriteLine(st.Elapsed);

            Console.WriteLine("real");
            st.Restart();
            for (int i = 0; i < 100000000; i++)
                F<A>(a, a);
            Console.WriteLine(st.Elapsed);

            st.Restart();
            for (int i = 0; i < 100000000; i++)
                F2(a, a);
            Console.WriteLine(st.Elapsed);

            Console.WriteLine("Done");
            Console.ReadLine();
        

    

结果如下:

warmup
00:00:00.0297904
00:00:00.0298949
real
00:00:00.0296838
00:00:00.0297823
Done

交换最后两个的顺序和第一个总是更短,因此实际上它们是“相同的时间”,因为它在实验误差范围内。

【讨论】:

好建议,但不是答案。我使用秒表、热身等验证了结果。请参阅上面的评论。 @dtb 我不同意你的结果,我刚刚发布了我的结果,我得到了 0.1ms 的差异,第一个总是赢。你做错了什么,导致第一个运行速度非常慢。 热身 00:00:00.2611852 00:00:00.0265555 真实 00:00:00.2700937 00:00:00.0181213 win7、vs 12、.net 4.5、x64 A 用的是什么?一个类(如我的)或类似 int 的东西(它实际上是一个结构)?另外,您是否在没有附加调试器的情况下以发布模式运行?附加调试器会极大地影响结果,因为它会禁用许多处理垃圾收集的内置优化器(因为如果您暂停程序并想查看监视窗口中的变量值,您可以t 在附加调试器时让变量很容易超出范围)。 有趣.... 以 x86 重新运行您的代码,看看会发生什么;)。我不知道为什么构建为 x64 会减慢两者之一的速度。【参考方案3】:

不要担心时间,担心正确性。

这些方法等效。其中一个使用class Aoperator==,另一个使用objectoperator==

【讨论】:

F使用object的operator==,而F2使用class A的operator==对吧? @dtb:是的,泛型必须使用System.Object 的运算符来调用。 所以除非 A 类重载 operator==,否则两者都使用对象的 operator==,因此等价的。 @dtb 好吧,如果A 或其任何超类型重载operator ==。如果object 是继承链中重载== 的派生最多的类型,那么它将使用它。 @dtb:这个问题中没有任何内容可以告诉我class A 是否有operator==... 并且至少对于一些可能的通用参数,结果会有所不同。【参考方案4】:

两件事:

    您正在使用DateTime.Now 进行基准测试。请改用Stopwatch。 您正在运行的代码不是在正常情况下。 JIT 很可能会影响第一次运行,使您的第一个方法变慢。

如果您切换测试顺序(即先测试非泛型方法),您的结果会反转吗?我会怀疑的。当我将您的代码插入LINQPad,然后复制它以便它运行两个测试两次时,第二次迭代的执行时间在几百个滴答声内。

所以,回答您的问题:是的,有人知道原因。这是因为你的基准不准确!

【讨论】:

订单不会改变任何东西。问题在于泛型方法中的装箱操作 @nirmus:这里没有拳击。对于引用类型,box 指令什么都不做。它将被 JIT 删除。【参考方案5】:

我重写了你的测试代码:

var stopwatch = new Stopwatch();
var a = new A();

stopwatch.Reset();
stopwatch.Start();
for (int i = 0; i < 100000000; i++)
    F<A>(a, a);
stopwatch.Stop();
Console.WriteLine(stopwatch.ElapsedMilliseconds);

stopwatch.Reset();
stopwatch.Start();
for (int i = 0; i < 100000000; i++)
    F2(a, a);
stopwatch.Stop();
Console.WriteLine(stopwatch.ElapsedMilliseconds);

交换顺序不会改变任何事情。

CIL 用于泛型方法:

L_0000: nop
L_0001: ldarg.0
L_0002: box !!T
L_0007: ldarg.1
L_0008: box !!T
L_000d: ceq
L_000f: stloc.0
L_0010: br.s L_0012
L_0012: ldloc.0
L_0013: ret

对于非泛型:

L_0000: nop
L_0001: ldarg.0
L_0002: ldarg.1
L_0003: ceq
L_0005: stloc.0
L_0006: br.s L_0008
L_0008: ldloc.0
L_0009: ret

所以拳击操作是你时差的原因。问题是为什么要加装箱操作。检查一下,堆栈溢出问题 Boxing when using generics in C#

【讨论】:

但是box指令如何影响JIT生成的代码(也就是处理器实际执行的代码)? 是的,我怎样才能停止在模板中装箱? @Dmitry:C# 没有模板。 .NET 泛型与 C++ 模板完全不同(至少就这个问题而言)。【参考方案6】:

在我的职业生涯中,我曾多次以专业身份进行过绩效分析,并有一些观察。

首先,测试太短而无法有效。我的经验法则是性能测试应该运行 30 分钟左右。 其次,重要的是多次运行测试以获得一系列计时。 第三,我很惊讶编译器没有优化掉循环,因为函数结果没有被使用并且被调用的函数没有副作用。 第四,微基准测试常常具有误导性。

我曾经在一个有一个大胆的性能目标的编译器团队工作。一个版本引入了一种优化,消除了特定序列的几条指令。它本应提高性能,但一个基准测试的性能却急剧下降。我们在具有直接映射缓存的硬件上运行。事实证明,循环的代码和内部循环中调用的函数占用了相同的缓存行,新的优化已经到位,但之前生成的代码没有。换句话说,该基准实际上是一个内存基准,并且完全依赖于内存缓存命中和未命中,而作者认为他们编写了一个计算基准。

【讨论】:

【参考方案7】:

这似乎更公平,不是吗?:D

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

namespace ConsoleApplication58

    internal class Program
    
        private class A
        

        

        private static bool F<T>(T a, T b) where T : class
        
            return a == b;
        

        private static bool F2(A a, A b)
        
            return a == b;
        

        private static void Main()
        
            const int rounds = 100, n = 10000000;
            var a = new A();
            var fList = new List<TimeSpan>();
            var f2List = new List<TimeSpan>();
            for (int i = 0; i < rounds; i++)
            
                //test generic
                GCClear();
                bool res;
                var sw = new Stopwatch();
                sw.Start();
                for (int j = 0; j < n; j++)
                
                    res = F(a, a);
                
                sw.Stop();
                fList.Add(sw.Elapsed);

                //test not-generic
                GCClear();
                bool res2;
                var sw2 = new Stopwatch();
                sw2.Start();
                for (int j = 0; j < n; j++)
                
                    res2 = F2(a, a);
                
                sw2.Stop();
                f2List.Add(sw2.Elapsed);
            
            double f1AverageTicks = fList.Average(ts => ts.Ticks);
            Console.WriteLine("Elapsed for F = 0 \t ticks = 1", fList.Average(ts => ts.TotalMilliseconds),
                              f1AverageTicks);
            double f2AverageTicks = f2List.Average(ts => ts.Ticks);
            Console.WriteLine("Elapsed for F2 = 0 \t ticks = 1", f2List.Average(ts => ts.TotalMilliseconds),
                  f2AverageTicks);
            Console.WriteLine("Not-generic method is 0 times faster, or on 1%", f1AverageTicks/f2AverageTicks,
                              (f1AverageTicks/f2AverageTicks - 1)*100);
            Console.ReadKey();
        

        private static void GCClear()
        
            GC.Collect();
            GC.WaitForPendingFinalizers();
            GC.Collect();
        
    

在我的笔记本电脑 i7-3615qm 上,通用比非通用

见http://ideone.com/Y1GIJK。

【讨论】:

您使用了哪些编译选项?文件名表明它是一个调试版本,没有优化,这并不有趣。 为了公平测试,我已将其发布在 ideone 上,您可以自己测试。什么是优化 - 我没有使用它,因为编译器无法编译一个没有在任何地方使用的变量的循环。您也可以将此代码复制粘贴到您的环境中,并随心所欲地进行测试 在我的笔记本电脑上,结果:非通用方法快 13.9610133964644 倍,或在 1296.10133964644% 上 这很奇怪......你没有改变N?增加 N 增加泛型性能... как-то странно, ну не может в 13 раз отличаться... 没有改变 N,但我使用的是 x64。有趣的 x86 结果遗传/不一样,但大 6 倍【参考方案8】:

我认为这是我问题的答案,但我可以使用什么魔法来拒绝拳击?

如果您的目标只是比较,您可以这样做:

    public class A : IEquatable<A> 
        public bool Equals( A other )  return this == other; 
    
    static bool F<T>( IEquatable<T> a, IEquatable<T> b ) where T : IEquatable<T> 
        return a==b;
    

这将避免拳击。

至于主要的计时偏差,我想每个人都已经确定,你如何设置秒表存在问题。我使用了一种不同的技术,如果我想从时间结果中删除循环本身,我会取一个空基线,然后从时间差中减去它。它并不完美,但它产生了一个公平的结果,并且不会因为一遍又一遍地启动和停止计时器而减慢。

【讨论】:

泛型与非泛型重载调用

】泛型与非泛型重载调用【英文标题】:GenericvsNon-GenericOverloadCalling【发布时间】:2012-04-1221:53:58【问题描述】:当我声明这样的方法时:voidDoWork<T>(Ta)voidDoWork(inta)然后用这个来调用它:inta=1;DoWork(a);它将调用什么DoWork方法... 查看详情

非泛型类的泛型类方法

】非泛型类的泛型类方法【英文标题】:Genericclassmethodonnon-genericclass【发布时间】:2021-02-1616:56:16【问题描述】:我正在尝试在非泛型类上创建一个方法,该方法会创建另一个泛型类并返回它。fromdataclassesimportdataclassfromtypingimpor... 查看详情

泛型的泛型的好处

...合类,如ArrayList。有关更多信息,请参见.NETFramework类库中的泛型(C#编程指南)。当然,也可以创建自定义泛型类型和方法,以提供自己的通用解决方案,设计类型安全的高效模式。下面的代码示例演示一个用于演示用途的简单... 查看详情

django rest框架中的泛型与视图集,如何选择使用哪一个?

】djangorest框架中的泛型与视图集,如何选择使用哪一个?【英文标题】:genericsvsviewsetindjangorestframework,howtopreferwhichonetouse?【发布时间】:2018-09-0401:35:18【问题描述】:如何选择使用泛型和视图集中的哪一个?换句话说,我什么... 查看详情

c#中的泛型和泛型集合

...性能,他的最常见应用就是创建集合类,可以约束集合类中的元素类型。比较典型的泛型集合是List<T>和Dictionary<>;泛型集合List<T>语法List<T>对象名=newList<T> 查看详情

为啥接口的泛型方法可以在 Java 中实现为非泛型?

】为啥接口的泛型方法可以在Java中实现为非泛型?【英文标题】:Whyagenericmethodofaninterfacecanbeimplementedasnon-genericinJava?为什么接口的泛型方法可以在Java中实现为非泛型?【发布时间】:2016-08-1002:49:11【问题描述】:假设我们有几... 查看详情

java泛型的作用及其基本概念

...p;java与c#一样,都存在泛型的概念,及类型的参数化。java中的泛型是在jdk5.0后出现的,但是java中的泛型与C#中的泛型是有本质区别的,首先从集合类型上来说,java中的ArrayList<Integer>和ArrayList<String>是同一个类型,在编译... 查看详情

c#泛型编程

1.泛型的概念   C#中的泛型与C++中的模板类似,泛型是实例化过程中提供的类型或类建立的。泛型并不限于类,还可以创建泛型接口、泛型方法,甚至泛型委托。这将极大提高代码的灵活性,正确使用泛型可以显著缩... 查看详情

泛型集合与非泛型集合的异同

简单的说:泛型集合就是需要自己指定数据类型,而且还不需要进行数据类型的转换,安全性提高了;而非泛型集合则是微软把它所存储的数据类型规定为Object类型(即:可以存储任何数据类型),使用时还要进行类型的转化,... 查看详情

c#集合

先来了解下集合的基本信息1、BCL中集合类型分为泛型集合与非泛型集合。2、非泛型集合的类和接口位于System.Collections命名空间。3、泛型集合的类和接口位于System.Collections.Generic命名空间。  ICollection接口是System.Collections命名... 查看详情

非泛型集合

...型等效类。  3)、可以使用一个整数索引访问此集合中的元素;索引从零开始。  4)、可以接收null空引用(VB中的Nothing)。  5)、允许 查看详情

返回类型为协议的泛型函数与参数和返回类型为协议的非泛型函数的区别

】返回类型为协议的泛型函数与参数和返回类型为协议的非泛型函数的区别【英文标题】:Differencebetweengenericfunctionwhosereturntypeisprotocolandnongenericfunctionwhoseargumentandreturntypeareprotocol【发布时间】:2021-05-2402:15:04【问题描述】:在... 查看详情

泛型与枚举

1:集合当中使用泛型:【1】统一集合当中的数据类型,更方便的操作数据。【2】参数化的类型【3】规定要操作的数据类型2:泛型类||泛型方法||泛型接口【1】泛型表示:大写字母【2】默认为object;自定义TEVK[!]:泛型方法与泛... 查看详情

详解c#泛型(代码片段)

  一、C#中的泛型引入了类型参数的概念,类似于C++中的模板,类型参数可以使类型或方法中的一个或多个类型的指定推迟到实例化或调用时,使用泛型可以更大程度的重用代码、保护类型安全性并提高性能;可以创建自定义... 查看详情

c#中的泛型

泛型(generic)是C#语言2.0和通用语言运行时(CLR)的一个新特性。泛型为.NET框架引入了类型参数(typeparameters)的概念。类型参数使得设计类和方法时,不必确定一个或多个具体参数,其的具体参数可延迟到客户代码中声明、实... 查看详情

c#中的泛型委托(@whitetaken)

今天学习一下c#中的泛型委托。1.一般的委托,delegate,可以又传入参数(<=32),声明的方法为 publicdelegatevoidSomethingDelegate(inta);1usingSystem;2usingSystem.Collections.Generic;3usingSystem.Linq;4usingSystem.Text;5usingSystem.Th 查看详情

c#编程(五十)----------栈

...栈是后进先出.Stack与Stack<T>,像队列一样,栈也提供了泛型与非泛型版本.Stack的方法:方法说明Pop()从栈顶读栈并删除元素Push()存放数据,存在栈顶Peek()从栈顶读,但不删除 案例:using System;using System.Collections;using 查看详情

c#中的泛型

...解一下泛型的定义:泛型允许我们延迟编写类或方法中的编程元素的数据类型的规范,直到实际在程序中使用它的时候。(也就是说泛型是可以与任何数据类型一起工作的类或方法)模块内高内聚,模块间低... 查看详情