泛型

 

             C#中最为复杂和强大的一种功能:泛型。毫不夸张的说,泛型的添加从根本上改变了C#的性质。泛型不仅添加了新的语法元素,也添加了不少新的功能,并且导致类库产生了许多改动和升级。

              一个泛型的示例:

  class Gen<T>
    {
        T ob;
        public Gen(T o)
        {
            ob = o;
        }
        public T GetOb()
        {
            return ob;
        }
        public void ShowType()
        {
            Console.WriteLine("Type of T is" + typeof(T));
        }
    }


    class Test
    {
        static void Main(string[] args)
        {
            Gen<int> iOb;
            iOb = new Gen<int>(102);
            iOb.ShowType();
            int v = iOb.GetOb();
            Console.WriteLine("value:" + v);
            Console.WriteLine();


            Gen<string> strOb = new Gen<string>("Generics add power.");
            strOb.ShowType();


            string str = strOb.GetOb();
            Console.WriteLine("value:" + str);
            Console.Read();
        }
    }
  其中T 是类型参数(type parameter)的名称。该名称是一个占位符,它在创建Gen对象时将实际值的类型传递给Gen类。

泛型类型因类型参数的不同而不同

           理解泛型类型的一个关键在于,对某个泛型类型的一个版本的引用并不能与同一个反省类型的另一个版本相兼容。例如:上面的程序中,下列代码将产生错误,从而导致无法编译。

iOb=strOb;    //Wrong!
尽管iOb和strOb都基于Gen<T>,但由于它们的类型实参不同,因此是对不同类型的引用。

泛型实现类型安全

泛型可以自动确保有关Gen的所有操作都是类型安全的。在处理过程中,泛型避免了手工进行转换和编写类型检查代码的必要。

泛型类的通用形式

前面示例中所示的泛型语法是通用的。下面是声明泛型类得语法:

class class-name<type-param-list>{}

类型约束

   C#定义了如下类型的约束:

基类约束:        可以使用“基类约束”来指定某个基类必须出现在类型实参中。这种约束是通过指定基类名称来实现的。该约束有一种变体,称为裸类型约束,其中通过类型参数指定积累,这样就可以在两个类型参数之间建立关系。

接口约束:        可以使用“接口约束”来指定某个类型实参必须实现一个或多个接口。这种约束是通过指定接口名称来实现的。

构造函数约束:可以要求类型实参必须提供一个无参数的构造函数,这杯称为构造函数约束,通过new()指定这种约束。

引用类型约束:可以通过关键字class指定“引用类型约束”,来限制某个类型实参必须是引用类型。

值类型约束:    可以通过关键字struct指定“值类型约束”,来限制某个类型实参必须是值类型。

具体使用:

1.基类约束,可以指定某个类型实参必须继承的基类。基类约束有两个重要的功能。首先,它允许在泛型类中使用由约束指定的基类所定义的成员。例如,可以调用基类的方法或者使用基类的属性。如果没有基类约束,编译器就无法知道某个类型实参拥有哪些成员。通过提供基类约束,编译器将指定所有的类型实参都拥有指定的类型所定义的成员。

基类约束的第二个功能是,确保只适用支持指定基类的类型实参。这意味着对于任意给定的基类约束,类型实参必须要么是基类本身,要么是派生于该基类的类。如果试图使用没有匹配或继承指定基类的类型实参,就会导致编译时错误。

基类约束使用下面形式的where字句:

where T : base-class-name
其中,T是类型参数的名称,base-class-name是基类的名称。这里只能指定一个基类。

 class A
    {
        public void Hello()
        {
            Console.WriteLine("Hello");
        }
    }

    class B : A { }
    class C { }

    class Gen<T> where T : A
    {
        T ob;
        public Gen(T o)
        {
            ob = o;
        }
        public void SayHello()
        {
            ob.Hello();
        }
    }

    class Test
    {
        static void Main(string[] args)
        {
            A a = new A();
            B b = new B();
            C c = new C();

            Gen<A> ga = new Gen<A>(a);
            ga.SayHello();

            Gen<B> gb = new Gen<B>(b);
            gb.SayHello();

            //Gen<C> gc = new Gen<C>(c);
            //gc.SayHello();
            Console.Read();
        }
    }

