泛型编程小结

我们将从下面的几个方面来诠释泛型:

⒈什么是泛型

⒉如何使用泛型集合

⒊CLR对泛型的支持

⒋泛型方法

⒌约束机制极其优点

⒍继承与泛型

⒎泛型与可替代性

⒏泛型和代理

⒐泛型与反射

⒑泛型的局限性

⒒什么是协变与反变

1.什么是泛型


  泛型允许你在编译时间实现类型安全。它们允许你创建一个数据结构而不限于一特定的数据类型。然而,当使用该数据结构时,编译器保证它使用的类型与类型安全是相一致的。泛型提供了类型安全,但是没有造成任何性能损失和代码臃肿。在这方面,它们很类似于C++中的模板,不过它们在实现上是很不同的。

  • 如果实例化泛型类型的参数相同,那么JIT编译器会重复使用该类型,因此C#的动态泛型能力避免了C++静态模板可能导致的代码膨胀的问题。
  • C# 泛型类型携带有丰富的元数据,因此C#的泛型类型可以应用于强大的反射技术。
  • C# 的泛型采用“基类, 接口, 构造器, 值类型/引用类型”的约束方式来实现对类型参数的“显式约束”,提高了类型安全的同时,也丧失了C++模板基于“签名”的隐式约束所具有的高灵活性。

 

2.如何使用泛型集合

namespace GenericPro
{
    class Program
    {
        static void Main(string[] args)
        {
            List<Int32> intList = new List<Int32>();
            intList.Add(1);
            intList.Add(2);
            //intList.Add(3.0);
            int total = 0;
            foreach (int val in intList)
            {
                total += val;
            }
            Console.WriteLine(total);
            Console.Read();
        }
    }
}

     上面的例子中,我们在尖括号中<>中指定了参数类型为Int32类型. 如果我们把intList.Add(3.0)的注释符号, 我们会发现编译错误, 这也就是应用泛型的一个好处, 我们得到的是一个编译时错误而不是运行时错误.

 

3.CLR对泛型的支持

     泛型不仅是一个语言级上的特征。.NET CLR能识别出泛型。在这种意义上说,泛型的使用是.NET中最为优秀的特征之一。对每个用于泛型化的类型的参数,类也同样没有脱离开微软中间语言(MSIL)。换句话说,你的配件集仅包含你的参数化的数据结构或类的一个定义,而不管使用多少种不同的类型来表达该参数化的类型。例如,如果你定义一个泛型类型MyList<T>,仅仅该类型的一个定义出现在MSIL中。当程序执行时,不同的类被动态地创建,每个类对应该参数化类型的一种类型。如果你使用MyList<int>和MyList<double>,有两种类即被创建。当你的程序执行时,让我们进一步在下面的例中分析这一点

