泛型类继承全解析

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

 

开放类型与封闭类型
在引入了泛型的概念之后,C#中的所有类型可以被划分为两部分:开放类型和封闭类型。
• 开放类型就是指含有类型参数的类型,包括:
 类型参数本身;  以开放类型为元素类型的数组类型;  包含开放类型的构造类型。
• 封闭类型则是指开放类型以外的所有类型。 这种递归的定义确实有些令人费解:当把一个开放类型中所包含的类型参数都替换成为封闭类型时,该类型也就成为了一个封闭类型。但从计算机的角度来理解,只有封闭类型才可以创建实例,并拥有内存存储,而开放类型不是真正的数据类型。这里“开放”的意思指的是具体的数据类型尚未最终确定。设S和T为类型参数,下列类型都属于开放类型:

 

普通基类与派生泛型类
这应当说是泛型类继承中最简单的一种情况。像普通类一样,派生的泛型类可以从基类继承各种成员,隐藏基类中的同名成员,以及通过重载来实现多态性。至于派生类如何在自己的其他成员中使用类型参数来实现泛型的功能,这和基类没有任何关系。惟一值得提醒的地方在于:不能用派生类中任何涉及到泛型的成员来重载基类中的成员。例如下面的代码定义了一个抽象类A: 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: /// /// 错误的派生泛型类 /// public class GA : 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对抽象类A的属性、构造函数以及方法的重载都是错误的。尽管构造类型GA能够达到重载的目的,但换成GA就会统统失败。泛型类需要保证它的所有构造类型都能够满足基类的要求。对于上面的例子,所定义的成员可以保留,但要去掉不必要的override修饰,并且增加能够满足重载要求的成员定义: /// /// 泛型类:集合Assemble /// public class GA : 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) { } }

 

泛型基类与普通派生类
由于开放类型不能从封闭类型中继承,所以对非泛型类来说,它只能以泛型类的某个构造类型作为基类,而且该构造类型不能含有类型参数。
和前一种情况不同,由于基类和派生类都是封闭类型,基类对派生类的重载要求可以直接确定。例如下面的代码定义了一个泛型类GA,并为其构造类型GA定义了一个派生类B: /// /// 泛型类 /// public abstract class GA { //字段 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 { //重载属性 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类型的字段成员m_v1,那么GA的派生类B所继承的就是int类型的字段成员m_v1。

 

泛型基类与泛型派生类
最为复杂的一种情况莫过于基类和派生类都是泛型类了。在定义了一个泛型类之后,
它自身以及它的某个构造类型都可能成为基类。例如,定义了泛型类GA之后,采用下面的方法来派生新的泛型类都是允许的: public class GB : GA { } public class GC : GA { } public class GD : GA { } 。
再涉及到多参数泛型类时,情况就更加混乱了。如果泛型类定义了多个类型参数,除了泛型类本身,其封闭的构造类型和开放的构造类型都可以作为其他泛型类的基类。例如,定义了泛型类A之后,采用下面的方法来派生新的泛型类都是允许的: public class B : A { } public class C : A { } public class D : A { } public class E : A { } public class F : A> { } public class G : A<A, A> { }。

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

由于带参构造函数和类型限制的原因,还可能导致泛型类的某些构造类型无法作为基类使用。例如下面的定义中,由于泛型类G1对类型参数T的限制,导致在G2的定义中T[]也要有带参构造函数及默认构造函数,并继承IComparable接口。但是数组类型既不可能实现IComparable接口,也不可能提供构造函数,因此不论怎样为S和T添加限制都无法满足继承要求: public class G1 where T : IComparable, new() { 
private T m_value; public G1(T tValue) { m_value = tValue; }
} public class G2 : G1 { } //error
在泛型类之间涉及到的继承和多态性问题更加复杂。如果在泛型类中定义了开放类型的成员,那么以泛型类本身作为基类时,派生类将直接继承这些成员;而如果是以泛型类的某个构造类型作为基类,派生类所继承的成员只是将相应的开放类型替换成为封闭类型。

在下面的代码中,泛型类G2作为G1的派生类,它所继承的字段和方法的类型就都成为T[][]: public class G1 { //字段 protected T[] m_list;
//构造函数 public T[] Generate() { return m_list; }
} public class G2 : G1 { }
作为泛型类继承的一个例子,程序P10_6.cs试图将程序P10_4.cs和P10_5.cs结合起来,希望能够以一个整数数组为索引,对联系人集合进行方便的管理。由于C#不支持多继承(幸亏如此,否则泛型类之间的多继承会使得局面更加难以驾御),这里考虑从程序P10_4.cs中定义的泛型类Relation出发,扩展实现所需的功能。
//程序清单P10_6.cs: using System; using P9_6; using P10_4; namespace P10_6 { public class GenericInheritSample { public static void Main() { IndexedAssemble conAsm = new
IndexedAssemble(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(); } } /// /// 泛型类:索引集合IndexedAssemble /// public class IndexedAssemble : P10_4.Relation 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]; Rig
ht[j] = tmp; } } } } public void Output() { for (int i = 0; i < Length; i++) Right[Left[i]].Output(); } } }
派生的泛型类IndexedAssemble从构造类型Relation中继承,自然Relation中定义的成员Left就成为一个整数数组,该数组在构造函数中进行初始化。Sort方法中的代码按照数组字段Right中各元素的大小关系对Left的元素进行排序,结果是在数组Left中按顺序存放数组Right中对应元素的下标。例如,数组Right中最小的元素下标为2,则Left[0]=2;次小的元素下标为5,则Left[1]=5,依此类推。如果数组Right的元素类型是复杂的引用类型,则在整数数组Left中进行排序远比直接在数组Right中进行排序效率要高。
顺便提一句,Sort方法中使用的是冒泡排序算法,有兴趣的读者可以参考数据结构与算法方面的专著。
对程序P10_6.cs进行编译的命令为: csc /r:P10_4.exe /r:P9_6.dll P10_6.cs
执行程序,可以观察到排序前后输出内容的变化。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值