第八章 泛型类

1 概述

1.1 引入泛型的原因

先从一个例子来说明这个问题

    class AssembleSample
    {
        static void Main()
        {
            ContactTable cont = new ContactTable(10);
            cont[0] = new Business("李明");
            Assemble a = cont;
            a[1] = new ClassMate("张鹏");
            IOutput i = cont;
            i.Output();
        }
    }

    /// <summary>
    /// 接口IAccessable
    /// </summary>
    public interface IAccessable
    {
        int Length
        {
            get;
        }

        object this[int index]
        {
            get;
            set;
        }

        object GetAt(int index);
        void SetAt(int index, object obj);
    }

    /// <summary>
    /// 抽象类:集合Assemble
    /// </summary>
    public abstract class Assemble:IAccessable
    {
        //字段
        protected object[] m_list;

        //属性
        public int Length
        {
            get
            {
                return m_list.Length;
            }
        }

        //索引函数
        public object this[int index]
        {
            get
            {
                return m_list[index];
            }
            set
            {
                m_list[index] = value;
            }
        }

        //构造函数
        public Assemble(int iLength)
        {
            m_list = new object[iLength];
        }

        //方法
        public object GetAt(int index)
        {
            return m_list[index];
        }

        public void SetAt(int index, object obj)
        {
            m_list[index] = obj;
        }
    }

    /// <summary>
    /// 派生类:联系人集合
    /// </summary>
    public class ContactTable:Assemble,IOutput
    {
        //覆盖索引函数
        public new Contact this[int index]
        {
            get
            {
                return (Contact)m_list[index];
            }
            set
            {
                m_list[index] = value;
            }
        }
        
        //构造函数
        public ContactTable(int iLength)
            : base(iLength)
        {
        }

        //方法
        public new Contact GetAt(int index)
        {
            return (Contact)m_list[index];
        }

        public void SetAt(int index, Contact c)
        {
            m_list[index] = c;
        }

        void IOutput.Output()
        {
            foreach (Contact c in m_list)
            {
                if (c != null)
                    c.Output();
            }
        }

    }
View Code

程序输出结果:

商务:李明先生/女士
办公电话:未知
手机:未知
商务传真:未知

同学:张鹏
住宅电话:未知
办公电话:未知
手机:未知
生日:0001-1-1 0:00:00

请按任意键继续. . .

  上例中的派生类ContactTable是从基类Assemble中继承,但是在它的索引函数以及GetAt方法的定义中,方法成员的返回类型与基类不一致,因此不能算是重载(override)方法,而是覆盖(new)方法。对于基类和派生类的的SetAt方法,由于方法参数不同,它们根本就是两个不同的方法。

  上例中定义了一个集合类Assemble,用于对数组对象进行封装。而Assemble的派生类ContactTable则用于对联系人数组进行封装。这里有两种方法来重用基类提供的功能,其一是像上例中,类ContactTable继承基类的字段成员m_list,由于该成员的类型为Object数组,那么在成员方法中要实现与联系人相关的功能,就需要进行类型转换:

        //方法
        public new Contact GetAt(int index)
        {
            return (Contact)m_list[index];
        }

  类型转换难以保证每次都是成功的,这就导致某些错误在编译时无法被检查出来,而是在运行时发生异常;即使转换成功了,程序执行的效率往往也是低下的。另一种方法是修改ContactTable的定义,使其封装的字段类型成为Contact数组:

public class ContactTable:Assemble,IOutput
{
    //
    protected Contact [] m_list;
    //
}

  这种方法虽然避免了类型转换,但情况却更加恶化了:整个程序几乎没有从继承中获得任何用处,新的集合类的字段是重新定义的,那么使用到该字段的方法成员几乎也都要重新定义。

  不仅如此,这类程序最大的弊端在于对复用能力的限制。如果要以集合的方式来管理一组同学,还要从ContactTable类再派生出一个ClassmateTable类,重新定义类的成员、修改方法代码。即使对现有的类都按照需要定义了相应的集合类,它也是没有扩展能力的。将来开发人员可能会定义各种各样的新派生类,如亲属类Family、朋友类Friend等,要对这些类进行集合管理,每次都要创建一个新的派生类。

  使用C#提供的泛型则几乎一劳永逸地解决了这个问题。下面的例子:

    class GenericSample
    {
        static void Main()
        {
            Assemble<ClassMate> cmAsm = new Assemble<ClassMate>(5);
            cmAsm[0] = new ClassMate("张鹏");
            cmAsm[0].Birthday = new DateTime(1977, 2, 19);
            cmAsm[1] = new ClassMate("王二");
            cmAsm[1].Birthday = new DateTime(1978, 12, 31);

            Assemble<Contact> conAsm = new Assemble<Contact>(10);
            conAsm[0] = cmAsm[0];
            conAsm[1] = cmAsm[1];
            conAsm[2] = new Business("李明");
            for (int i = 0; i < 3; i++)
                conAsm[i].Output();

        }
    }

    /// <summary>
    /// 泛型类:集合Assemble
    /// </summary>
    public class Assemble<T>
    {
        //字段
        protected T[] m_list;

        //属性
        public int Length
        {
            get
            {
                return m_list.Length;
            }
        }

        //索引函数
        public T this[int index]
        {
            get
            {
                return m_list[index];
            }
            set
            {
                m_list[index] = value;
            }
        }

        //构造函数
        public Assemble(int iLength)
        {
            m_list = new T[iLength];
        }

        //方法
        public T GetAt(int index)
        {
            return m_list[index];
        }

        public void SetAt(int index, T obj)
        {
            m_list[index] = obj;
        }
    }

程序输出结果:

同学:张鹏
住宅电话:未知
办公电话:未知
手机:未知
生日:1977-2-19 0:00:00

同学:王二
住宅电话:未知
办公电话:未知
手机:未知
生日:1978-12-31 0:00:00

商务:李明先生/女士
办公电话:未知
手机:未知
商务传真:未知

请按任意键继续. . .

1.2 泛型类的语法定义

  在普通的类定义之后增加了一个类型参数<T>,类型参数表示对数据类型的抽象,可以把它理解为一个替换标记或者是一个占位符,它在类的定义代码中的每次出现都表示一个非特色的数据类型。

  类型参数T可以被替换为其它任意的数据类型

  泛型类的构造函数名称仍然和类名相同,但定义中不包含类型参数。

2 泛型类的成员

  和普通类一样,泛型类可以拥有各种成员,包括非静态和静态的字段、方法、构造函数、索引函数、代表和事件等。泛型类中的所有成员都可以使用类型参数来指代某种数据类型。

2.1 在成员中使用类型参数

  在泛型类的成员中可以自由的使用类型参数来指代数据类型,包括用来定义字段类型和方法成员的返回类型、传递给方法成员的参数类型,以及在方法成员的执行代码中定义局部变量的类型。

  类型参数作为传递给方法的参数使用时,既可以作为一般参数,也可以作为引用参数和输出参数,还可以是数组参数。

  声明了具体的构造类型之后,在调用这些方法时,传递给方法的实际参数必须和替换类型参数的具体类型相一致。

2.2 类型参数的成员和默认值

  类型参数在使用时会被一个具体的类型所取代,这个类型即可以是引用类型,也可以是一个值类型。

  泛型类的定义中,如果使用了类型参数来指代某个变量的类型,这时唯一能肯定的就是该类型是Object的派生类型,那么对该变量所能调用的成员也就只有Object的公有实例成员。参考Object类型定义的公用成员。

  在第二章1.1节中讲过,如果没有在类的定义中为字段定义默认值,也没有在类的实例中为字段指定一个值,这时候字段就会被赋予其类型的默认值。

  很多情况下,也希望将类型的默认值赋予一个变量。对于泛型,允许使用default关键字来说明其默认值:  