program.cs
namespace GenericPro
{
    class Program
    {
        static void Main(string[] args)
        {
            GenericList<int> myIntList1 = new GenericList<int>();
            Console.WriteLine("----------First Time----------");
            Console.WriteLine(myIntList1.Count);
            GenericList<int> myIntList2 = new GenericList<int>();
            Console.WriteLine("----------Second Time----------");
            Console.WriteLine(myIntList1.Count);
            Console.WriteLine(myIntList2.Count);
            GenericList<double> myDoubleList = new GenericList<double>();
            GenericList<SampleClass> mySampleList = new GenericList<SampleClass>();
            Console.WriteLine("----------Third Time----------");
            Console.WriteLine(myIntList1.Count);
            Console.WriteLine(myIntList2.Count);
            Console.WriteLine(myDoubleList.Count);
            Console.WriteLine(mySampleList.Count);
            Console.WriteLine(new GenericList<SampleClass>().Count);
            Console.Read();
        }
    }
}
GenericList.cs
namespace GenericPro
{
    public class GenericList<T>
    {
        private static int objCount = 0;
        public GenericList()
        {
            objCount++; 
        }
        public int Count
        {
            get{return objCount;}
        }
    }
}
 

     还有一个空的SampleClass.cs ,得到的结果如下是 1 , 2 2 , 2 2 , 1 1 2

     前面两个2对应MyList<int>,第一个1对应MyList<double>,第二个1对应MyList<SampleClass>--在此,仅创建一个这种类型的实例。最后一个2对应MyList<SampleClass>,因为代码中又创建了这种类型的另外一个实例。上面的例子说明MyList<int>是一个与MyList<double>不同的类,而MyList<double>又是一个与MyList<SampleClass>不同的类。因此,在这个例中,我们有四个类:MyList: MyList<T>,MyList<int>,MyList<double>和MyList<X>。注意,虽然有4个MyList类,但仅有一个(GenericList`1)被存储在MSIL。怎么能证明这一点?我们来看看program.cs的IL指令

 

    .method private hidebysig static void Main(string[] args) cil managed
    {
        .entrypoint     //定义程序的入口点(该函数在程序启动的时候由.NET 运行库调用)
        .maxstack 1    //定义函数代码所用堆栈的最大深度。C#编译器可以对每个函数设置准确的值, 这里, 我们把他设为1
        .locals init (
            [0] class GenericPro.GenericList`1<int32> myIntList1,
            [1] class GenericPro.GenericList`1<int32> myIntList2,
            [2] class GenericPro.GenericList`1<float64> myDoubleList,
            [3] class GenericPro.GenericList`1<class GenericPro.SampleClass> mySampleList)
        L_0000: nop  //无操作
        L_0001: newobj instance void GenericPro.GenericList`1<int32>::.ctor()//用构造函数constructor生成一个类的实例
        L_0006: stloc.0     //把堆栈中取出的值存入第0个局部变量
        L_0007: ldstr "----------First Time----------"          // 把字符串压入堆栈
        L_000c: call void [mscorlib]System.Console::WriteLine(string)   //调用方法
        L_0011: nop 
        L_0012: ldloc.0 //把第0个变量装到堆栈中
        L_0013: callvirt instance int32 GenericPro.GenericList`1<int32>::get_Count()//调用一个对象(GenericList<>)的后期绑定方法(get_Count)
        L_0018: call void [mscorlib]System.Console::WriteLine(int32)
        L_001d: nop 
        L_001e: newobj instance void GenericPro.GenericList`1<int32>::.ctor()//用构造函数constructor生成一个类的实例
        L_0023: stloc.1      //把堆栈中取出的值存入第1个局部变量
        L_0024: ldstr "----------Second Time----------"// 把字符串压入堆栈
        L_0029: call void [mscorlib]System.Console::WriteLine(string)
        L_002e: nop 
        L_002f: ldloc.0  //把第0个变量装到堆栈中
        L_0030: callvirt instance int32 GenericPro.GenericList`1<int32>::get_Count()
        L_0035: call void [mscorlib]System.Console::WriteLine(int32)
        L_003a: nop 
        L_003b: ldloc.1  //把第1个变量装到堆栈中
        L_003c: callvirt instance int32 GenericPro.GenericList`1<int32>::get_Count()
        L_0041: call void [mscorlib]System.Console::WriteLine(int32)
        L_0046: nop 
        L_0047: ldstr "----------Third Time----------"// 把字符串压入堆栈
        L_004c: call void [mscorlib]System.Console::WriteLine(string)
        L_0051: nop 
        L_0052: newobj instance void GenericPro.GenericList`1<float64>::.ctor()
        L_0057: stloc.2     //把堆栈中取出的值存入第2个局部变量
        L_0058: newobj instance void GenericPro.GenericList`1<class GenericPro.SampleClass>::.ctor()
        L_005d: stloc.3     //把堆栈中取出的值存入第3个局部变量
        L_005e: ldloc.0  //把第0个变量装到堆栈中
        L_005f: callvirt instance int32 GenericPro.GenericList`1<int32>::get_Count()
        L_0064: call void [mscorlib]System.Console::WriteLine(int32)
        L_0069: nop 
        L_006a: ldloc.1  //把第1个变量装到堆栈中
        L_006b: callvirt instance int32 GenericPro.GenericList`1<int32>::get_Count()
        L_0070: call void [mscorlib]System.Console::WriteLine(int32)
        L_0075: nop 
        L_0076: ldstr "----------Forth Time----------"// 把字符串压入堆栈
        L_007b: call void [mscorlib]System.Console::WriteLine(string)
        L_0080: nop 
        L_0081: ldloc.2  //把第2个变量装到堆栈中
        L_0082: callvirt instance int32 GenericPro.GenericList`1<float64>::get_Count()
        L_0087: call void [mscorlib]System.Console::WriteLine(int32)
        L_008c: nop 
        L_008d: ldloc.3  //把第3个变量装到堆栈中
        L_008e: callvirt instance int32 GenericPro.GenericList`1<class GenericPro.SampleClass>::get_Count()
        L_0093: call void [mscorlib]System.Console::WriteLine(int32)
        L_0098: nop 
        L_0099: newobj instance void GenericPro.GenericList`1<class GenericPro.SampleClass>::.ctor()
        L_009e: call instance int32 GenericPro.GenericList`1<class GenericPro.SampleClass>::get_Count()
        L_00a3: call void [mscorlib]System.Console::WriteLine(int32)
        L_00a8: nop 
        L_00a9: call int32 [mscorlib]System.Console::Read()
        L_00ae: pop      //取出栈顶的值。当我们不需要把值存入变量时使用
        L_00af: ret      //从一个函数中返回
	}
 