2.接口约束

        接口约束用于指定某个类型实参必须实现的接口。接口约束的两个主要功能与基类约束完全一样。首先,它允许开发人员在泛型类中使用接口的成员。其次,它确保只能使用实现了特定接口的类型参数。这意味着对于任何给定的接口约束,类型实参要么是接口本身,要么是实现了接口的类。

         接口约束使用的where字句具有以下形式:

          where T :interface-name

其中,T是类型参数的名称,而interface-name是接口的名称。可以通过使用由逗号分隔的列表来同时指定多个接口。如果某个约束同时包含基类和接口,则先指定基类列表在制定接口列表。

 

 interface A
    {
        void Hello();
    }

    class B : A
    {
        #region A 成员
        public void Hello()
        {
            Console.WriteLine("我已经实现了接口A!");
        }
        #endregion
    }
    class C : A
    {
        #region A 成员
        public void Hello()
        {
            Console.WriteLine("我已经实现了接口A!");
        }
        #endregion
    }

    class D { }

    class Gen<T> where T : A
    {
        T ob;
        public Gen(T o)
        {
            ob = o;
        }
        public void SayHello()
        {
            ob.Hello();
        }
    }

    class Test
    {
        static void Main(string[] args)
        {
            B b = new B();
            C c = new C();
            D d = new D();
            Gen<B> gb = new Gen<B>(b);
            gb.SayHello();
            Gen<C> gc = new Gen<C>(c);
            gc.SayHello();
            //Gen<D> gd = new Gen<D>(d);
            //gd.SayHello();
            Console.Read();
        }
    }
3.new()构造函数约束

            new()构造函数约束允许开发人员实例化一个泛型类型的对象。一般情况下,无法创建一个泛型类型参数的实例。然而,new()约束改变了这种情况,它要求类型实参必须提供一个无参数的构造函数(这种无参数的构造函数可以是在没有显示声明构造函数或没有显示定义无参数构造函数时自动提供的默认构造函数)。在使用new()约束的时候,可以通过调用该无参构造函数来创建对象。

    public class MyClass
    {
        public MyClass() { }
        public void print()
        {
            Console.WriteLine("Hello World!");
        }
    }
    public class ClassTest<T> where T : new()
    {
        T ob;
        public ClassTest()
        {
            ob = new T();
        }
    }

    class Test
    {
        static void Main(string[] args)
        {
            ClassTest<MyClass> te = new ClassTest<MyClass>();
            
            Console.Read();
        }
    }
由于使用了new()约束,因此每一个类型实参都必须提供一个无参构造函数。

使用new()时应当注意3点:

第一,它可以与其他约束一起使用,但必须位于约束列表的末端。

第二,new()仅允许开发人员使用无参构造函数来构建一个对象,即使同时参在其他的构造函数。换句话说,不允许给类型参数的构造函数传递实参。

第三,不可以同时使用new()约束和值类型约束。

4.引用类型和值类型约束

       接下来介绍的两个约束允许开发人员限制某个类型实参必须是引用类型,或者必须是值类型。如果引用类型和值类型之间的差别对于泛型代码非常重要,这些约束就非常有用。

引用类型约束:

where T :class
值类型约束:

where T :struct
在这两种情况中,在同时存在其他约束是,class或struct必须位于约束列表的开头。

 public class A { };
    public class ClassT<T> where T : class
    {}

    public class StructT<T> where T : struct
    {}
    class Test
    {
        static void Main(string[] args)
        {
            ClassT<A> m1 = new ClassT<A>();
            //StructT<A> m2 = new StructT<A>();  //error

            //ClassT<A> m1 = new ClassT<A>();    //error
            StructT<int> m2 = new StructT<int>();  
            Console.Read();
        }
    }

使用约束建立两个类型参数之间的关系

基类约束有一些更加有趣的作用,其中之一是它允许开发人员在两个类型参数之间建立一种关系。例如,考虑下面的泛型类声明:

class Gen<T,V> where V:T

在该声明中,where 字句负责告诉编译器传递给V的类型实参必须等同于或继承于传递给T类型的实参。如果在声明Gen类型的对象时,这种关系不存在,那么将导致编译错误。这种类型参数的约束被称为“裸类型约束”

 public class A { };
    public class B : A { }
    public class ClassT<T, V> where T : V
    { }
    class Test
    {
        static void Main(string[] args)
        {
            ClassT<B, A> m2 = new ClassT<B, A>();
            //ClassT<A, B> m2 = new ClassT<A, B>();  //error
            Console.Read();
        }
    }

