正文

深入剖析C#继承机制2006-02-27 17:07:00

【评论】 【打印】 【字体: 】 本文链接:http://blog.pfan.cn/Csharpsky/10598.html

分享到:

2) 隐藏基类成员

  想想看,如果所有的类都可以被继承,继承的滥用会带来什么后果?类的层次结构体系将变得十分庞,大类之间的关系杂乱无章,对类的理解和使用都会变得十分困难。有时候,我们并不希望自己编写的类被继承。另一些时候,有的类已经没有再被继承的必要。C#提出了一个密封类(sealed class)的概念,帮助开发人员来解决这一问题。

  密封类在声明中使用sealed 修饰符,这样就可以防止该类被其它类继承。如果试图将一个密封类作为其它类的基类,C#将提示出错。理所当然,密封类不能同时又是抽象类,因为抽象总是希望被继承的。

  在哪些场合下使用密封类呢?密封类可以阻止其它程序员在无意中继承该类。而且密封类可以起到运行时优化的效果。实际上,密封类中不可能有派生类。如果密封类实例中存在虚成员函数,该成员函数可以转化为非虚的,函数修饰符virtual 不再生效。

  让我们看下面的例子:

bstract class A
{
public abstract void F( ) ;
}
sealed class B: A
{
public override void F( )
{ // F 的具体实现代码 }
}

  如果我们尝试写下面的代码

class C: B{ }

  C#会指出这个错误,告诉你B 是一个密封类,不能试图从B 中派生任何类。

  (3) 密封方法

  我们已经知道,使用密封类可以防止对类的继承。C#还提出了密封方法(sealedmethod) 的概念,以防止在方法所在类的派生类中对该方法的重载。对方法可以使用sealed 修饰符,这时我们称该方法是一个密封方法。

  不是类的每个成员方法都可以作为密封方法密封方法,必须对基类的虚方法进行重载,提供具体的实现方法。所以,在方法的声明中,sealed 修饰符总是和override 修饰符同时使用。请看下面的例子代码:

using System ;
class A
{
public virtual void F( )
{ Console.WriteLine("A.F") ; }
public virtual void G( )
{ Console.WriteLine("A.G") ; }
}
class B: A
{
sealed override public void F( )
{ Console.WriteLine("B.F") ; }
override public void G( )
{ Console.WriteLine("B.G") ; }
}
class C: B
{
override public void G( )
{ Console.WriteLine("C.G") ; }
}

  类B 对基类A 中的两个虚方法均进行了重载,其中F 方法使用了sealed 修饰符,成为一个密封方法。G 方法不是密封方法,所以在B 的派生类C 中,可以重载方法G,但不能重载方法F。

  (4) 使用 new 修饰符隐藏基类成员

  使用 new 修饰符可以显式隐藏从基类继承的成员。若要隐藏继承的成员,请使用相同名称在派生类中声明该成员,并用 new 修饰符修饰它。

  请看下面的类:

public class MyBase
{
public int x ;
public void MyVoke() ;
}

  在派生类中用 MyVoke名称声明成员会隐藏基类中的 MyVoke方法,即:

public class MyDerived : MyBase
{ new public void MyVoke (); }

  但是,因为字段 x 不是通过类似名隐藏的,所以不会影响该字段。

  通过继承隐藏名称采用下列形式之一:

   a、引入类或结构中的常数、指定、属性或类型隐藏具有相同名称的所有基类成员。

   b、引入类或结构中的方法隐藏基类中具有相同名称的属性、字段和类型。同时也隐藏具有相同签名的所有基类方法。

   c、引入类或结构中的索引器将隐藏具有相同名称的所有基类索引器。

  注意:在同一成员上同时使用 new 和 override 是错误的。同时使用 new 和 virtual 可保证一个新的专用化点。在不隐藏继承成员的声明中使用 new 修饰符将发出警告。

  示例1:在该例中,基类 MyBaseC 和派生类 MyDerivedC 使用相同的字段名 x,从而隐藏了继承字段的值。该例说明了 new 修饰符的使用。同时也说明了如何使用完全限定名访问基类的隐藏成员。

using System ;
public class MyBase
{
public static int x = 55 ;
public static int y = 22 ;
}
public class MyDerived : MyBase
{
new public static int x = 100; // 利用new 隐藏基类的x
public static void Main()
{
// 打印x:
Console.WriteLine(x);
//访问隐藏基类的 x:
Console.WriteLine(MyBase.x);
//打印不隐藏的y:
Console.WriteLine(y);
}
}

  输出: 100 55 22