4.泛型方法

namespace GenericPro
{
    class Program
    {
        static void Main(string[] args)
        {
            List<Int32> list1 = new List<Int32>();
            list1.Add(1);
            list1.Add(2);
            List<Int32> list2 = new List<Int32>();
            Copy<Int32>(list1, list2);    //泛型方法调用
            Console.WriteLine(list2.Count);
            Console.Read();
        }
        public static void Copy<T>(List<T> source, List<T> destination)//泛型方法声明
        {
            foreach (T obj in source)
            {
                destination.Add(obj);
            }
        }
    }
}

泛型方法的重载

    class MyClass
    {
        void F1<T>(T[] a, int i); // 不可以构成重载方法

        void F1<U>(U[] a, int i);
        void F2<T>(int x); //可以构成重载方法

        void F2(int x);
        void F3<T>(T t) where T : A; //不可以构成重载方法
        void F3<T>(T t) where T : B;
    } 

泛型方法的重写

    abstract class Base 
    { 
        public abstract T F<T,U>(T t, U u) where U: T; 
        public abstract T G<T>(T t) where T: IComparable;
    } 
    class Derived: Base{ 
        //合法的重写,约束被默认继承
        public override X F<X,Y>(X x, Y y) { } 
        //非法的重写,指定任何约束都是多余的
        public override T G<T>(T t) where T: IComparable {} 
    } 
 

5.约束机制及其优点

        public static T Max<T>(T source, T destination) 
        {
            if (source.CompareTo(destination) < 0)
                return source;
            return destination;
        }
 

     这里会出现这样的错误 : 1 “T”不包含“CompareTo”的定义,并且找不到可接受类型为“T”的第一个参数的扩展方法“CompareTo”(是否缺少 using 指令或程序集引用?) E:\我的C#\VS2010\WindowsFormsApplication1\GenericPro\Program.cs 17 24 GenericPro

     但是如果我们指定一个约束, 把上面的方法改成

             public static T Max<T>(T source, T destination)  where T:IComparable

     那么错误就消失了, 我指定的约束是,用于参数化类型的类型必须继承自(实现)Icomparable。

  • 基类约束
namespace GenericPro
{
    public class A {
        public void F1() { Console.WriteLine("this is int type"); }
    }
    public class B {
        public void F2() { Console.WriteLine("this is string type"); }
    }
    public class SubA : A { 
    }
    public class SubB : B { 
    }
    public class C<T, E>
        where T : A
        where E : B
    {
        public void F3(T t,E e){
            t.F1();//T类型的实例可以调用F1
            e.F2();//E类型的实例可以调用F2
        }
    }
    class Program
    {
        static void Main(string[] args)
        {
            C<SubA, SubB> c = new C<SubA,SubB>();
            c.F3(new SubA(), new SubB());
            Console.Read();
        }
    }
}
  • 接口约束

    interface IPrintable { void Print(); }
    interface IComparable<T> { int CompareTo(T v);}
    interface IKeyProvider<T> { T GetKey(); }
    class Dictionary<K,V> 
        where K: IComparable<K> 
        where V: IPrintable, IKeyProvider<K> 
    { 
        // 可以在类型为K的变量上调用CompareTo, 
        // 可以在类型为V的变量上调用Print和GetKey 
        //…. 
    } 
  • 构造器约束

     下面的约束是可以使用的
  where T : struct 类型必须是一种值类型(struct)
  where T : class 类型必须是一种引用类型(class)
  where T : new() 类型必须有一个无参数的构造器
  where T : class_name 类型可以是class_name或者是它的一个子类
  where T : interface_name 类型必须实现指定的接口
  你可以指定约束的组合,就象: where T : IComparable, new()。这就是说,用于参数化类型的类型必须实现Icomparable接口并且必须有一个无参构造器

 