default(T)

  例如,可以修改泛型类Assemble<T>的索引函数定义,使得索引超过数据长度时返回类型的默认值,而不是抛出一个异常:

//索引函数
public T this[int index]
{
    get
    {
        if(index>Length)
            retun default(T);
    }
    set
    {
        //
    }
}

2.3 嵌套泛型类

  泛型类的定义还可以是嵌套的,这包括在普通类中定义嵌套泛型类、在泛型类中定义嵌套普通类、在泛型类中定义嵌套泛型类。此时,内外层之间的访问限制和名称使用规则与普通的嵌套成员也都相同,而泛型类的类型参数的作用域也有类似的规定:

  --嵌套的泛型类,其类型参数在外部也不能使用

  --外部的泛型类,其类型参数在内部可以被使用

  下面的例子中,泛型类OuterClass<T>不能使用内部泛型S来定义字段,而泛型类InnerClass<S>则可以使用外部泛型T来定义字段:

    /// <summary>
    /// 泛型类
    /// </summary>
    public class OuterClass<T>
    {
        //字段
        private InnerClass<int> m_inner;
        private T m_v1;

        //private S m_v2;//错误:不能使用内部嵌套类类型参数

        //方法
        public void OutMethod()
        {
            m_inner = new InnerClass<int>(m_v1);
            m_inner.InnerMethod();
        }

        //嵌套泛型类
        public class InnerClass<S>
        {
            //字段
            private OuterClass<double> m_outer;
            private T m_v1;
            private S m_v2;

            //构造函数
            public InnerClass(T t1)
            {
                m_v1 = t1;
            }

            //方法
            public void InnerMethod()
            {
                m_outer = new OuterClass<double>();
                m_outer.OutMethod();
            }
        }
    }

2.4 静态成员

  类的静态成员不属于类的某个实例,而属于类本身所有。

  泛型类的静态成员则更特殊,它既不属于泛型类的某个实例,也不属于泛型类,而是属于泛型类的构造类型。

  在使用泛型类的静态成员时,圆点连接符前面既不是某个具体的对象变量,也不是泛型类的名称,而应当是构造类型的名称。假设有下面的泛型类:

public class Class1<T>
{
    //字段
    public int x;
    public T y;
    //静态字段
    public static int sx;
    public static T sy;
}

  那么下面的代码都是错误的:

Class<T>.sx=10;
Class<int> c1=new Class1<int>();
c1.sy=20;

  正确的写法应当是:

Class<int>.sx=10;
Class<int>.sy=20;

  对于普通的类而言,一个静态字段在内存中只有一份拷贝。对于泛型类而言,为其指定了多少构造类型,一个静态字段就在内存中拥有多少份拷贝。

  对于泛型类的静态构造函数,每个构造类型都会被调用一次。

  下面的例子演示了在泛型类的构造函数和静态构造函数中进行对象和类的计数:

    class StaticMemberSample
    {
        static void Main()
        {
            Person<int> p1 = new Person<int>("Mike");
            Person<int> p2 = new Person<int>("John");
            Person<double> p3 = new Person<double>("Mary");
        }
    }

    /// <summary>
    /// 泛型类
    /// </summary>
    /// <typeparam name="T"></typeparam>
    public class Person<T>
    {
        public string m_name;
        public static int m_object = 0;
        public static int m_classes = -1;

        //构造函数
        public Person(string Name)
        {
            m_name = Name;
            Console.WriteLine(m_name);
            Console.WriteLine("Object before:{0}", m_object);
            m_object++;
            Console.WriteLine("Object after:{0}", m_object);
        }
        //静态构造函数
        static Person()
        {
            Console.WriteLine("Classes before:{0}", m_classes);
            m_classes++;
            Console.WriteLine("Classes after:{0}", m_classes);
        }
    }
