继承与派生类(C# 与 Java)

更新:2007 年 11 月

您可以通过创建一个从现有类派生的新类来扩展现有类的功能。此派生类继承基类的属性,而且您可以根据需要添加或重写方法及属性。

在 C# 中,继承及接口实现均由 : 运算符定义,此运算符与 Java 中的 extends 和 implements 等效。在类声明中,基类应始终位于最左侧。

与 Java 一样,C# 也不支持多重继承,这意味着这些类不能从多个类中继承。不过,您可以使用接口实现此目的,具体操作方法与 Java 中的操作方法相同。

下面的代码使用两个私有成员变量 x 和 y 定义一个名为 CoOrds 的类,x 和 y 表示该点的位置。这两个变量分别通过名为 X 和 Y 属性访问:

public class CoOrds
{
    private int x, y;

    public CoOrds()  // constructor
    {
        x = 0;
        y = 0;
    }

    public int X
    {
        get { return x; }
        set { x = value; }
    }

    public int Y
    {
        get { return y; }
        set { y = value; }
    }
}

按如下方式从 CoOrds 类派生一个名为 ColorCoOrds 的新类:

public class ColorCoOrds : CoOrds

这样,ColorCoOrds 继承了基类的所有字段和方法,您可以根据需要向基类添加新的字段和方法,以在此派生类中提供其他功能。在此示例中,您将添加一个私有成员和多个访问器,以便向该类中添加颜色:

public class ColorCoOrds : CoOrds
{
    private System.Drawing.Color screenColor;


    public ColorCoOrds()  // constructor
    {
        screenColor = System.Drawing.Color.Red;
    }

    public System.Drawing.Color ScreenColor
    {
        get { return screenColor; }
        set { screenColor = value; }
    }
}

派生类的构造函数隐式调用基类的构造函数(在 Java 术语中,称为超类)。在继承中,所有基类构造函数先于派生类构造函数获得调用,并按照这些类在类层次结构中出现的先后顺序调用。

在类型上强制转换为基类

与在 Java 中一样,不能使用对基类的引用访问派生类的成员和方法,即使此基类引用可能包含对此派生类型的某个对象的有效引用。

您可以使用对此派生类型的引用来隐式引用某个派生类:

ColorCoOrds color1 = new ColorCoOrds();
CoOrds coords1 = color1;

在此代码中,基类引用 coords1 包含了 color1 引用的副本。

基关键字

您可以在子类中使用 base 关键字访问基类成员,即使当这些基成员在超类中被重写时也可执行此操作。例如,您可以创建一个派生类,该派生类中包含一个签名与基类中的签名相同的方法。如果该方法以 new 关键字开头,则表示此方法是属于该派生类的全新方法。您还可以使用基关键字再提供一个方法,用于访问基类中的原始方法。

例如,假设 CoOrds 基类有一个名为 Invert() 的方法,它用于交换 x 和 y 坐标。您可以使用下列代码在 ColorCoOrds 派生类中提供此方法的替换方法:

public new void Invert()
{
    int temp = X;
    X = Y;
    Y = temp;
    screenColor = System.Drawing.Color.Gray;
}

正如您所看到的,此方法先交换 x 和 y,然后将坐标的颜色设置为灰色。您可以为此方法提供对基实现的访问,方法是在 ColorCoOrds 中再创建一个方法(如以下方法):

public void BaseInvert()
{
    base.Invert();
}

接着通过调用 BaseInvert() 方法在 ColorCoOrds 对象中调用基方法。

ColorCoOrds color1 = new ColorCoOrds();
color1.BaseInvert();

请记住,如果先将对基类的引用分配给 ColorCoOrds 的一个实例,然后再访问其方法,也可以得到同样的效果:

CoOrds coords1 = color1;
coords1.Invert();

选择构造函数

基类对象总是在任何派生类之前构建,因此,基类构造函数先于派生类构造函数获得执行。如果基类有多个构造函数,则派生类可以决定要调用哪个构造函数。例如,若要添加第二个构造函数,您可以按如下方式修改 CoOrds 类:

public class CoOrds
{
    private int x, y;

    public CoOrds()
    {
        x = 0;
        y = 0;
    }

    public CoOrds(int x, int y)
    {
        this.x = x;
        this.y = y;
    }
}

接着可以使用基关键字更改 ColorCoOrds 类以使用某个特定的可用构造函数:

public class ColorCoOrds : CoOrds
{
    public System.Drawing.Color color;

    public ColorCoOrds() : base ()
    {
        color = System.Drawing.Color.Red;
    }

    public ColorCoOrds(int x, int y) : base (x, y)
    {
        color = System.Drawing.Color.Red;
    }
}

在 Java 中,此功能是通过 super 关键字实现的。

方法重写

通过为已声明方法提供一个新实现,可以让派生类重写基类的方法。Java 和 C# 的一个重要区别是:默认情况下,Java 方法被标记为 virtual,而在 C# 中,必须使用 virtual 修饰符将方法显式标记为 virtual。属性访问器和方法均可以重写,它们的重写方法非常相似。

虚方法

派生类中要重写的方法使用 virtual 修饰符进行声明。在派生类中,已重写的方法是使用 override 修饰符声明的。

override 修饰符表示派生类的一个方法或属性,它代替基类中具有相同名称和签名的方法或属性。要重写的基方法必须声明为 virtual、abstract 或 override:不能按此方式重写非虚方法或非静态方法。已重写和正在重写的方法或属性必须具有同样的访问级别修饰符。

下面的示例演示派生类中使用重写修饰符进行重写的 StepUp 虚方法:

public class CountClass 
{
    public int count; 

    public CountClass(int startValue)  // constructor
    {
        count = startValue;
    }

    public virtual int StepUp()
    {
        return ++count;
    }
}

class Count100Class : CountClass 
{
    public Count100Class(int x) : base(x)  // constructor 
    {
    }

    public override int StepUp()
    {
        return ((base.count) + 100);
    }
}

class TestCounters
{
    static void Main()
    {
        CountClass counter1 = new CountClass(1);
        CountClass counter100 = new Count100Class(1);

        System.Console.WriteLine("Count in base class = {0}", counter1.StepUp());
        System.Console.WriteLine("Count in derived class = {0}", counter100.StepUp());
    } 
}

运行此代码时,可以看到派生类的构造函数使用的是基类中给定的方法体,因此您可以初始化计数成员,而无需复制此代码。输出如下:

Count in base class = 2

Count in derived class = 101

抽象类

抽象类将一个或多个方法或属性声明为抽象。此类方法不具有声明它们的类中所提供的实现,尽管抽象类也可以包含非抽象方法(即已为其提供了实现的方法)。抽象类不能直接实例化,它只能作为一个派生类。此类派生类必须使用 override 关键字为所有抽象方法和属性提供实现,除非派生成员本身被声明为抽象。

下面的示例声明 Employee 抽象类。您还将创建一个名为 Manager 的派生类,它提供在 Employee 类中定义的 Show() 抽象方法的实现:

public abstract class Employee 
{ 
    protected string name;

    public Employee(string name)  // constructor
    { 
        this.name = name;
    }

    public abstract void Show();  // abstract show method
}

public class Manager: Employee
{ 
    public Manager(string name) : base(name) {}  // constructor

    public override void Show()  //override the abstract show method
    {
        System.Console.WriteLine("Name : " + name);
    }
}

class TestEmployeeAndManager
{ 
    static void Main()
    { 
        // Create an instance of Manager and assign it to a Manager reference:
        Manager m1 = new Manager("H. Ackerman");
        m1.Show();

        // Create an instance of Manager and assign it to an Employee reference:
        Employee ee1 = new Manager("M. Knott");
        ee1.Show();  //call the show method of the Manager class
    } 
}

此代码调用 Manager 类提供的 Show() 的实现,并在屏幕上显示员工姓名。输出如下:

Name : H. Ackerman

Name : M. Knott

接口

接口是一种主干类,它包含方法签名但不包含方法实现。因此,接口类似于只包含抽象方法的抽象类。C# 接口与 Java 接口极其相似,而且工作方式也几乎相同。

根据定义,所有接口成员均为公共成员,并且接口不能包含常量、字段(私有数据成员)、构造函数、析构函数或任何类型的静态成员。如果为某个接口成员指定了任何修饰符,编译器将产生一个错误。

您可以从接口派生类,以实现此接口。此类派生类必须为接口的所有方法提供实现,除非派生类被声明为抽象。

接口声明方式与 Java 中的声明方式相同。在接口定义中,属性仅指示它的类型,并仅通过 getset 关键字指示它为只读、只写还是读/写。以下接口声明一个只读属性:

public interface ICDPlayer
{ 
    void Play();  // method signature
    void Stop();  // method signature

    int FastForward(float numberOfSeconds);

    int CurrentTrack  // read-only property
    {
        get;
    } 
}

如果使用冒号代替 Java 中的 implements 关键字,则类可以从此接口中继承。实现类必须按如下方式为所有方法及任何所需的属性访问器提供定义:

public class CDPlayer : ICDPlayer
{
    private int currentTrack = 0;

    // implement methods defined in the interface
    public void Play()
    {
        // code to start CD...
    }

    public void Stop()
    {
        // code to stop CD...
    }

    public int FastForward(float numberOfSeconds)
    {
        // code to fast forward CD using numberOfSeconds...

        return 0;  //return success code
    }

    public int CurrentTrack  // read-only property
    { 
        get 
        { 
            return currentTrack; 
        } 
    }

    // Add additional methods if required...
}

实现多重接口

使用以下语法,可以让类实现多重接口:

public class CDAndDVDComboPlayer : ICDPlayer, IDVDPlayer

如果一个类在成员名称存在多义性的情况下实现多个接口,则使用属性或方法名称的完全限定符来解析此类。换言之,派生类可以使用方法的完全限定名称表明此派生类属于哪个接口,从而解决冲突,这与 ICDPlayer.Play() 中一样。

请参见

概念

C# 编程指南

参考

继承(C# 编程指南)

其他资源

C# 编程语言(针对 Java 开发人员)