6.继承与泛型

  • 一个参数化类型的泛型类,象MyClass1<T>,称作开放结构的泛型。一个不使用参数化类型的泛型类,象MyClass1<int>,称作封闭结构的泛型。
  • 你可以从一个封闭结构的泛型进行派生;也就是说,你可以从另外一个称为MyClass1的类派生一个称为MyClass2的类,就象:public class MyClass2<T> : MyClass1<int>;
  • 你也可以从一个开放结构的泛型进行派生,如果类型被参数化的话,如:public class MyClass2<T> : MyClass2<T>,是有效的,
  • 但是public class MyClass2<T> : MyClass2<Y>是无效的,这里Y是一个被参数化的类型。
  • 非泛型类可以从一个封闭结构的泛型类进行派生,但是不能从一个开放结构的泛型类派生。即:public class MyClass : MyClass1<int>是有效的, 但是public class MyClass : MyClass1<T>是无效的。

 

7.泛型与可替代性

     当我们使用泛型时,要小心可代替性的情况。如果B继承自A,那么在使用对象A的地方,可能都会用到对象B。假定我们有一篮子水果(a Basket of Fruits (Basket<Fruit>)),而且有继承自Fruit的Apple和Banana(皆为Fruit的种类)。一篮子苹果--Basket of Apples (Basket<apple>)可以继承自Basket of Fruits (Basket<Fruit>)?答案是否定的,如果我们考虑一下可代替性的话。为什么?请考虑一个a Basket of Fruits可以工作的方法

static void Package(Basket<Fruit> aBasket)
{
	aBasket.Add(new Apple());
	aBasket.Add(new Banana());
}
 

代码如下:

Program.cs
namespace GenericPro
{
    class Program
    {
        static void Main(string[] args)
        {
            Basket<Fruit> basket = new Basket<Fruit>();
            Package(basket);
            foreach (Fruit fruit in basket.objList)
            {
                fruit.show();
            }
            Console.Read();
        }
        static void Package(Basket<Fruit> aBasket)
        {
            aBasket.Add(new Apple());
            aBasket.Add(new Banana());
        }
    }
}
Fruit.cs
    public interface Fruit
    {
         void show();
	}
Apple.cs
    public class Apple:Fruit
    {
        #region Fruit 成员

        void Fruit.show()
        {
            Console.WriteLine("i am an apple ! ");
        }

        #endregion
	}
Banana.cs
    public class Banana:Fruit
    {
        void Fruit.show()
        {
            Console.WriteLine("i am a banana ! ");
        }
	}
Basket.cs
    public class Basket<T>
    {
        public List<T> objList = new List<T>();
        public void Add(T obj)
        {
            objList.Add(obj);
        }
    }
 

如果我们发送一个Basket<Fruit>给Package, 这个方法将会添加一个apple和一个banana. 但是如果发送一个Basket<Apple>的实例给这个方法的时候, 会是什么情形呢?

 

Basket<Apple> basket = new Basket<Apple>();

            Package(basket);

2

 

     下面我们来考虑另一种情况: 如果有个Animal的类型(含有Monkey子类), Animal类型可以吃Fruit(无论是苹果还是香蕉,都可以),

        public void Eat(Basket<Fruit> fruits) 
        {
            Console.WriteLine("Eeating following fruits");
            foreach (Fruit fruit in fruits.objList)
            {
                //吃水果
            }
        }
     如果我们把一个Basket<Fruit>传给Eat是可以成功的, 但是如果是Basket<Apple>, 我们会发现编译器保护了我们的代码, 这里我们又要用到约束:
        public void Eat<T>(Basket<T> fruits) where T:Fruit
        {
            Console.WriteLine("Eeating following fruits");
            foreach (Fruit fruit in fruits.objList)
            {
                //吃水果
            }
        }

     酱紫就可以通过编译器的保护了

 

8.泛型和代理

    

     代理也可以是泛型化的。这样就带来了巨大的灵活性。
     假定我们对写一个框架程序很感兴趣。我们需要提供一种机制给事件源以使之可以与对该事件感兴趣的对象进行通讯。我们的框架可能无法控制事件是什么。你可能在处理某种股票价格变化(double price),而我可能在处理水壶中的温度变化(temperature value),这里Temperature可以是一种具有值、单位、门槛值等信息的对象。那么,怎样为这些事件定义一接口呢?