View Code
Classes before:-1
Classes after:0
Mike
Object before:0
Object after:1
John
Object before:1
Object after:2
Classes before:-1
Classes after:0
Mary
Object before:0
Object after:1
请按任意键继续. . .

  对于不同的构造类型<int>和<double>,它们各自进行计数。

  此外,泛型类中不允许定义静态的Main方法,因为该方法作为应用程序的入口使用,需要包含在具体的类型当中。

2.5 泛型类中的操作符重载

  允许被重载的操作符,在泛型类中一样可以被重载,但普通类中操作符重载的规则必须得到满足,例如要求声明是公有和静态的。泛型类中操作符重载的特殊之外在于:

  --重载一元操作符时,参数类型应当为当前类型,也就是泛型类(不能是构造类型或普通类型)

  --重载转换操作符时,可以使用类型参数来指代转换的目标类型

  --重载二元操作符时,至少有一个参数类型应为泛型类

3 多参数泛型类

3.1 多个类型参数的定义与使用

public class Assemble1<R,S,T>

  下面为一个完整的示例:

    class MoreGenericSample
    {
        static void Main(string[] args)
        {
            Relation<int, double> sinTable = new Relation<int, double>(180);
            for (int i = 0; i < 180; i++)
            {
                sinTable.Left[i] = i;
                sinTable.Right[i] = Math.Sin(Math.PI * i / 180);
            }

            //求正弦函数
            Console.WriteLine("请输入度数(0~179之间):");
            int x = int.Parse(Console.ReadLine());
            double y = sinTable.Right[x];
            Console.WriteLine("Sin({0}度)={1}", x, y);

            //求反正弦函数
            Console.WriteLine("请输入一个实数(0~1之间):");
            double r = double.Parse(Console.ReadLine());
            int pos;
            for (pos = 0; pos < sinTable.Length; pos++)
            {
                if (Math.Abs(r-sinTable.Right[pos]) < 0.005)
                {
                    break;
                }
            }
            if (pos == sinTable.Length)
                Console.WriteLine("没有找到指定的反正弦值");
            else
                Console.WriteLine("arcsin({0})={1}度", r, sinTable.Left[pos]);
        }
    }

    /// <summary>
    /// 泛型类:关联Relation
    /// </summary>
    public class Relation<L,R>
    {
        //字段
        public L[] Left;
        public R[] Right;

        //属性
        public int Length
        {
            get
            {
                return Left.Length;
            }
        }

        //构造函数
        public Relation(int iLength)
        {
            Left = new L[iLength];
            Right = new R[iLength];
        }

        //方法
        public int FindLeft(L left)
        {
            for (int i = 0; i < Length; i++)
            {
                if (Left[i].Equals(left))
                    return i;
            }
            return -1;
        }

        public int FindRight(R right)
        {
            for (int i = 0; i < Length; i++)
            {
                if (Right[i].Equals(right))
                    return i;
            }
            return -1;
        }
    }
View Code

  泛型类Relation<L,R>所提供的两个方法FindLeft和FindRight分别用于确定数组中元素的位置,其中用到了Object类型提供的Equals方法。这里不用使用操作符"=="来替代。因为操作符左右两边的类型是T,当前不知道它在构造时会成为一个值类型还是一个引用类型。

  尽管可以直接写出求正弦和反正弦函数的方法,但此程序仍然具有一定的实用性,因为本程序可以将泛型类所实现的正弦函数表存放在文件中、数据库中、或者是系统的高速缓存中,在需要时直接读出结果。对于某些复杂的函数,其计算量可能是相当大的。对本例稍加改动,可以将其应用于包括各种初等函数、分布函数在内的各种数表实现,这也是泛型强大功能的一个体现。

  程序输出结果。

请输入度数(0~179之间):
30
Sin(30度)=0.5
请输入一个实数(0~1之间):
0.707
arcsin(0.707)=45度
请按任意键继续. . .

 

3.2 泛型类在方法的标识

  类中不能有两个标识相同的方法成员。对于泛型类也是如此。但在调用方法时,可能会出现二义性,特别对于多参数泛型类

  例如c1.Add(3,5);在方法Class<T>.Add(T,int)和Class<T>.Add(int,T)之间就存在二义性,无法通过编译。如果增加一个方法定义:  