  如果移除 new 修饰符,程序将继续编译和运行,但您会收到以下警告:

The keyword new is required on 'MyDerivedC.x' because it hides inherited member 'MyBaseC.x'.

  如果嵌套类型正在隐藏另一种类型,如下例所示,也可以使用 new 修饰符修改此嵌套类型。

四、多级继承

  一些面向对象语言允许一个类从多个基类中继承,而另一些面向对象语言只允许从一个类继承,但可以随意从几个接口或纯抽象类中继承。

  只有C++支持多级继承,许多程序员对此褒贬不一。多级继承常会引起继承来的类之间的混乱,继承而来的方法往往没有唯一性,所以C#中类的继承只可以是一个,即子类只能派生于一个父类,而有时你必须继承多个类的特性,为了实现多重继承必须使用接口技术,下面是对接口的多重继承进行介绍:

using System ;
//定义一个描述点的接口
interface IPoint
{
int x {
get ;
set ;
}
int y {
get ;
set ;
}
}
interface IPoint2
{
int y {
get ;
set ;
}
}
//在point中继承了两个父类接口,并分别使用了两个父类接口的方法
class Point:IPoint,IPoint2
{
//定义两个类内部访问的私有成员变量
private int pX ;
private int pY ;
public Point(int x,int y) {
pX=x ;
pY=y ;
}
//定义的属性,IPoint接口方法实现
public int x
{
get
{ return pX ; }
set
{ pX =value ; }
}
//IPoint1接口方法实现
public int y
{
get
{ return pY ; }
set
{ pY =value ; }
}
}
class Test
{
private static void OutPut( IPoint p )
{ Console.WriteLine("x={0},y={1}",p.x,p.y) ; }
public static void Main( ) {
Point p =new Point(15,30) ;
Console.Write("The New Point is:") ;
OutPut( p ) ;
string myName =Console.ReadLine( ) ;
Console.Write("my name is {0}", myName) ;
}
}

五、继承与访问修饰符

  访问修饰符是一些关键字,用于指定声明的成员或类型的可访问性。类的继承中有四个访问修饰符: public protected internal private。使用这些访问修饰符可指定下列五个可访问性级别: public protected internal internal protected private。

声明的可访问性 意义
public 访问不受限制。
protected 访问仅限于包含类或从包含类派生的类型。
internal 访问仅限于当前项目。
protected internal 访问仅限于从包含类派生的当前项目或类型。
private 访问仅限于包含类型。

  1、继承中关于可访问域的一些问题

  基类的所有成员(实例构造函数、析构函数和静态构造函数除外)都由派生类型继承。这甚至包括基类的私有成员。但是,私有成员的可访问域只包括声明该成员的类型的程序文本。在下面的示例中

class A
{
int x ;
static void F(B b) {
b.x = 1 ; // 对
}
}
class B: A
{
static void F(B b) {
b.x = 1 ; // 错误
}
}

  类 B 继承类 A 的私有成员 x。因为该成员是私有的,所以只能在 A 的"类体"中对它进行访问。因此,对 b.x 的访问在 A.F 方法中取得了成功,在 B.F 方法中却失败了。

  2、继承中关于属性的一些问题

  和类的成员方法一样,我们也可以定义属性的重载、虚属性、抽象属性以及密封属性的概念。与类和方法一样,属性的修饰也应符合下列规则:

  属性的重载

  1. 在派生类中使用修饰符的属性,表示对基类中的同名属性进行重载。

  2. 在重载的声明中,属性的名称、类型、访问修饰符都应该与基类中被继承的属性一致。

  3. 如果基类的属性只有一个属性访问器,重载后的属性也应只有一个。但如果基类的属性同时包含get 和set 属性访问器,重载后的属性可以只有一个,也可以同时有两个属性访问器。

  注意:与方法重载不同的是,属性的重载声明实际上并没有声明新的属性,而只是为已有的虚属性提供访问器的具体实现。

  虚属性

  1. 使用virtual 修饰符声明的属性为虚属性。

  2. 虚属性的访问器包括get 访问器和set 访问器,同样也是虚的。

  抽象属性

  1. 使用abstract 修饰符声明的属性为抽象属性。

  2. 抽象属性的访问器也是虚的,而且没有提供访问器的具体实现。这就要求在非虚的派生类中,由派生类自己通过重载属性来提供对访问器的具体实现。

  3. abstract 和override 修饰符的同时使用,不但表示属性是抽象的,。而且它重载了基类中的虚属性这时属性的访问器也是抽象的。

  4. 抽象属性只允许在抽象类中声明。