ISource.cs

   
    public delegate void NotifyDelegate(Object info);
    public interface ISource
    {
        event NotifyDelegate NotifyActivity;
	}

intSource.cs

    class intSource:ISource
    {
        public event NotifyDelegate NotifyActivity;
	}

doubleSource.cs

	class doubleSource:ISource
	{
        public event NotifyDelegate NotifyActivity;
	}

     然后我们给每个委托注册个事件

    class Program
    {
        static void Main(string[] args)
        {
            intSource intsource = new intSource();
            intsource.NotifyActivity += new NotifyDelegate(intsource_NotifyActivity);
            doubleSource doublesource = new doubleSource();
            doublesource.NotifyActivity += new NotifyDelegate(doublesource_NotifyActivity);
        }

        static void doublesource_NotifyActivity(object info)
        {
            double price = (double)info;
            //在使用前downcast需要的类型
        }

        static void intsource_NotifyActivity(object info)
        {
            Temperature value = info as Temperature;
            //在使用前downcast需要的类型
        }
    }

     上面的代码并不直观,且因使用downcast而有些凌乱。借助于泛型,代码将变得更易读且更容易使用。让我们看一下泛型的工作原理:

namespace GenericPro
{
    class Program
    {
        static void Main(string[] args)
        {
            intSource intsource = new intSource();
            intsource.NotifyActivity+=new NotifyDelegate<int>(intsource_NotifyActivity);
            doubleSource doublesource = new doubleSource();
            doublesource.NotifyActivity += new NotifyDelegate<double>(doublesource_NotifyActivity);
        }

        static void doublesource_NotifyActivity(double info)
        {
            //在使用前不再需要downcast需要的类型
        }

        static void intsource_NotifyActivity(int info)
        {
            //使用前不再需要downcast需要的类型
        }
	}
}
namespace GenericPro
{
    public delegate void NotifyDelegate<T> (T info);
    public interface ISource<T>
    {
        event NotifyDelegate<T> NotifyActivity;
    }
}
namespace GenericPro
{
    class doubleSource : ISource<Double>
    {
        public event NotifyDelegate<Double> NotifyActivity;
    }
}
namespace GenericPro
{
    class intSource:ISource<Int32>
    {
        public event NotifyDelegate<Int32> NotifyActivity;
    }
}

9.泛型与反射

     既然泛型是在CLR级上得到支持的,你可以使用反射API来取得关于泛型的信息。如果你是编程的新手,可能有一件事让你疑惑:你必须记住既有你写的泛型类也有在运行时从该泛型类创建的类型。因此,当使用反射API时,你需要另外记住你在使用哪一种类型。我将在下面的例子中证明

namespace GenericPro
{
    public class MyClass<T> { }
    class Program
    {
        static void Main(string[] args)
        {
            MyClass<int> obj1 = new MyClass<int>();
            MyClass<double> obj2 = new MyClass<double>();
            Type type1 = obj1.GetType();
            Type type2 = obj2.GetType();
            Console.WriteLine("obj1’ˉs Type");
            Console.WriteLine(type1.FullName);
            Console.WriteLine(type1.GetGenericTypeDefinition().FullName);
            Console.WriteLine("obj2’ˉs Type");
            Console.WriteLine(type2.FullName);
            Console.WriteLine(type2.GetGenericTypeDefinition().FullName);

        }
    }
}

  在本例中,有一个MyClass<int>的实例,程序中要查询该实例的类名。然后我查询这种类型的GenericTypeDefinition()。GenericTypeDefinition()会返回MyClass<T>的类型元数据。你可以调用IsGenericTypeDefinition来查询是否这是一个泛型类型(象MyClass<T>)或者是否已指定它的类型参数(象MyClass<int>)。同样地,我查询MyClass<double>的实例的元数据。上面的程序输出如下:

obj1’s Type
GenericPro.MyClass`1[[System.Int32, mscorlib, Version=4.0.0.0, Culture=neutral,
PublicKeyToken=b77a5c561934e089]]
GenericPro.MyClass`1
obj2’s Type
GenericPro.MyClass`1[[System.Double, mscorlib, Version=4.0.0.0, Culture=neutral,
 PublicKeyToken=b77a5c561934e089]]
GenericPro.MyClass`1
 