public void Add(int i,int j){}

  调用Add(3,5)就合法了

4 类型限制

  对于泛型类,可以将泛型具体化为任意的数据类型,包括对象类型object、整数类型int等,很多情况下泛型类的设计作者本意并不如此。

  为了防止滥用,同时保证程序的效率,可以对类型参数添加一定限制。通过where关键字来指定泛型必须继承的基类或接口,其书写格式示例为:  

public class Assemble<T> where T:IOutput

  --可以为一个类型参数同时添加多个限制,之间用逗号分割

  --用于限制的其类不能是密封类

  --System程序集中的这4个类不能作为类型参数限制基类:Array、Delegate、Enum、ValueType  

public class Assemble<T> where T:Contact,IOutput

  对于多参数类,可以为其中的一个或多个类型参数添加限制。要为多个类型参数添加限制,每一个都使用where关键字,相互之间通过空格或换行符进行分割。  

public class Relation<L,R> where L:IComparable where R:Contact,IOutput

  用作类型限制的类还可以泛型类,但必须满足限制基类的所有类型参数都在所定义的类型中出现。例如:

public class class1<L,R>{}
public class class2<T> where T:class1<int,T>{}

  而下面的定义是不合法的,因为类型参数S没有出现在泛型类class2<T>的类型参数列表中:

public class class2<T> where T:class1<S,T>{}

  对类型参数的最后一种限制是要求泛型必须提供一个默认的无参构造函数,书写格式是额外附加new关键字及一对括号。如果类型参数同时还有其它限制,应当把new限制放在最后。例如:

public class Assemble<T> where T:IOutput,new()

  类型参数有了new限制后,就可以在泛型类的成员方法中调用构造函数了:

public Assemble(int iLength)
{
    m_list=new T[iLength];
    for(int i=0;i<iLength;i++)
        m_list[i]=new T();
}

  下面为一个完整的示例:

    class GenericConstraintSample
    {
        static void Main(string[] args)
        {
            Assemble<ClassMate> cmAsm = new Assemble<ClassMate>(5);
            cmAsm[0] = new ClassMate("张鹏");
            cmAsm[0].Birthday = new DateTime(1977, 2, 19);
            cmAsm[1] = new ClassMate("王二");
            cmAsm[1].Birthday = new DateTime(1978, 12, 31);

            Assemble<Contact> conAsm = new Assemble<Contact>(10);
            conAsm[0] = cmAsm[0];
            conAsm[1] = cmAsm[1];
            conAsm[2] = new Business("李明");
            conAsm.Output();
        }
    }
    /// <summary>
    /// 泛型类:集合Assemble
    /// </summary>
    public class Assemble<T> where T:IOutput
    {
        //字段
        protected T[] m_list;

        //属性
        public int Length
        {
            get
            {
                return m_list.Length;
            }
        }

        //索引函数
        public T this[int index]
        {
            get
            {
                return m_list[index];
            }
            set
            {
                m_list[index] = value;
            }
        }

        //构造函数
        public Assemble(int iLength)
        {
            m_list = new T[iLength];
        }

        //方法
        public T GetAt(int index)
        {
            return m_list[index];
        }

        public void SetAt(int index, T obj)
        {
            m_list[index] = obj;
        }

        public void Output()
        {
            foreach (T obj in m_list)
            {
                if (obj != null)
                    obj.Output();
            }
        }
    }

  输出结果:

同学:张鹏
住宅电话:未知
办公电话:未知
手机:未知
生日:1977-2-19 0:00:00

同学:王二
住宅电话:未知
办公电话:未知
手机:未知
生日:1978-12-31 0:00:00

商务:李明先生/女士
住宅电话:未知
办公电话:未知
手机:未知
商务传真:未知

请按任意键继续. . .