使用多重约束

同一个参数可以使用多个约束。在这种情况下,需要使用一个由逗号分隔的约束列表。在该列表中,第一个约束必须是class或者struct(如果存在的话),或者基类(如果被指定)。指定class或struct约束的同时也指定基类约束是非法的。接下来必须是所有的接口约束。最后是new()约束。

例如:

第一种情况:           class Gen<T> where T: class ,interface,new()
第二种情况:           class Gen<T> where T: struct ,interface,new()
在使用两个或更多的类型参数时,可以使用多个where字句分别为它们指定约束。例如:

class Gen<T,V> where T:class 
               where V:struct
在这个例子中,Gen接受两个类型实参,它们各有一条where字句。空格用于分隔第一个where字句和第二个where字句。并且,无需使用其他的分隔符,其他的分隔符都是无效的。

创建类型参数的默认值

                     在编写泛型代码时,值类型和参数类型之间的差别可能会产生一些问题。例如希望给一个类型参数的对象指定默认值。对于引用类型,它的默认值为null。而对于非struct的值类型,其默认值为0。struct的默认值是一个struct对象,并且对象中的所有字段都被设置为默认值。因此,如果希望给一个类型参数的变量指定默认值,就会产生疑问。使用哪一个值呢——null、0,还是其他的值?

例如,给定一个名为Test的泛型类,它的声明如下:

 public class Test<T>
    {
        T obj;
        //如果希望给obj指定一个默认值,是使用
        obj=null; //
        //还是
        obj=0;
    }

该问题的解决方法是使用default关键字的另一种形式,如下所示:

default(type)

这是default的运算符形式,无论type是什么类型,它都将生成该类型的默认值。因此,继续前面的例子,为了给obj赋予T类型的默认值,可以使用下面的语句。

obj=default(T);
它适用于所有的类型实参,无论它们是值类型还是引用类型。

    public class Test<T>
    {
        public T obj;
        public Test()
        {
            obj = default(T);
        }
    }
    class NTest
    {
        static void Main(string[] args)
        {
            Test<string> a = new Test<string>();
            Test<int> b = new Test<int>();

            if (a.obj == null)
                Console.WriteLine("a.obj is null");
            if (b.obj == 0)
                Console.WriteLine("b.obj is 0");
            
            Console.Read();
        }
    }
泛型结构

C#允许创建泛型结构。其语法与泛型完全一样。

 public struct XY<T>
    {
        T x;
        T y;
        public XY(T a, T b)
        {
            x = a;
            y = b;
        }
        public T X
        {
            get { return x; }
            set { x = value; }
        }
        public T Y
        {
            get { return y; }
            set { y = value; }
        }
    }
    class NTest
    {
        static void Main(string[] args)
        {
            XY<int> xy = new XY<int>(10, 20);
            XY<double> xy2 = new XY<double>(80.9, 99.0);
            Console.WriteLine(xy.X+",  "+xy.Y);
            Console.WriteLine(xy2.X + ",  " + xy2.Y);
            Console.Read();
        }
    }
泛型方法

通用形式为:

return-type  method-name <type-param-list>(param-list){}
其中,type-param-list 是一个由逗号分隔的类型参数列表。对于泛型方法,类型参数位于方法名之后。

 public class Test
    {
        public static bool CopyInsert<T>(T e, uint index, T[] src, T[] target)
        {
            if (target.Length < src.Length)
                return false;

            for (int i = 0, j = 0; i < src.Length; i++, j++)
            {
                if (i == index)
                {
                    target[j] = e;
                    j++;
                }
                target[j] = src[i];
            }
            return false;
        }
    }
    class NTest
    {
        static void Main(string[] args)
        {
            int[] a = new int[] { 1, 2, 3, 4, 6 };
            int[] b = new int[6];
            Test.CopyInsert(5, 4, a, b);
            //Test.CopyInsert(5.5, 4, a, b);   //当前 5.5 参数类型不一致
            foreach (int i in b)
            {
                Console.WriteLine("当前数:" + i);
            }
            Console.Read();
        }
    }

      1.调用泛型方法时,显示地指定类型实参

              对于大部分的泛型方法调用,由编译器进行隐式的类型推测就足够了,但是还可以显示地制定类型实参。为此,可以在调用方法的时候,在方法名称之后指定类型 实参。例如:
