Vererbung: Ableiten von Typen, um spezielleres Verhalten zu erstellen

Die Vererbung ist, zusammen mit der Kapselung und der Polymorphie, eines der drei primären Charakteristika des objektorientierten Programmierens. Die Vererbung ermöglicht die Erstellung neuer Klassen, die in anderen Klassen definiertes Verhalten wieder verwenden, erweitern und ändern. Die Klasse, deren Member vererbt werden, wird Basisklasse genannt, und die Klasse, die diese Member erbt, wird abgeleitete Klasse genannt. Eine abgeleitete Klasse kann nur eine direkte Basisklasse haben. Doch die Vererbung ist transitiv. Wenn ClassC von ClassB und ClassB von ClassA abgeleitet wird, erbt ClassC die Member, die in ClassB und ClassA deklariert wurden.

Hinweis

Strukturen unterstützen die Vererbung nicht, aber sie können Schnittstellen implementieren.

Konzeptuell gesehen ist die abgeleitete Klasse eine Spezialisierung der Basisklasse. Wenn Sie beispielsweise eine Basisklasse Animal haben, haben Sie möglicherweise eine abgeleitete Klasse mit dem Namen Mammal und eine andere abgeleitete Klasse mit dem Namen Reptile. Ein Mammal ist ein Animal, und ein Reptile ist ein Animal, aber jede abgeleitete Klasse repräsentiert unterschiedliche Spezialisierungen der Basisklasse.

Schnittstellendeklarationen können eine Standardimplementierung ihrer Member definieren. Diese Implementierungen werden von abgeleiteten Schnittstellen und von Klassen geerbt, die diese Schnittstellen implementieren. Weitere Informationen zu Standardschnittstellenmethoden finden Sie im Artikel zu Schnittstellen.

Wenn Sie eine Klasse definieren, die von einer anderen Klasse abgeleitet werden soll, erhält die abgeleitete Klasse implizit alle Member der Basisklasse – ausgenommen davon sind deren Konstruktoren und Finalizern. Die abgeleitete Klasse verwendet den Code in der Basisklasse wieder, ohne ihn nochmal implementieren zu müssen. Sie können weitere Member in der abgeleiteten Klasse hinzufügen. Die abgeleitete Klasse erweitert die Funktionalität der Basisklasse.

Die folgende Abbildung zeigt eine Klasse WorkItem, die ein Arbeitselement in einem Geschäftsprozess repräsentiert. Wie alle Klassen leitet es sich von System.Object ab und erbt dessen Methoden. WorkItem fügt von allein sechs Member hinzu. Diese Member beinhalten einen Konstruktor, weil Konstruktoren nicht vererbt werden. Die Klasse ChangeRequest erbt von WorkItem und repräsentiert eine bestimmt Art von Arbeitselementen. ChangeRequest fügt den von WorkItem und Object geerbten Membern zwei weitere Member hinzu. Es muss seinen eigenen Konstruktor hinzufügen, und es fügt auch originalItemID hinzu. Die Eigenschaft originalItemID ermöglicht es der ChangeRequest-Instanz, mit dem ursprünglichen WorkItem verknüpft zu werden, für das die Änderungsanforderung gilt.

Diagram that shows class inheritance

Das folgende Beispiel zeigt, wie die in der oben stehenden Abbildung veranschaulichten Klassenbeziehungen in C# ausgedrückt werden. Das Beispiel zeigt auch, wie WorkItem die virtuelle Methode Object.ToString außer Kraft setzt, und wie die ChangeRequest-Klasse die WorkItem-Implementierung der Methode erbt. Im ersten Codeblock werden die Klassen definiert:

// 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;
    }
}

Im nächsten Codeblock wird die Verwendung der Basis- und abgeleiteten Klassen veranschaulicht:

// 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
*/

Abstrakte und virtuelle Methoden

Wenn eine Basisklasse eine Methode als virtual deklariert, kann eine abgeleitete Klasse die Methode mithilfe des override-Modifizierers mit ihrer eigenen Implementierung überschreiben. Wenn eine Basisklasse eine Methode als abstract deklariert, muss diese Methode in jeder nicht abstrakten Klasse überschrieben werden, die direkt von dieser Klasse erbt. Wenn eine abgeleitete Klasse selbst abstrakt ist, erbt sie abstrakte Member, ohne diese zu implementieren. Abstrakte und virtuelle Member sind die Basis für Polymorphie, die das zweite charakteristische Merkmal des objektorientierten Programmierens ist. Weitere Informationen finden Sie unter Polymorphie.

Abstrakte Basisklassen

Sie können eine Klasse als abstrakt deklarieren, wenn Sie die direkte Instanziierung mit dem new-Operator vermeiden möchten. Eine abstrakte Klasse kann nur verwendet werden, wenn eine neue Klasse von ihr abgeleitet wird. Eine abstrakte Klasse kann mindestens eine Methodensignatur enthalten, die selbst auch als abstrakt deklariert wurden. Diese Signaturen geben die Parameter und Rückgabewerte an, verfügen aber über keine Implementierung (Methodenkörper). Eine abstrakte Klasse muss nicht zwangsläufig abstrakte Member enthalten. Wenn eine Klasse allerdings einen abstrakten Member enthält, muss die Klasse an sich auch als abstrakt deklariert werden. Abgeleitete Klassen, die nicht selbst abstrakt sind, müssen eine Implementierung für jede beliebige abstrakte Methode aus einer abstrakten Basisklasse bereitstellen.

Schnittstellen

Eine Schnittstelle ist ein Verweistyp, mit dem eine Gruppe von Membern definiert wird. Alle Klassen und Strukturen, die diese Schnittstelle implementieren, müssen diese Gruppe von Membern implementieren. Eine Schnittstelle kann eine Standardimplementierung für beliebige oder alle dieser Member definieren. Eine Klasse kann mehrere Schnittstellen implementieren, auch wenn sie nur von einer einzelnen direkten Basisklasse ableiten kann.

Schnittstellen werden verwendet, um bestimmte Funktionen zu definieren, die nicht unbedingt in einer „ist ein“-Beziehung zueinander stehen. Beispielsweise kann die Schnittstelle System.IEquatable<T> von einer beliebigen Klasse oder Struktur implementiert werden, um zu bestimmen, ob zwei Objekte des Typs äquivalent sind (die Äquivalenz wird jedoch vom Typ definiert). IEquatable<T> impliziert nicht dieselbe Art einer „ist ein“-Beziehung, wie sie zwischen einer Basis- und einer abgeleiteten Klasse besteht (z. B. ist ein Mammal ein Animal). Weitere Informationen finden Sie unter Schnittstellen.

Verhindern weiterer Ableitung

Eine Klasse kann andere Klassen daran hindern, von ihr oder einem ihrer Member zu erben, indem sie sich selbst oder den Member als sealed deklariert.

Verbergen von Basisklassenmembern durch abgeleitete Klassen

Eine abgeleitete Klasse kann Basisklassenmember verbergen, indem sie Member mit demselben Namen und derselben Signatur deklariert. Der Modifizierer new kann verwendet werden, um explizit anzugeben, dass der Member den Basismember nicht überschreiben soll. Das Verwenden von new ist nicht erforderlich, aber einer Compilerwarnung wird generiert, wenn new nicht verwendet wird. Weitere Informationen finden Sie unter Versionsverwaltung mit den Schlüsselwörtern „override“ und „new“ und Wann müssen die Schlüsselwörter „override“ und „new“ verwendet werden?.