5 泛型类之间的继承

  普通的继承规则,包括成员的隐藏、重载和访问限制等,原则上都适用于泛型类。但由于泛型类是抽象的而非具体的数据类型,所以泛型类的继承问题是一个既有趣又容易混淆的问题。因为基类和派生类可能只有一个是泛型类,也可能二者都是。下面对这3种情况进行说明,在讲之前,首先要引入开放类型与封闭类型的概念。

5.1 开放类型与封闭类型

  在引入了泛型的概念之后,C#中的所有类型可以划分为两部分:开放类型和封闭类型。

  开放类型是指含有类型参数<T>的类型,包括:

    --类型参数本身

    --以开发类型为元素类型的数组类型

    --包含开放类型的构造类型

  封闭类型是指开发类型以外的所有类型。当把一个开放类型中所包含的类型参数都替换成封闭类型时,该类型也就成为了一个封闭类型。

  从计算机的角度来理解,只有封闭类型才可以创建实例,并拥有内存存储,而开放类型不是真正的数据类型。“开放”的意思指的是具体的数据类型尚未最终确定。

  开放类型不能从封闭类型中继承。

5.2 普通基类与派生泛型类

  这是泛型类继承中最简单的一种情况。和普通类一样,派生的泛型类可以从基类继承各种成员,隐藏基类中的同名成员,以及通过重载来实现多态性。

  唯一值得提醒的地方在于:

    --不能用派生类中任何涉及到泛型的成员来重载基类中的成员。

  例如:

public abstract class A
{
    //字段
    protected int m_v1;

    //虚拟属性
    public virtual int V1
    {
        get
        {
            return m_v1;
        }
        set
        {
            m_v1_value;
        }
    }

    //构造函数
    public A(int iValue)
    {
        m_v1=iValue;
    }

    //抽象方法
    public abstract void FA(int iValue);
}

  由于该抽象类中定义了抽象方法及带参数的构造函数,其派生类就必须对抽象方法进行重载,并且定义具有相同形参的构造方法。看下面定义的泛型派生类GA<T>:

/// <summary>
/// 错误的派生泛型类
/// </summary>
public class GA<T>:A
{
    //字段
    protected new T m_v1;

    //重载属性
    public override T V1 //error
    {
        get
        {
            return m_v1;
        }
    }

    //构造函数
    public GA(T tp) //error
    {
        m_v1=tp;
    }

    //重载方法
    public override void FA(T tp) //error
    {
    }
}

  上面的派生类GA<T>中,对类A的属性、构造函数、方法的重载都是错误的。因为当构造类型换成GA<string>时就会统统失败。

  泛型类需要保证它的所有构造类型都能够满足基类的要求。

  对于上面的例子,去掉不必要的override,并且增加能够满足重载要求的成员定义就可以了:

public class GA<T>:A
{
    //字段
    protected new T m_v1;
    
    //属性
    public new T V1
    {
        get
        {
            return m_v1;
        }
    }

    //构造函数
    public GA(int iValue):base(iValue)
    {
    }

    public GA(T tp):base(1)
    {
        m_v1=tp;
    }

    //方法
    public override void FA(int iValue)
    {
        base.m_v1*=iValue;
    }
    
    public void FA(T tp)
    {
    }
}

5.3 泛型基类与普通派生类

  由于开放类型不能从封闭类型中继承,所以对非泛型类来说,它只能以泛型类的某个构造类型作为基类,而且该构造类型不能含用类型参数。

/// 泛型基类
public abstract class GA<T>
{
    //字段
    protected T m_v1;
    
    //虚拟属性
    public virtual T V1
    {
        get
        {
            return m_v1;
        }
    }

    //构造函数
    public GA(int iValue)
    {
    }
    public GA(T tp)
    {
        m_v1=tp;
    }

    //抽象方法
    public abstract void FA(T tp);
}

///派生类
public class B:GA<int>
{
    //重载属性
    public override int V1
    {
        get
        {
            return m_v1;
        }
    }