显式地传递string类型:

Test.CopyInsert<string>(5, 4, a, b);
当编译器不能推测T参数的类型或者重写类型推测是,就需要显式地指定类型。

      2.为泛型方法指定约束:

 可以在一个方形方法类型实参列表后面为它指定约束。例如:下面版本的CopyInsert()只能处理引用类型:

 public static bool CopyInsert<T>(T e, uint index, T[] src, T[] target) where T:class{}

泛型委托
像方法一样,委托也可以是泛型的。要声明一个泛型委托,可以使用下面的通用形式:

delegate return-type delete-name <type-parameter-list>(arg-list);
请注意参数类型的位置,它紧跟在委托的名称之后。泛型委托的有点在于,它允许开发人员以类型安全的方式定义一个通用形式,该形式可用于匹配任意兼容的方法。

 下面的程序演示了一个名为SomeOp的泛型委托,它由一个类型参数T。该委托返回类型T并且接受一个T类型的实参。

 delegate T SomeOp<T>(T v);
    class NTest
    {
        static int Sum(int v)
        {
            int result = 0;
            for (int i = 1; i <= v; i++)
            {
                result += i;
            }
            return result;
        }
        static string Reflect(string world)
        {
            string result = "";
            foreach (char c in world)
            {
                result = c + result;
            }
            return result;
        }
        static void Main(string[] args)
        {

            SomeOp<int> tint = Sum;
            SomeOp<string> tstr = Reflect;

            Console.WriteLine(tint(3));
            Console.WriteLine(tstr("Hello World"));
            Console.Read();
        }
    }

泛型接口

反省接口的定义与反省类基本相同。他处理的数据类型现在是通过类型参数指定的。

 public interface ISeries<T>
    {
        T GetNext();
        void Reset();
        void SetStart(T v);
    }

    public interface ByTwos<T> : ISeries<T> { 
    
    }

实现泛型接口的类本身也必须是泛型的。这一点很重要,如果没有定义T,编译将不通过。

泛型接口的类型参数可以像泛型类一样使用约束条件。例如,下面版本的ISeries将对象限制为引用类型:

public interface ISeries<T> where T: class

在实现该版本的ISeries时,实现接口的类必须为T指定同样地约束,如下所示:

public interface ByTwos<T> : ISeries<T> where T:class{ 

比较同一类型参数的实例

  为了允许同一个泛型类型参数的两个对象之间能够进行比较,可以使用由一个名为IComparable的.NET标准接口定义的CompareTo()方法。该接口被所有的C#内置类型实现,包括int,string和double。用户也可以很容易地位自己创建的类实现该接口。

 IComparable接口仅定义了下面的CompareTo方法:  int CompareTo(object obj)

  public class Test : IComparable<Test>
    {
        public int Val;
        public Test(int x)
        {
            Val = x;
        }
        public int CompareTo(Test v)
        {
            return Val - v.Val;
        }
    }

    class NTest
    {
        public static bool IsIn<T>(T what, T[] obs) where T : IComparable
        {
            foreach (T v in obs)
                if (v.CompareTo(what) == 0)
                    return true;

            return false;
        }

        static void Main(string[] args)
        {

            int[] ints = new int[] {1,2,3,4,5,6,7 };

            if (IsIn(2, ints))
                Console.WriteLine("2 is found.");

            if (IsIn(99, ints))
                Console.WriteLine("99 is found.");

            Console.Read();
        }
    }

反省类的层次结构

与非泛型类一样,反省类也可以是类得层次结构的一部分。因此,泛型类可以等同于一个基类或者一个派生类。泛型结构与非泛型结构的主要不同在于,在泛型层次结构中,泛型基类所需的任意类型实参都必须通过派生类向上传递。这类似于构造函数的实参在层次结构中传递的情形。

public class A<T>
    {
        T ob;
        public A(T o)
        {
            ob = o;
        }
        public T GetOb()
        {
            return ob;
        }
    }

    public class B<T> : A<T>
    {
        public B(T o)
            : base(o)
        { }
    }