  5. 除了同时使用abstract 和override 修饰符这种情况之外,static, virtual, override和abstract 修饰符中任意两个不能再同时出现。

  密封属性

  1. 使用sealed 修饰符声明的属性为密封属性。类的密封属性不允许在派生类中被继承。密封属性的访问器同样也是密封的。

  2. 属性声明时如果有sealed 修饰符,同时也必须要有override 修饰符。

  从上面可以看出,属性的这些规则与方法十分类似。对于属性的访问器,我们可以把get 访问器看成是一个与属性修饰符相同、没有参数、返回值为属性的值类型的方法,把set 访问器看成是一个与属性修饰符相同、仅含有一个value 参数、返回类型为void 的方法。看下面的程序:

using System ;
public enum sex
{ woman, man, } ;
abstract public class People
{
private string s_name;
public virtual string Name
{
get
{ return s_name ; }
}
private sex m_sex ;
public virtual sex Sex
{
get
{ return m_sex ; }
protected string s_card;
public abstract string Card
{ get; set; }
}

  上面的例子中声明了"人"这个类,人的姓名Name 和性别Sex 是两个只读的虚属性:身份证号Card 是一个抽象属性,允许读写,因为类People 中包含了抽象属性Card,所以People 必须声明是抽象的。下面我们为住宿的客人编写一个类类从People 中继承。再看下面的程序:

class Customer: People
{
string s_no ;
int i_day ;
public string No
{
get
{ return s_no ; }
set
{
if (s_no != value)
s_no = value;
}
}
public int Day
{
get
{ return i_day ; }
set
{
if (i_day != value)
i_day = value;
}
}
public override string Name
{
get { return base.Name; }
}
public override sex Sex
{
get { return base.Sex }
}
public override string Card
{
get
{ return s_ card ; }
set
{ s_ card = value ; }
}
}

  在类Customer 中,属性Name、 Sex 和Card 的声明都加上了override 修饰符,属性的声明都与基类People 中保持一致。Name 和Sex 的get 访问器,Card 的get 和set访问器都使用了base 关键字来访问基类People 中的访问器属性。Card 的声明重载了基类People 中的抽象访问器。这样,在Customer 类中没有抽象成员的存在,Customer可以是非虚的。

3、继承中对使用可访问性级别的限制

  声明类型时,最重要的是查看该类型是否必须"至少"与其他成员或类型"具有同样的可访问性"。例如,直接基类必须至少与派生类具有同样的可访问性。以下声明将导致编译器错误,因为基类 BaseClass 的可访问性小于 MyClass:

class BaseClass {...}
public class MyClass: BaseClass {...} // Error

  下表汇总了对使用声明的可访问性级别的限制。

上下文 备注
类类型的直接基类必须至少与类类型本身具有同样的可访问性。
接口 接口类型的显式基接口必须至少与接口类型本身具有同样的可访问性。
委托 委托类型的返回类型和参数类型必须至少与委托类型本身具有同样的可访问性。
常数 常数的类型必须至少与常数本身具有同样的可访问性。
字段 字段的类型必须与至少字段本身具有同样的可访问性。
方法 方法的返回类型和参数类型必须至少与方法本身具有同样的可访问性。
属性 属性的类型必须至少与属性本身具有同样的可访问性。
事件 事件的类型必须至少与事件本身具有同样的可访问性。
索引器 索引器的类型和参数类型必须至少与索引器本身具有同样的可访问性。
运算符 运算符的返回类型和参数类型必须至少与运算符本身具有同样的可访问性。
构造函数 构造函数的参数类型必须至少与构造函数本身具有同样的可访问性。

  示例:以下示例包含不同类型的错误声明。每个声明后的注释指示了预期的编译器错误。

using System ;
delegate int MyDelegate( ) ;
class B
{ // 定义一个私有的函数:
static int MyPrivateMethod()
{ return 0 ; }
}
public class A
{ // 字段定义:
public B myField = new B();// 错误: 类型B与A字段A.myField级别不同
// 构造函数:
public readonly B myConst = new B(); //错误: 类型B是仅读的
//方法:
public B MyMethod()
{
return new B();
}
//属性:
public B MyProp
{
set { }
}
public static B operator + (A m1, B m2)
{
return new B();
}
static void Main()
{
Console.Write("Compiled successfully");
}
}

阅读(1974) | 评论(0)


版权声明:编程爱好者网站为此博客服务提供商,如本文牵涉到版权问题,编程爱好者网站不承担相关责任,如有版权问题请直接与本文作者联系解决。谢谢!

评论

暂无评论
您需要登录后才能评论,请 登录 或者 注册