10.泛型的局限性

至此,我们已了解了泛型的强大威力。是否其也有不足呢?我发现了一处。我希望微软能够明确指出泛型存在的这一局制性。在表达约束的时候,我们能指定参数类型必须继承自一个类。然而,指定参数必须是某种类的基类型该如何呢?为什么要那样做呢?
  在前面中,我展示了一个Copy()方法,它能够把一个源List的内容复制到一个目标list中去。然而,如果我想要把apple对象从一个列表复制到另一个Fruit列表(Apple继承自Fruit),情况会如何呢?当然,一个Fruit列表可以容纳Apple对象。所以我要这样编写代码:

List<Apple> appleList1 = new List<Apple>();
List<Fruit> fruitsList2 = new List<Fruit>();
…
Copy(appleList1, fruitsList2);

  这不会成功编译。你将得到两个个错误.
  编译器基于调用参数并不能决定T应该是什么。其实我想说,Copy方法应该接受一个某种数据类型的List作为第一个参数,一个相同类型的List或者它的基类型的List作为第二个参数。
  尽管无法说明一种类型必须是另外一种类型的基类型,但是你可以通过仍旧使用约束机制来克服这一限制。下面是这种方法的实现:
public static void Copy<T,E>(List<T> source, List<E> destination) where T:E
  在此,我已指定类型T必须和E属同一种类型或者是E的子类型。我们很幸运。为什么?T和E在这里都定义了!我们能够指定这种约束(然而,C#中并不鼓励当E也被定义的时候使用E来定义对T的约束)。
  然而,请考虑下列的代码:
    public class MyList<T>
    { 
        public void CopyTo(MyList<T> destination)
        { 
            //…
        }
     }

 

我应该能够调用CopyTo:

MyList<apple> appleList = new MyList<apple>();
MyList<apple> appleList2 = new MyList<apple>();
//…appleList.CopyTo(appleList2);

我也必须这样做:
MyList<apple> appleList = new MyList<apple>();
MyList<fruit> fruitList2 = new MyList<fruit>();
//…
appleList.CopyTo(fruitList2);

 

        这当然不会成功。如何修改呢?我们说,CopyTo()的参数可以是某种类型(或这种类型的基类型)的MyList。然而,约束机制不允许我们指定一个基类型。下面情况又该如何呢?
                public void CopyTo<E>(MyList<E> destination) where T : E
        但是, 它将给出一个编译错误:
               Error 1 ’TestApp.MyList<t>.CopyTo<e>()’ does not define type parameter ’T’

        当然,你可以把代码写成接收任意类型的MyList,然后在代码中,校验该类型是可以接收的类型。然而,这把检查工作推到了运行时刻,丢掉了编译时类型安全的优点。

 

11.什么是协变与反变

      协变(covariant)与反变(contravariant) : 假设有这样两个类型:TChild是TParent的子类,显然TChild型引用是可以安全转换为TParent型引用的。如果一个泛型接口IFoo<T>,IFoo<TChild>可以转换为IFoo<TParent>的话,我们称这个过程(这种与原始类型转换方向相同的可变性)为协变(covariant),而且说这个泛型接口支持对T的协变。而如果一个泛型接口IBar<T>,IBar<TParent>可以转换为T<TChild>的话,我们称这个过程为反变(contravariant),而且说这个接口支持对T的反变。

        因此很好理解,如果一个可变性和子类到父类转换的方向一样,就称作协变;而如果可变性和子类到父类的转换方向相反,就叫反变性.

 

结论


  .NET 4.0中的泛型是强有力的,你写的代码不必限定于一特定类型,然而你的代码却能具有类型安全性。泛型的实现目标是既提高程序的性能又不造成代码的臃肿。然而,在它的约束机制存在不足(无法指定一类型必须是另外一种类型的基类型)的同时,该约束机制也给你书写代码带来很大的灵活性,因为你不必拘泥于各种类型的“最小公分母”能力
希望用户看完之后注册用户发表你的意见,将给予更多的学习资料!


 

参考资料:

http://lqs.13e4.cn/bbs/Announce/Announce.asp?BoardID=103&ID=33&q=1&r=24

http://www.cnblogs.com/Ninputer/archive/2008/11/22/generic_covariant.html

http://www.cnblogs.com/adam/archive/2007/02/28/659325.html

原文地址:https://www.cnblogs.com/TivonStone/p/1845099.html