    //构造函数
    public B(int i):base(i)
    {
    }

    //重载方法
    public override void FA(int tp)
    {
        m_v1*=tp;
    }
}

  此时派生类和基类都是封闭类型,派生类的所有成员,包括从基类中继承的成员,当然也都要求是封闭类型。如果泛型类中定义了开放类型的成员,那么需要通过作为基类的构造函数来决定派生类所继承的成员。

  上面的例子中,泛型类GA<T>定义了一个T类型的字段成员m_v1,那么GA<int>的派生类所继承的就是int类型的字段成员m_v1。

5.4 泛型基类与泛型派生类

  这是最复杂一种继承方式。在定义了一个泛型类之后,它自身以及它的某个构造类型都可能成为基类。例如,定义了泛型类GA<T>之后,采用下面的方法来派生新的泛型类都是允许的:

public class GB<T>:GA<T>{}
public class GC<T>:GA<int>{}
public class GC<U>:GA<double>{}

  如果是多参数泛型类时,情况就更加复杂了。如果泛型类定义了多个类型参数,除了泛型类本身,其封闭的构造类型和开放的构造类型都可以作为其它泛型类的基类。例如,定义了泛型类A<S,T>之后,采用下面的方法来派生新的泛型类都是允许的:

public class B<S,T> :A <S,T>{}
public class C<R,S,T> : A<S,T>{}
public class D<U> : A<U,string>{}
public class E<V> : A<string,int>{}
public class F<S,T> : A<int,A<int,string>>{}
public class G<U,V> : A<A<U,V>,A<int,U>>{}

  这种继承方式的限制在于:基类中如果出现了类型参数,那么所有这些类型参数都必须在派生类中定义中出现。

  不过,在派生类定义时所指定的类型参数的名称不一定要和基类定义相同,如上面的D<U>,E<V>,G<U,V>。而下面的定义就是不合法的:

public class B<S> : A<S,T>{} //错误:T未在B的定义中出现

  如果基类和派生类中同时出现了某个类型参数,那么二者的含义是一致的:它们指代同一个开放类型,而最终在使用时被同一个封闭类型取代。

  如果为泛型类定义了类型限制,在继承过程中就必须保持这种类型限制的一致性。

public class Assemble<T> where T : IComparable,new(){}
public class G1<T> where T : new(){}
public class G2<S,T> : G1<S> where S:new(){}
public class G3<R,S,T> :G2<Assemble<R>,string> where R: IComparable,new(){}

  上面的各个泛型类的类型限制都是相互关联的。G1<T>为类型参数T添加了new限制(即无参构造函数限制),而G2<S,T>的基类G1<S>,就必须为类型参数S添加new限制;同样,由于G3<R,S,T>的继承定义中出现了构造类型G2<Assemble<R>,string>,而泛型类Assemble<T>中同时定义了IComparable接口限制和new限制,这种限制就必须传递给G3类型参数R。

  由于带参构造函数和类型限制的原因,还可能导致泛型类的某些构造类型无法作为基类使用。例如下面的定义中,由于泛型类G1<T>对类型参数T的限制,导致G2<S,T>的定义中T[]也要有带参构造函数及默认构造函数,并继承IComparable接口。但是数组类型既不可能实现IComparable接口,也不可能提供构造函数,因此不论怎样为S和T添加限制都无法满足继承要求:

public class G1<T> where T : IComparable,new()
{
    private T m_value;
    public G1 (T tValue)
    {
        m_value=tValue);
    }
}

public class G2<S,T> : G1<T[]>{} //error

  在泛型类之间涉及到的继承和多态性问题更加复杂。如果在泛型类中定义了开放类型的成员,那么以泛型类本身作为基类时,派生类将直接继承这些成员;而如果是以泛型类的某个构造类型作为基类,派生类所继承的成员只是将相应的开放类型替换成为封闭类型。

  在下面的代码中,泛型类G2<S,T>作为G1<T[]>的派生类,它所继承的字段和方法的类型就都成为T[][]:

