继承 - 派生用于创建更具体的行为的类型

继承(以及封装和多态性)是面向对象的编程的三个主要特征之一。 通过继承,可以创建新类,以便重用、扩展和修改在其他类中定义的行为。 其成员被继承的类称为“基类”,继承这些成员的类称为“派生类”。 派生类只能有一个直接基类。 但是,继承是可传递的。 如果 ClassC 派生自 ClassB,并且 ClassB 派生自 ClassA,则 ClassC 将继承在 ClassBClassA 中声明的成员。

注意

结构不支持继承,但它们可以实现接口。

从概念上讲,派生类是基类的专门化。 例如,如果有一个基类 Animal,则可以有一个名为 Mammal 的派生类,以及另一个名为 Reptile 的派生类。 MammalAnimalReptile 也是 Animal,但每个派生类表示基类的不同专门化。

接口声明可以为其成员定义默认实现。 这些实现通过派生接口和实现这些接口的类来继承。 有关默认接口方法的详细信息,请参阅关于接口的文章。

定义要从其他类派生的类时,派生类会隐式获得基类的所有成员(除了其构造函数和终结器)。 派生类可以重用基类中的代码,而无需重新实现。 可以在派生类中添加更多成员。 派生类扩展了基类的功能。

下图显示一个类 WorkItem,它表示某个业务流程中的工作项。 像所有类一样,它派生自 System.Object 且继承其所有方法。 WorkItem 会添加其自己的六个成员。 这些成员中包括一个构造函数,因为不会继承构造函数。 类 ChangeRequest 继承自 WorkItem,表示特定类型的工作项。 ChangeRequest 将另外两个成员添加到它从 WorkItemObject 继承的成员中。 它必须添加自己的构造函数,并且还添加了 originalItemID。 属性 originalItemID 使 ChangeRequest 实例可以与向其应用更改请求的原始 WorkItem 相关联。

Diagram that shows class inheritance

下面的示例演示如何在 C# 中表示前面图中所示的类关系。 该示例还演示了 WorkItem 替代虚方法 Object.ToString 的方式,以及 ChangeRequest 类继承该方法的 WorkItem 的实现方式。 第一个块定义类:

// WorkItem implicitly inherits from the Object class.
public class WorkItem
{
    // Static field currentID stores the job ID of the last WorkItem that
    // has been created.
    private static int currentID;

    //Properties.
    protected int ID { get; set; }
    protected string Title { get; set; }
    protected string Description { get; set; }
    protected TimeSpan jobLength { get; set; }

    // Default constructor. If a derived class does not invoke a base-
    // class constructor explicitly, the default constructor is called
    // implicitly.
    public WorkItem()
    {
        ID = 0;
        Title = "Default title";
        Description = "Default description.";
        jobLength = new TimeSpan();
    }

    // Instance constructor that has three parameters.
    public WorkItem(string title, string desc, TimeSpan joblen)
    {
        this.ID = GetNextID();
        this.Title = title;
        this.Description = desc;
        this.jobLength = joblen;
    }

    // Static constructor to initialize the static member, currentID. This
    // constructor is called one time, automatically, before any instance
    // of WorkItem or ChangeRequest is created, or currentID is referenced.
    static WorkItem() => currentID = 0;

    // currentID is a static field. It is incremented each time a new
    // instance of WorkItem is created.
    protected int GetNextID() => ++currentID;

    // Method Update enables you to update the title and job length of an
    // existing WorkItem object.
    public void Update(string title, TimeSpan joblen)
    {
        this.Title = title;
        this.jobLength = joblen;
    }

    // Virtual method override of the ToString method that is inherited
    // from System.Object.
    public override string ToString() =>
        $"{this.ID} - {this.Title}";
}

// ChangeRequest derives from WorkItem and adds a property (originalItemID)
// and two constructors.
public class ChangeRequest : WorkItem
{
    protected int originalItemID { get; set; }

    // Constructors. Because neither constructor calls a base-class
    // constructor explicitly, the default constructor in the base class
    // is called implicitly. The base class must contain a default
    // constructor.

    // Default constructor for the derived class.
    public ChangeRequest() { }

    // Instance constructor that has four parameters.
    public ChangeRequest(string title, string desc, TimeSpan jobLen,
                         int originalID)
    {
        // The following properties and the GetNexID method are inherited
        // from WorkItem.
        this.ID = GetNextID();
        this.Title = title;
        this.Description = desc;
        this.jobLength = jobLen;

        // Property originalItemID is a member of ChangeRequest, but not
        // of WorkItem.
        this.originalItemID = originalID;
    }
}

下一个块显示如何使用基类和派生类:

// Create an instance of WorkItem by using the constructor in the
// base class that takes three arguments.
WorkItem item = new WorkItem("Fix Bugs",
                            "Fix all bugs in my code branch",
                            new TimeSpan(3, 4, 0, 0));

// Create an instance of ChangeRequest by using the constructor in
// the derived class that takes four arguments.
ChangeRequest change = new ChangeRequest("Change Base Class Design",
                                        "Add members to the class",
                                        new TimeSpan(4, 0, 0),
                                        1);

// Use the ToString method defined in WorkItem.
Console.WriteLine(item.ToString());

// Use the inherited Update method to change the title of the
// ChangeRequest object.
change.Update("Change the Design of the Base Class",
    new TimeSpan(4, 0, 0));

// ChangeRequest inherits WorkItem's override of ToString.
Console.WriteLine(change.ToString());
/* Output:
    1 - Fix Bugs
    2 - Change the Design of the Base Class
*/

抽象方法和虚方法

基类将方法声明为 virtual 时,派生类可以使用其自己的实现override该方法。 如果基类将成员声明为 abstract,则必须在直接继承自该类的任何非抽象类中重写该方法。 如果派生类本身是抽象的,则它会继承抽象成员而不会实现它们。 抽象和虚拟成员是多形性(面向对象的编程的第二个主要特征)的基础。 有关详细信息,请参阅多态性

抽象基类

如果要通过使用 new 运算符来防止直接实例化,则可以将类声明为抽象。 只有当一个新类派生自该类时,才能使用抽象类。 抽象类可以包含一个或多个本身声明为抽象的方法签名。 这些签名指定参数和返回值,但没有任何实现(方法体)。 抽象类不必包含抽象成员;但是,如果类包含抽象成员,则类本身必须声明为抽象。 本身不抽象的派生类必须为来自抽象基类的任何抽象方法提供实现。

接口

接口是定义一组成员的引用类型。 实现该接口的所有类和结构都必须实现这组成员。 接口可以为其中任何成员或全部成员定义默认实现。 类可以实现多个接口,即使它只能派生自单个直接基类。

接口用于为类定义特定功能,这些功能不一定具有“is a (是)”关系。 例如,System.IEquatable<T> 接口可由任何类或结构实现,以确定该类型的两个对象是否等效(但是由该类型定义等效性)。 IEquatable<T> 不表示基类和派生类之间存在的同一种“是”关系(例如,MammalAnimal)。 有关详细信息,请参阅接口

防止进一步派生

类可以通过将自己或成员声明为 sealed,来防止其他类继承自它或继承自其任何成员。

基类成员的派生类隐藏

派生类可以通过使用相同名称和签名声明成员来隐藏基类成员。 new 修饰符可以用于显式指示成员不应作为基类成员的重写。 使用 new 不是必需的,但如果未使用 new,则会产生编译器警告。 有关详细信息,请参阅使用 Override 和 New 关键字进行版本控制了解何时使用 Override 和 New 关键字