    class NTest
    {
        public static bool IsIn<T>(T what, T[] obs) where T : IComparable
        {
            foreach (T v in obs)
                if (v.CompareTo(what) == 0)
                    return true;
            return false;
        }
        static void Main(string[] args)
        {
            int[] ints = new int[] { 1, 2, 3, 4, 5, 6, 7 };
            if (IsIn(2, ints))
                Console.WriteLine("2 is found.");
            if (IsIn(99, ints))
                Console.WriteLine("99 is found.");
            Console.Read();
        }
    }

重写泛型类中的虚方法

 public class A<T>
    {
        T ob;
        public A(T o)
        {
            ob = o;
        }
        public virtual T GetOb()
        {
            Console.WriteLine("父类"+ob);
            return ob;
        }
    }

    public class B<T> : A<T>
    {
        T ob;
        public B(T o)
            : base(o)
        {
            ob = o;
        }

        
        public override T GetOb()
        {
            Console.WriteLine("子类"+ob);
            return ob;
        }
    }

    class NTest
    {
        
        static void Main(string[] args)
        {
            A<int> a = new A<int>(123);
            B<int> b = new B<int>(456);
            a.GetOb();
            b.GetOb();
            Console.Read();
        }
    }

重载带类型参数的方法

可以重载使用类型参数声明方法参数的方法。重载带类型参数的方法遵循严格的规则。参数的类型或数量不能相同。但是,参数差别的判定不是基于泛型类型参数,而是基于创建一个构造类型时替换类型参数的类型实参。因此,有可能重载了一个使用类型参数的、看起来正确的方法,但它在实际中却不起作用。

例如,考虑下面的泛型类

public class A<T, V>
    {
        T ob1;
        V ob2;
        public void Set(T o)
        {
            ob1 = o;
        }
        public void Set(V o)
        {
            ob2 = o;
        }
    }

    class NTest
    {

        static void Main(string[] args)
        {
            A<int, int> a = new A<int, int>();
            a.Set(10);   //在以下方法或属性之间调用不明确 ,error
            Console.Read();
        }
    }

这种重载会导致潜在的二义性问题。 在编写A时,没有要求T和V必须是不同类型。

在这个例子中,T和V都被替换为int。使得两个版本的Set()完全一样,从而产生错误。因此Main()在试图调用Set()方法时,就会产生二义性,引起编译错误。

通常,允许重载使用类型参数的方法,只要构造的类型不导致冲突即可。有一点非常重要,类型约束并不参与重载识别。因此,类型约束不能用来消除二义性。和方法一样,带类型参数的构造函数、运算符和索引器也都可以被重载,并遵循相同的规则。

泛型类型的实例化

泛型类型是否会在程序运行的时候是代码膨胀?答:不会。这是因为C#以一种高效的方式来实现泛型,只有在需要的时候才创建新的构造类型。下面是它的处理过程。

在反省类被编译成MSIL时,它以泛型形式保存所有的类型参数。在运行时,如果需要特定类得实例,JIT编译器将使用类型实参替换类型参数,为类构造一个具体的、可执行的代码版本。只要实参类型相同,类创建的每一个实例都使用相同的可执行代码版本。当需要创建一个不同的已构造类型时,编译器会编译一个新版本的类。

通常,对于每一个已构造的类型(类型参数是一个值类型,例如int或double),都会创建一个新的可执行版本的泛型。因此,每一个A<int> 对象都会使用同一个版本的A,而每一个A<Double>类型的对象将使用另一个版本的A.一个版本的Gen对应一个具体的值类型。然而,如果类型实参是一个引用类型,就只存在一个版本的泛型类,它可以处理所有的情况。这是因为所有引用的大小(以字节为单位)都是相同的。因此,只需要一个版本就可以处理所有类型的引用。这种优化也有助于减轻代码的膨胀问题。


使用泛型时的一些局限

1.属性,运算符、索引器和事件不能被泛型化。尽管如此,这些项仍可以用在反省类中,并且可以使用类得泛型类型参数。

2.extern修饰符不能用于泛型方法。

指针类型不能用作类型实参。

如果泛型类包含一个static字段,那么每一个构造类型都会有该字段的独立副本。这意味着同一个构造类型的所有实例都会共享同一个static字段。然而,不同的已构造类型使用不同的字段副本。因此,static字段并不是由所有的已构造类型共享。


评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值