public class G1<T>
{
    //字段
    protected T[] m_list;
    
    //属性
    public T[] Generate()
    {
        return m_list;
    }
}

public class G2<S,T>:G1<T[]>
{
}

  下面示例一个完整的程序:

    class GenericInheritSample
    {
        static void Main()
        {
            IndexedAssemble<Contact> conAsm = new IndexedAssemble<Contact>(5);
            conAsm[0] = new Contact("Mike");
            conAsm[1] = new ClassMate("David");
            conAsm[2] = new Business("李明");
            conAsm[3]=new Business("John");
            conAsm[4] = new Business("White");

            Console.WriteLine("初始顺序:");
            conAsm.Output();
            conAsm.Sort();
            Console.WriteLine("排序后:");
            conAsm.Output();

        }
    }
    /// <summary>
    /// 泛型类:索引集合
    /// </summary>
    public class IndexedAssemble<T> : Relation<int,T> where T:IOutput,IComparable
    {
        //索引函数
        public T this[int index]
        {
            get
            {
                return Right[index];
            }
            set
            {
                Right[index] = value;
            }
        }

        //构造函数
        public IndexedAssemble(int iLength)
            : base(iLength)
        {
            for (int i = 0; i < Length; i++)
                Left[i] = i;
        }

        //方法
        public void Sort()
        {
            T tmp;
            for (int i=Length-1;i>0;i--)
            {
                for(int j=0;j<i;j++)
                {
                    if(Right[Left[j]].CompareTo(Right[Left[j+1]])>0)
                    {
                        tmp=Right[j+1];
                        Right[j+1]=Right[j];
                        Right[j]=tmp;
                    }
                }
            }
        }

        public void Output()
        {
            for (int i = 0; i < Length; i++)
                Right[Left[i]].Output();
        }
    }
View Code

 

  派生的泛型类IndexedAssemble<T>从构造类型Relation<int,T>中继承,自然Relation中的定义的成员Left就成为一个整数数组,该数组在构造函数中进行初始化。

  程序输出结果为:

 
  

初始顺序:
姓名:Mike
住宅电话:未知
办公电话:未知
手机:未知

 
  

同学:David
住宅电话:未知
办公电话:未知
手机:未知
生日:0001-1-1 0:00:00

 
  

商务:李明先生/女士
住宅电话:未知
办公电话:未知
手机:未知
商务传真:未知

 
  

商务:John先生/女士
住宅电话:未知
办公电话:未知
手机:未知
商务传真:未知

 
  

商务:White先生/女士
住宅电话:未知
办公电话:未知
手机:未知
商务传真:未知

 
  

排序后:
同学:David
住宅电话:未知
办公电话:未知
手机:未知
生日:0001-1-1 0:00:00

 
  

商务:John先生/女士
住宅电话:未知
办公电话:未知
手机:未知
商务传真:未知

 
  

姓名:Mike
住宅电话:未知
办公电话:未知
手机:未知

 
  

商务:White先生/女士
住宅电话:未知
办公电话:未知
手机:未知
商务传真:未知

 
  

商务:李明先生/女士
住宅电话:未知
办公电话:未知
手机:未知
商务传真:未知

 
  

请按任意键继续. . . 

 

6 小结

  泛型是继承之外另一个能够带来高度可重用性的技术。在C#语言中,面向对象的特性和泛型特性相辅相成,这使得开发人员能够在更高的抽象层次上进行代码的重用。同时泛型类的引入也使用C#的继承机制更加丰富和完善。

  泛型类的抽象性直接来源于类型参数,它在泛型类的定义中表示抽象数据类型,而在使用时将被具体的数据类型取代。通过对类型参数的限制,既能够避免对泛型的滥用、提高程序的效率,还能够为类型参数本身带来各种功能。学习泛型类的关键就在于对类型参数的掌握。

转载于:https://www.cnblogs.com/boywg/p/4130730.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值