Vererbung in C# und .NET

Dieses Tutorial macht Sie mit der Vererbung in C# vertraut. Vererbung ist eine Funktion der objektorientierten Programmiersprachen, die Ihnen ermöglicht, eine Basisklasse zu definieren, die eine bestimmte Funktionalität bietet (Daten und Verhalten), und abgeleitete Klassen zu definieren, die diese Funktionalität entweder übernehmen oder außer Kraft setzen.

Voraussetzungen

Ausführen der Beispiele

Verwenden Sie zum Erstellen und Ausführen der Beispiele in diesem Tutorial das Befehlszeilenhilfsprogramm dotnet. Gehen Sie für jedes Beispiel wie folgt vor:

  1. Erstellen Sie ein Verzeichnis zum Speichern des Beispiels.

  2. Geben Sie den Befehl dotnet new console in einer Befehlszeile ein, um ein neues .NET Core-Projekt zu erstellen.

  3. Kopieren Sie den Code aus dem Beispiel, und fügen Sie ihn in den Code-Editor ein.

  4. Geben Sie den Befehl dotnet restore in der Befehlszeile ein, um die Abhängigkeiten des Projekts zu laden oder wiederherzustellen.

    Sie müssen dotnet restore nicht ausführen, da der Befehl implizit von allen Befehlen ausgeführt wird, die eine Wiederherstellung erfordern (z. B. dotnet new, dotnet build, dotnet run, dotnet test, dotnet publish und dotnet pack). Verwenden Sie die Option --no-restore, um die implizite Wiederherstellung zu deaktivieren.

    In bestimmten Fällen eignet sich der dotnet restore-Befehl dennoch. Dies ist etwa bei Szenarios der Fall, in denen die explizite Wiederherstellung sinnvoll ist. Beispiele hierfür sind Continuous Integration-Builds in Azure DevOps Services oder Buildsysteme, die den Zeitpunkt für die Wiederherstellung explizit steuern müssen.

    Informationen zum Verwalten von NuGet-Feeds finden Sie in der dotnet restoreDokumentation.

  5. Geben Sie den Befehl dotnet run zum Kompilieren und Ausführen des Beispiels ein.

Hintergrund: Was ist Vererbung?

Vererbung ist eines der wichtigsten Attribute bei der objektorientierten Programmierung. Sie können damit eine untergeordnete Klasse definieren, die das Verhalten einer übergeordneten Klasse wiederverwendet (erbt), erweitert oder ändert. Die Klasse, deren Member geerbt werden, ist die Basisklasse. Die Klasse, die die Member der Basisklasse erbt, ist die abgeleitete Klasse.

C# und .NET unterstützen nur die einzelne Vererbung. D.h., eine Klasse kann nur von einer einzelnen Klasse erben. Allerdings ist Vererbung transitiv, sodass Sie eine Vererbungshierarchie für einen Satz von Typen definieren können. Mit anderen Worten: Typ D kann von Typ C erben, der von Typ B erbt, der vom Basisklassentyp A erbt. Da Vererbung transitiv ist, stehen die Member des Typs A Typ D zur Verfügung.

Nicht alle Member einer Basisklasse werden von abgeleiteten Klassen geerbt. Die folgenden Member werden nicht geerbt:

  • Statische Konstruktoren, die die statischen Daten einer Klasse initialisieren.

  • Instanzkonstruktoren, die Sie aufrufen, um eine neue Instanz der Klasse zu erstellen. Jede Klasse muss ihre eigenen Konstruktoren definieren.

  • Finalizer, die vom Garbage Collector der Laufzeit aufgerufen werden, um Instanzen einer Klasse zu zerstören.

Während alle anderen Member einer Basisklasse von abgeleiteten Klassen geerbt werden, hängt ihre Sichtbarkeit davon ab, ob auf sie zugegriffen werden kann. Ob auf einen Member zugegriffen werden kann, beeinflusst dessen Sichtbarkeit für abgeleitete Klassen wie folgt:

  • Private Member sind nur in abgeleiteten Klassen sichtbar, die in ihrer Basisklasse geschachtelt sind. Andernfalls sind sie in abgeleiteten Klassen nicht sichtbar. Im folgenden Beispiel ist A.B eine geschachtelte Klasse, die sich von A ableitet, und C leitet sich von A ab. Das private Feld A._value ist in „A.B“ sichtbar. Wenn Sie jedoch die Kommentare aus der C.GetValue-Methode entfernen und versuchen, das Beispiel zu kompilieren, tritt der Compilerfehler CS0122 auf: "Der Zugriff auf ‘A._value‘ ist aufgrund des Schutzgrads nicht möglich."

    public class A
    {
        private int _value = 10;
    
        public class B : A
        {
            public int GetValue()
            {
                return _value;
            }
        }
    }
    
    public class C : A
    {
        //    public int GetValue()
        //    {
        //        return _value;
        //    }
    }
    
    public class AccessExample
    {
        public static void Main(string[] args)
        {
            var b = new A.B();
            Console.WriteLine(b.GetValue());
        }
    }
    // The example displays the following output:
    //       10
    
  • Geschützte Member sind nur in abgeleiteten Klassen sichtbar.

  • Interne Member sind nur in abgeleiteten Klassen sichtbar, die sich in der gleichen Assembly wie die Basisklasse befinden. Sie sind nicht in abgeleiteten Klassen sichtbar, die sich in einer anderen Assembly als die Basisklasse befinden.

  • Öffentliche Member sind in abgeleiteten Klassen sichtbar und Teil der öffentlichen Schnittstelle der abgeleiteten Klasse. Öffentlich geerbte Member können so aufgerufen werden, als ob sie in der abgeleiteten Klasse definiert sind. Im folgenden Beispiel definiert Klasse A eine Methode namens Method1, und Klasse B erbt von Klasse A. Das Beispiel ruft dann Method1 auf, als wäre sie eine Instanzmethode von B.

    public class A
    {
        public void Method1()
        {
            // Method implementation.
        }
    }
    
    public class B : A
    { }
    
    public class Example
    {
        public static void Main()
        {
            B b = new ();
            b.Method1();
        }
    }
    

Abgeleitete Klassen können auch geerbte Member überschreiben, indem sie eine alternative Implementierung bereitstellen. Um einen Member überschreiben zu können, muss der Member in der Basisklasse mit dem Schlüsselwort virtual markiert sein. Standardmäßig sind Member der Basisklasse nicht als virtual markiert und können nicht überschrieben werden. Der Versuch, wie im folgenden Beispiel gezeigt einen nicht virtuellen Member zu überschreiben, verursacht den Compilerfehler CS0506: "<Member>: Der geerbte Member <Member> kann nicht überschrieben werden, da er nicht als „virtual“, „abstract“ oder „override“ markiert ist."

public class A
{
    public void Method1()
    {
        // Do something.
    }
}

public class B : A
{
    public override void Method1() // Generates CS0506.
    {
        // Do something else.
    }
}

In einigen Fällen muss eine abgeleitete Klasse die Basisklassenimplementierung überschreiben. Basisklassenmember, die mit dem Schlüsselwort abstract markiert sind, erfordern, dass abgeleitete Klassen sie überschreiben. Der Versuch, das folgende Beispiel zu kompilieren, verursacht den Compilerfehler CS0534: „Die <Klasse> implementiert den geerbten abstrakten Member <Member> nicht.“, da die Klasse B keine Implementierung für A.Method1 bietet.

public abstract class A
{
    public abstract void Method1();
}

public class B : A // Generates CS0534.
{
    public void Method3()
    {
        // Do something.
    }
}

Vererbung gilt nur für Klassen und Schnittstellen. Andere Typkategorien (Strukturen, Delegate und Enumerationen) unterstützen keine Vererbung. Aufgrund dieser Regeln tritt beim Versuch, Code wie im folgenden Beispiel zu kompilieren, der Compilerfehler CS0527 auf: "Der Typ ‘ValueType‘ in der Schnittstellenliste ist keine Schnittstelle." Die Fehlermeldung gibt an, dass Sie zwar die Schnittstellen definieren können, die eine Struktur implementiert, die Vererbung jedoch nicht unterstützt wird.

public struct ValueStructure : ValueType // Generates CS0527.
{
}

Implizite Vererbung

Neben Typen, die sie vielleicht über die einzelne Vererbung erben, erben alle Typen im Typensystem von .NET implizit von Object oder einem davon abgeleiteten Typ. Die allgemeine Funktionalität von Object ist für jeden beliebigen Typ verfügbar.

Um zu sehen, was implizite Vererbung bedeutet, definieren wir eine neue Klasse SimpleClass, die einfach eine leere Klassendefinition ist:

public class SimpleClass
{ }

Sie können dann die Reflektion (die Ihnen ermöglicht, die Metadaten eines Typs zu überprüfen, um Informationen zu diesem Typ zu erhalten) verwenden, um eine Liste der Member abzurufen, die zum SimpleClass-Typ gehören. Obwohl Sie keine Member in Ihrer SimpleClass-Klasse definiert haben, gibt die Ausgabe des Beispiels an, dass sie tatsächlich neun Member hat. Davon ist ein Member ein parameterloser (oder standardmäßiger) Konstruktor, der automatisch vom C#-Compiler für den SimpleClass-Typ angegeben wird. Die verbleibenden acht sind Member von Object, dem Typ, von dem alle Klassen und Schnittstellen im .NET-Typsystem letztlich implizit erben.

using System.Reflection;

public class SimpleClassExample
{
    public static void Main()
    {
        Type t = typeof(SimpleClass);
        BindingFlags flags = BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public |
                             BindingFlags.NonPublic | BindingFlags.FlattenHierarchy;
        MemberInfo[] members = t.GetMembers(flags);
        Console.WriteLine($"Type {t.Name} has {members.Length} members: ");
        foreach (MemberInfo member in members)
        {
            string access = "";
            string stat = "";
            var method = member as MethodBase;
            if (method != null)
            {
                if (method.IsPublic)
                    access = " Public";
                else if (method.IsPrivate)
                    access = " Private";
                else if (method.IsFamily)
                    access = " Protected";
                else if (method.IsAssembly)
                    access = " Internal";
                else if (method.IsFamilyOrAssembly)
                    access = " Protected Internal ";
                if (method.IsStatic)
                    stat = " Static";
            }
            string output = $"{member.Name} ({member.MemberType}): {access}{stat}, Declared by {member.DeclaringType}";
            Console.WriteLine(output);
        }
    }
}
// The example displays the following output:
//	Type SimpleClass has 9 members:
//	ToString (Method):  Public, Declared by System.Object
//	Equals (Method):  Public, Declared by System.Object
//	Equals (Method):  Public Static, Declared by System.Object
//	ReferenceEquals (Method):  Public Static, Declared by System.Object
//	GetHashCode (Method):  Public, Declared by System.Object
//	GetType (Method):  Public, Declared by System.Object
//	Finalize (Method):  Internal, Declared by System.Object
//	MemberwiseClone (Method):  Internal, Declared by System.Object
//	.ctor (Constructor):  Public, Declared by SimpleClass

Implizite Vererbung von der Object -Klasse macht diese Methoden der SimpleClass-Klasse verfügbar:

  • Die öffentliche ToString-Methode, die ein SimpleClass-Objekt in seine Zeichenfolgendarstellung konvertiert, gibt den vollqualifizierten Typnamen zurück. In diesem Fall gibt die ToString-Methode die Zeichenfolge „SimpleClass“ zurück.

  • Drei Methoden, die zwei Objekte auf Gleichheit testen: die öffentliche Equals(Object)-Instanzmethode, die öffentliche statische Equals(Object, Object)-Methode und die öffentliche statische ReferenceEquals(Object, Object)-Methode. Standardmäßig testen diese Methoden auf Verweisgleichheit; d.h., um gleich zu sein, müssen zwei Objektvariablen auf das gleiche Objekt verweisen.

  • Die öffentliche GetHashCode-Methode, die einen Wert, berechnet, der die Verwendung einer Instanz des Typs in Hashauflistungen ermöglicht.

  • Die öffentliche GetType -Methode, die ein Type -Objekt zurückgibt, das den SimpleClass -Typ darstellt.

  • Die geschützte Finalize -Methode, die nicht verwaltete Ressourcen freigeben soll, bevor der Speicher eines Objekts durch den Garbage Collector freigegeben wird.

  • Die geschützte MemberwiseClone -Methode, die einen flachen Klon des aktuellen Objekts erstellt.

Aufgrund der impliziten Vererbung können Sie alle geerbten Member aus einem SimpleClass-Objekt einfach aufrufen, als wären sie tatsächlich in der SimpleClass-Klasse definierte Member. Im folgenden Beispiel wird die SimpleClass.ToString-Methode aufgerufen, die SimpleClass von Object erbt.

public class EmptyClass
{ }

public class ClassNameExample
{
    public static void Main()
    {
        EmptyClass sc = new();
        Console.WriteLine(sc.ToString());
    }
}
// The example displays the following output:
//        EmptyClass

Die folgende Tabelle enthält die Kategorien von Typen, die Sie in C# erstellen können, und die Typen, von denen sie implizit erben. Jeder Basistyp macht implizit abgeleiteten Typen über Vererbung einen anderen Satz von Membern verfügbar.

Typkategorie Erbt implizit von
class Object
struct ValueType, Object
enum Enum, ValueType, Object
delegate MulticastDelegate, Delegate, Object

Vererbung und eine „ist ein“-Beziehung

Mit Vererbung wird normalerweise eine „ist ein“-Beziehung zwischen einer Basisklasse und einer oder mehreren abgeleiteten Klassen ausgedrückt, wobei die abgeleiteten Klassen spezialisierte Versionen der Basisklasse sind; die abgeleitete Klasse ist ein Typ der Basisklasse. Die Publication-Klasse stellt z.B. eine Publikation beliebiger Art dar, und die Book- und Magazine-Klasse stellen bestimmte Typen von Publikationen dar.

Hinweis

Eine Klasse oder Struktur kann eine oder mehrere Schnittstellen implementieren. Die Schnittstellenimplementierung wird zwar oft als Problemumgehung für einzelne Vererbung oder Möglichkeit der Verwendung von Vererbung mit Strukturen dargestellt, doch sie soll eine andere Beziehung (eine „tun können“-Beziehung) zwischen einer Schnittstelle und ihrem implementierenden Typ ausdrücken als Vererbung. Eine Schnittstelle definiert eine Teilmenge der Funktionalität (z.B. die Möglichkeit zum Testen auf Gleichheit, zum Vergleichen oder Sortieren von Objekten oder zum Unterstützen kulturspezifischer Analyse und Formatierung), die die Schnittstelle den implementierenden Typen zur Verfügung stellt.

Beachten Sie, dass „ist ein“ auch die Beziehung zwischen einem Typ und einer bestimmten Instanziierung des betreffenden Typs ausdrückt. Im folgenden Beispiel ist Automobile eine Klasse mit drei eindeutigen schreibgeschützten Eigenschaften: Make, der Autohersteller; Model, den Autotyp, und Year, das Herstellungsjahr. Ihre Automobile-Klasse hat auch einen Konstruktor, dessen Argumente den Eigenschaftswerten zugewiesen werden. Er überschreibt die Object.ToString-Methode, um eine Zeichenfolge zu erzeugen, die eindeutig die Automobile-Instanz anstelle der Automobile-Klasse identifiziert.

public class Automobile
{
    public Automobile(string make, string model, int year)
    {
        if (make == null)
            throw new ArgumentNullException(nameof(make), "The make cannot be null.");
        else if (string.IsNullOrWhiteSpace(make))
            throw new ArgumentException("make cannot be an empty string or have space characters only.");
        Make = make;

        if (model == null)
            throw new ArgumentNullException(nameof(model), "The model cannot be null.");
        else if (string.IsNullOrWhiteSpace(model))
            throw new ArgumentException("model cannot be an empty string or have space characters only.");
        Model = model;

        if (year < 1857 || year > DateTime.Now.Year + 2)
            throw new ArgumentException("The year is out of range.");
        Year = year;
    }

    public string Make { get; }

    public string Model { get; }

    public int Year { get; }

    public override string ToString() => $"{Year} {Make} {Model}";
}

In diesem Fall sollten Sie sich nicht auf die Vererbung verlassen, um bestimmte Automarken und Modelle darzustellen. Sie müssen z. B. keinen Packard-Typ definieren, um Autos darzustellen, die von der Packard Motor Car Company hergestellt werden. Stattdessen können Sie sie durch Erstellen eines Automobile-Objekts darstellen, wobei die entsprechenden Werten an dessen Klassenkonstruktor übergeben werden, wie im folgenden Beispiel dargestellt.

using System;

public class Example
{
    public static void Main()
    {
        var packard = new Automobile("Packard", "Custom Eight", 1948);
        Console.WriteLine(packard);
    }
}
// The example displays the following output:
//        1948 Packard Custom Eight

Eine auf Vererbung basierende „ist ein“-Beziehung wird am besten auf eine Basisklasse und abgeleitete Klassen angewendet, die der Basisklasse weitere Member hinzufügen oder zusätzliche Funktionalität erfordern, die in der Basisklasse nicht vorhanden ist.

Entwerfen der Basisklasse und abgeleiteter Klassen

Wir betrachten das Entwerfen einer Basisklasse und ihrer abgeleiteten Klassen. In diesem Abschnitt definieren Sie eine Basisklasse Publication, die eine beliebige Veröffentlichung darstellt, z. B. ein Buch, eine Zeitschrift, eine Zeitung, ein Journal, einen Artikel usw. Sie definieren auch eine Book-Klasse, die von Publication abgeleitet ist. Sie könnten das Beispiel einfach erweitern, um andere abgeleitete Klassen wie Magazine, Journal, Newspaper und Article zu definieren.

Die Basisklasse „Publication“

Beim Entwurf Ihrer Publication-Klasse müssen Sie einige Entwurfsentscheidungen treffen:

  • Welche Member sollen in Ihre Basisklasse Publication einbezogen werden? Sollen die Publication-Member Methodenimplementierungen bereitstellen, oder ist Publication eine abstrakte Basisklasse, die als Vorlage für ihre abgeleiteten Klassen dient?

    In diesem Fall stellt die Publication-Klasse Methodenimplementierungen bereit. Der Abschnitt Entwerfen abstrakter Basisklassen und ihrer abgeleiteten Klassen enthält ein Beispiel, in dem eine abstrakte Basisklasse verwendet wird, um die Methoden zu definieren, die abgeleitete Klassen überschreiben müssen. Abgeleitete Klassen können beliebige Implementierungen bereitstellen, die für den abgeleiteten Typ geeignet sind.

    Die Möglichkeit zur Wiederverwendung von Code (d.h., mehrere abgeleitete Klassen nutzen gemeinsam die Deklaration und Implementierung von Basisklassenmethoden und müssen sie nicht überschreiben) ist ein Vorteil der nicht abstrakten Basisklassen. Daher sollten Sie Publication Member hinzufügen, wenn ihr Code vermutlich von einigen oder den meisten spezialisierten Publication-Typen gemeinsam genutzt wird. Wenn es Ihnen nicht gelingt, Basisklassenimplementierungen effizient bereitzustellen, müssen Sie letztendlich weitgehend identische Memberimplementierungen in abgeleiteten Klassen bereitstellen, statt einer einzelnen Implementierung in der Basisklasse. Die Notwendigkeit, duplizierten Code an mehreren Standorten zu verwalten, ist eine potenzielle Fehlerquelle.

    Um sowohl die Wiederverwendung von Codes zu maximieren als auch eine logische und intuitive Vererbungshierarchie zu erstellen, müssen Sie sichergehen, dass Sie in die Publication-Klasse nur die Daten und Funktionen einbeziehen, die alle bzw. die meisten Veröffentlichungen gemeinsam haben. Abgeleitete Klassen implementieren dann Member, die für die jeweiligen Publikationsarten, die sie darstellen, eindeutig sind.

  • Wie weit sollten Sie Ihre Klassenhierarchie erweitern? Möchten Sie statt einer einzigen Basisklasse und einer oder mehreren abgeleiteten Klassen eine Hierarchie von mindestens drei Klassen entwickeln? Beispielsweise könnte Publication eine Basisklasse von Periodical sein, was wiederum eine Basisklasse von Magazine, Journal und Newspaper ist.

    Für Ihr Beispiel verwenden Sie die flache Hierarchie einer Publication-Klasse und einer einzelnen abgeleiteten Klasse Book. Sie könnten das Beispiel mühelos erweitern, um eine Reihe von zusätzlichen Klassen zu erstellen, die von Publication abgeleitet sind, z. B. Magazine und Article.

  • Ist es sinnvoll, die Basisklasse zu instanziieren? Wenn das nicht der Fall ist, wenden Sie das Schlüsselwort abstract auf die Klasse an. Andernfalls kann Ihre Publication-Klasse durch Aufruf ihres Klassenkonstruktors instanziiert werden. Wenn versucht wird, eine mit dem Schlüsselwort abstract gekennzeichnete Klasse durch einen direkten Aufruf ihres Klassenkonstruktors zu instanziieren, generiert der C#-Compiler den Fehler CS0144: "Es konnte keine Instanz der abstrakten Klasse oder Schnittstelle erstellt werden." Wenn versucht wird, die Klasse mithilfe von Reflexion zu instanziieren, löst die Reflexionsmethode eine MemberAccessException-Klasse aus.

    Standardmäßig kann eine Basisklasse durch Aufruf ihres Klassenkonstruktors instanziiert werden. Sie müssen keinen Klassenkonstruktor explizit definieren. Wenn im Quellcode der Basisklasse keiner vorhanden ist, stellt der C#-Compiler automatisch einen (parameterlosen) Standardkonstruktor bereit.

    In Ihrem Beispiel markieren Sie die Publication-Klasse als abstract, sodass sie nicht instanziiert werden kann. Eine abstract-Klasse ohne abstract-Methoden zeigt an, dass diese Klasse ein abstraktes Konzept darstellt, das von mehreren konkreten Klassen geteilt wird (z. B. Book und Journal).

  • Müssen abgeleitete Klassen die Implementierung der Basisklasse eines bestimmten Members erben, können sie die Implementierung der Basisklasse optional überschreiben, oder müssen sie eine Implementierung bereitstellen? Mit dem Schlüsselwort abstract können Sie abgeleitete Klassen dazu zwingen, eine Implementierung bereitzustellen. Mit dem Schlüsselwort virtual können Sie abgeleiteten Klassen erlauben, eine Basisklassenmethode zu überschreiben. Standardmäßig können in der Basisklasse definierte Methoden nicht überschrieben werden.

    Die Publication-Klasse hat keine abstract-Methoden, ist allerdings selbst abstract.

  • Stellt eine abgeleitete Klasse die endgültige Klasse in der Vererbungshierarchie dar und kann nicht selbst als Basisklasse für weitere abgeleitete Klassen verwendet werden? Standardmäßig kann jede Klasse als Basisklasse dienen. Sie können das sealed-Schlüsselwort anwenden, um anzugeben, dass eine Klasse nicht als Basisklasse für zusätzliche Klassen dienen kann. Beim Versuch der Ableitung von einer versiegelten Klasse wird der Compilerfehler CS0509 generiert: "Vom versiegelten Typ <typeName> kann nicht abgeleitet werden."

    Für Ihr Beispiel markieren Sie Ihre abgeleitete Klasse als sealed.

Das folgende Beispiel zeigt sowohl den Quellcode für die Publication-Klasse als auch eine PublicationType-Enumeration, die von der Eigenschaft Publication.PublicationType zurückgegeben wird. Zusätzlich zu den Membern, die sie von Object erbt, definiert die Publication-Klasse die folgenden eindeutigen Member und Memberüberschreibungen:


public enum PublicationType { Misc, Book, Magazine, Article };

public abstract class Publication
{
    private bool _published = false;
    private DateTime _datePublished;
    private int _totalPages;

    public Publication(string title, string publisher, PublicationType type)
    {
        if (string.IsNullOrWhiteSpace(publisher))
            throw new ArgumentException("The publisher is required.");
        Publisher = publisher;

        if (string.IsNullOrWhiteSpace(title))
            throw new ArgumentException("The title is required.");
        Title = title;

        Type = type;
    }

    public string Publisher { get; }

    public string Title { get; }

    public PublicationType Type { get; }

    public string? CopyrightName { get; private set; }

    public int CopyrightDate { get; private set; }

    public int Pages
    {
        get { return _totalPages; }
        set
        {
            if (value <= 0)
                throw new ArgumentOutOfRangeException(nameof(value), "The number of pages cannot be zero or negative.");
            _totalPages = value;
        }
    }

    public string GetPublicationDate()
    {
        if (!_published)
            return "NYP";
        else
            return _datePublished.ToString("d");
    }

    public void Publish(DateTime datePublished)
    {
        _published = true;
        _datePublished = datePublished;
    }

    public void Copyright(string copyrightName, int copyrightDate)
    {
        if (string.IsNullOrWhiteSpace(copyrightName))
            throw new ArgumentException("The name of the copyright holder is required.");
        CopyrightName = copyrightName;

        int currentYear = DateTime.Now.Year;
        if (copyrightDate < currentYear - 10 || copyrightDate > currentYear + 2)
            throw new ArgumentOutOfRangeException($"The copyright year must be between {currentYear - 10} and {currentYear + 1}");
        CopyrightDate = copyrightDate;
    }

    public override string ToString() => Title;
}
  • Ein Konstruktor

    Da die Publication-Klasse abstract ist, kann sie nicht direkt von Codes wie dem folgenden aus instanziiert werden:

    var publication = new Publication("Tiddlywinks for Experts", "Fun and Games",
                                      PublicationType.Book);
    

    Ihr Instanzkonstruktor kann jedoch direkt von abgeleiteten Klassenkonstruktoren aufgerufen werden, wie der Quellcode für die Book-Klasse veranschaulicht.

  • Zwei publikationsbezogene Eigenschaften

    Title ist eine schreibgeschützte String-Eigenschaft, deren Wert durch Aufrufen des Publication-Konstruktors bereitgestellt wird.

    Pages ist eine schreibgeschützte Eigenschaft Int32, die angibt, wie viele Seiten die Publikation insgesamt hat. Der Wert wird in einem privaten Feld namens totalPages gespeichert. Er muss eine positive Zahl sein; andernfalls wird eine ArgumentOutOfRangeException ausgelöst.

  • Herausgeberbezogene Elemente

    Zwei schreibgeschützte Eigenschaften, Publisher und Type. Die Werte werden ursprünglich durch den Aufruf des Publication-Klassenkonstruktors abgerufen.

  • Veröffentlichungsbezogene Elemente

    Zwei Methoden, Publish und GetPublicationDate, legen das Veröffentlichungsdatum fest und geben es zurück. Die Publish-Methode legt ein privates published-Flag auf true fest, wenn sie aufgerufen wird, und weist dem privaten datePublished-Feld das ihr übergebene Datum als Argument zu. Die GetPublicationDate-Methode gibt die Zeichenfolge „NYP“ zurück, wenn das published-Flag false ist, und den Wert des Felds datePublished, wenn es true ist.

  • Copyrightbezogene Elemente

    Die Methode Copyright übernimmt den Namen des Urheberrechtsinhabers und das Jahr des Copyrights als Argumente und weist sie den Eigenschaften CopyrightName und CopyrightDate zu.

  • Eine Überschreibung der ToString-Methode

    Wenn ein Typ die Object.ToString -Methode nicht überschreibt, gibt sie den vollqualifizierten Namen des Typs zurück, was zur Unterscheidung einer Instanz von einer anderen von geringem Nutzen ist. Die Publication-Klasse überschreibt Object.ToString, um den Wert der Eigenschaft Title zurückzugeben.

Die folgende Abbildung veranschaulicht die Beziehung zwischen Ihrer Basisklasse Publication und der implizit geerbten Klasse Object.

Die Klassen „Object“ und „Publication“

Die Book-Klasse

Die Book-Klasse stellt ein Buch als einen speziellen Typ der Publikation dar. Das folgende Beispiel zeigt den Quellcode für die Book-Klasse.

using System;

public sealed class Book : Publication
{
    public Book(string title, string author, string publisher) :
           this(title, string.Empty, author, publisher)
    { }

    public Book(string title, string isbn, string author, string publisher) : base(title, publisher, PublicationType.Book)
    {
        // isbn argument must be a 10- or 13-character numeric string without "-" characters.
        // We could also determine whether the ISBN is valid by comparing its checksum digit
        // with a computed checksum.
        //
        if (!string.IsNullOrEmpty(isbn))
        {
            // Determine if ISBN length is correct.
            if (!(isbn.Length == 10 | isbn.Length == 13))
                throw new ArgumentException("The ISBN must be a 10- or 13-character numeric string.");
            if (!ulong.TryParse(isbn, out _))
                throw new ArgumentException("The ISBN can consist of numeric characters only.");
        }
        ISBN = isbn;

        Author = author;
    }

    public string ISBN { get; }

    public string Author { get; }

    public decimal Price { get; private set; }

    // A three-digit ISO currency symbol.
    public string? Currency { get; private set; }

    // Returns the old price, and sets a new price.
    public decimal SetPrice(decimal price, string currency)
    {
        if (price < 0)
            throw new ArgumentOutOfRangeException(nameof(price), "The price cannot be negative.");
        decimal oldValue = Price;
        Price = price;

        if (currency.Length != 3)
            throw new ArgumentException("The ISO currency symbol is a 3-character string.");
        Currency = currency;

        return oldValue;
    }

    public override bool Equals(object? obj)
    {
        if (obj is not Book book)
            return false;
        else
            return ISBN == book.ISBN;
    }

    public override int GetHashCode() => ISBN.GetHashCode();

    public override string ToString() => $"{(string.IsNullOrEmpty(Author) ? "" : Author + ", ")}{Title}";
}

Zusätzlich zu den Membern, die sie von Publication erbt, definiert die Book-Klasse die folgenden eindeutigen Member und Memberüberschreibungen:

  • Zwei Konstruktoren

    Die beiden Book-Konstruktoren nutzen gemeinsam drei allgemeine Parameter. Zwei, title und publisher, entsprechen den Parametern des Publication-Konstruktors. Der dritte ist author, der in einer öffentlichen unveränderlichen Author-Eigenschaft gespeichert ist. Ein Konstruktor enthält einen ISBN-Parameter, der in der Auto-Eigenschaft ISBN gespeichert ist.

    Der erste Konstruktor verwendet das this-Schlüsselwort, um den anderen Konstruktor aufzurufen. Die Konstruktorverkettung ist ein häufiges Muster beim Definieren von Konstruktoren. Konstruktoren mit weniger Parametern stellen beim Aufrufen des Konstruktors mit der größten Anzahl von Parametern Standardwerte zur Verfügung.

    Der zweite Konstruktor verwendet das base-Schlüsselwort, um Titel und Herausgebername an den Basisklassenkonstruktor zu übergeben. Wenn Ihr Quellcode keinen expliziten Aufruf eines Basisklassenkonstruktors enthält, stellt der C#-Compiler automatisch einen Aufruf des standardmäßigen oder parameterlosen Konstruktors der Basisklasse bereit.

  • Eine schreibgeschützte Eigenschaft ISBN, die die ISBN des Book-Objekts zurückgibt, eine eindeutige 10- oder 13-stellige Nummer. Die ISBN wird einem der Book-Konstruktoren als Argument übergeben. Die ISBN wird in einem privaten Unterstützungsfeld gespeichert, das automatisch vom Compiler generiert wird.

  • Eine schreibgeschützte Eigenschaft Author. Der Autorenname wird als Argument beiden Book-Konstruktoren übergeben und in der Eigenschaft gespeichert.

  • Zwei schreibgeschützte preisbezogene Eigenschaften, Price und Currency. Ihre Werte werden in einem Aufruf der SetPrice-Methode als Argumente bereitgestellt. Die Eigenschaft Currency ist das dreistellige ISO-Währungssymbol (z. B. USD für den US-Dollar). ISO-Währungssymbole können aus der Eigenschaft ISOCurrencySymbol abgerufen werden. Diese beiden Eigenschaften sind aus externer Richtung schreibgeschützt, aber beide können durch Code in der Book-Klasse festgelegt werden.

  • Eine SetPrice-Methode, die die Werte der Eigenschaften Price und Currency festlegt. Diese Werte werden von diesen selben Eigenschaften zurückgegeben.

  • Überschreibt die ToString-Methode (geerbt von Publication) und die Methoden Object.Equals(Object) und GetHashCode (geerbt von Object).

    Sofern sie nicht überschrieben wird, führt die Methode Object.Equals(Object) Tests hinsichtlich der Verweisgleichheit durch. D.h., zwei Objektvariablen werden als gleich betrachtet, wenn sie auf das gleiche Objekt verweisen. Andererseits sollten in der Book-Klasse zwei Book-Objekte gleich sein, wenn sie die gleiche ISBN haben.

    Wenn Sie die Object.Equals(Object)-Methode überschreiben, müssen Sie auch die GetHashCode-Methode überschreiben. Diese gibt einen Wert zurück, den die Laufzeit zum Speichern von Elementen in Hashauflistungen für einen effizienten Abruf verwendet. Der Hashcode sollte einen Wert zurückgeben, der mit dem Test auf Gleichheit konsistent ist. Da Sie Object.Equals(Object) überschrieben haben, sodass true zurückgegeben wird, wenn die ISBN-Eigenschaften von zwei Book-Objekten gleich sind, geben Sie den Hash zurück, der durch Aufrufen der GetHashCode-Methode der von der Eigenschaft ISBN zurückgegebenen Zeichenfolge berechnet wurde.

Die folgende Abbildung veranschaulicht die Beziehung zwischen der Book-Klasse und Publication, ihrer Basisklasse.

Die Klassen „Publication“ und „Book“

Sie können jetzt ein Book-Objekt instanziieren, sowohl dessen eindeutige als auch geerbte Member aufrufen und es als Argument an eine Methode übergeben, die einen Parameter des Typs Publication oder Book erwartet, wie im folgenden Beispiel dargestellt.

public class ClassExample
{
    public static void Main()
    {
        var book = new Book("The Tempest", "0971655819", "Shakespeare, William",
                            "Public Domain Press");
        ShowPublicationInfo(book);
        book.Publish(new DateTime(2016, 8, 18));
        ShowPublicationInfo(book);

        var book2 = new Book("The Tempest", "Classic Works Press", "Shakespeare, William");
        Console.Write($"{book.Title} and {book2.Title} are the same publication: " +
              $"{((Publication)book).Equals(book2)}");
    }

    public static void ShowPublicationInfo(Publication pub)
    {
        string pubDate = pub.GetPublicationDate();
        Console.WriteLine($"{pub.Title}, " +
                  $"{(pubDate == "NYP" ? "Not Yet Published" : "published on " + pubDate):d} by {pub.Publisher}");
    }
}
// The example displays the following output:
//        The Tempest, Not Yet Published by Public Domain Press
//        The Tempest, published on 8/18/2016 by Public Domain Press
//        The Tempest and The Tempest are the same publication: False

Entwerfen abstrakter Basisklassen und der von ihnen abgeleiteten Klassen

Im vorherigen Beispiel haben Sie eine Basisklasse definiert, die eine Implementierung für eine Reihe von Methoden bereitstellte, um abgeleiteten Klassen die gemeinsame Codenutzung zu erlauben. In vielen Fällen wird jedoch nicht erwartet, dass die Basisklasse eine Implementierung bereitstellt. Stattdessen ist die Basisklasse eine abstrakte Klasse, die abstrakte Methoden deklariert. Sie dient als Vorlage, die die Member definiert, die jede abgeleitete Klasse implementieren muss. In einer abstrakten Basisklasse ist die Implementierung jedes abgeleiteten Typs in der Regel für diesen Typ eindeutig. Sie haben die Klasse mit dem Schlüsselwort „abstract“ markiert, weil ein Publication-Objekt nicht instanziiert werden sollte, obwohl die Klasse für Veröffentlichungen übliche Funktionsimplementierungen bereitgestellt hat.

Jede geschlossene zweidimensionale geometrische Form besitzt beispielsweise zwei Eigenschaften: den Flächeninhalt, die innere Ausdehnung der Form; und den Umfang, d.h. die Länge der Kanten der Form. Wie diese Eigenschaften berechnet werden, hängt jedoch vollständig von der jeweiligen Form ab. Die Formel zum Berechnen des Umfangs eines Kreises unterscheidet sich beispielsweise von der Formel zum Berechnen des Umfangs eines Vierecks. Die Shape-Klasse ist eine abstract-Klasse mit abstract-Methoden. Das bedeutet, dass abgeleitete Klassen die gleiche Funktionalität haben, diese Funktionalität jedoch unterschiedlich implementieren.

Das folgende Beispiel definiert eine abstrakte Basisklasse mit dem Namen Shape, die zwei Eigenschaften definiert: Area und Perimeter. Jede Klasse wird mit dem abstract-Schlüsselwort markiert und auch jeder Instanzmember wird mit dem abstract-Schlüsselwort markiert. In diesem Fall überschreibt Shape auch die Object.ToString -Methode, um den Namen des Typs anstelle dessen vollqualifizierten Namens zurückzugeben. Außerdem definiert sie zwei statische Member, GetArea und GetPerimeter, die Aufrufern ermöglichen, mühelos Fläche und Umfang einer Instanz einer beliebigen abgeleiteten Klasse abzurufen. Wenn Sie eine Instanz einer abgeleiteten Klasse an eine der beiden Methoden übergeben, ruft die Laufzeit die Methodenüberschreibung der abgeleiteten Klasse auf.

public abstract class Shape
{
    public abstract double Area { get; }

    public abstract double Perimeter { get; }

    public override string ToString() => GetType().Name;

    public static double GetArea(Shape shape) => shape.Area;

    public static double GetPerimeter(Shape shape) => shape.Perimeter;
}

Dann können Sie einige Klassen von Shape ableiten, die bestimmte Formen darstellen. Das folgende Beispiel definiert drei Klassen, Square, Rectangle und Circle. Jede verwendet eine Formel, die für die Berechnung von Fläche und Umfang der betreffenden Form eindeutig ist. Einige der abgeleiteten Klassen definieren auch Eigenschaften, z.B. Rectangle.Diagonal und Circle.Diameter, die für die Form, die sie darstellen, eindeutig sind.

using System;

public class Square : Shape
{
    public Square(double length)
    {
        Side = length;
    }

    public double Side { get; }

    public override double Area => Math.Pow(Side, 2);

    public override double Perimeter => Side * 4;

    public double Diagonal => Math.Round(Math.Sqrt(2) * Side, 2);
}

public class Rectangle : Shape
{
    public Rectangle(double length, double width)
    {
        Length = length;
        Width = width;
    }

    public double Length { get; }

    public double Width { get; }

    public override double Area => Length * Width;

    public override double Perimeter => 2 * Length + 2 * Width;

    public bool IsSquare() => Length == Width;

    public double Diagonal => Math.Round(Math.Sqrt(Math.Pow(Length, 2) + Math.Pow(Width, 2)), 2);
}

public class Circle : Shape
{
    public Circle(double radius)
    {
        Radius = radius;
    }

    public override double Area => Math.Round(Math.PI * Math.Pow(Radius, 2), 2);

    public override double Perimeter => Math.Round(Math.PI * 2 * Radius, 2);

    // Define a circumference, since it's the more familiar term.
    public double Circumference => Perimeter;

    public double Radius { get; }

    public double Diameter => Radius * 2;
}

Im folgenden Beispiel werden von Shape abgeleitete Objekte verwendet. Es instanziiert ein Array von Objekten, die von Shape abgeleitet sind, und ruft die statischen Methoden der Shape-Klasse auf, deren Umschließungen Shape-Eigenschaftswerte zurückgeben. Die Laufzeit ruft Werte aus den überschriebenen Eigenschaften der abgeleiteten Typen ab. Das Beispiel wandelt auch jedes Shape -Objekt im Array in seinen abgeleiteten Typ um, und wenn die Umwandlung erfolgreich ist, ruft es Eigenschaften dieser bestimmten Unterklasse von Shape auf.

using System;

public class Example
{
    public static void Main()
    {
        Shape[] shapes = { new Rectangle(10, 12), new Square(5),
                    new Circle(3) };
        foreach (Shape shape in shapes)
        {
            Console.WriteLine($"{shape}: area, {Shape.GetArea(shape)}; " +
                              $"perimeter, {Shape.GetPerimeter(shape)}");
            if (shape is Rectangle rect)
            {
                Console.WriteLine($"   Is Square: {rect.IsSquare()}, Diagonal: {rect.Diagonal}");
                continue;
            }
            if (shape is Square sq)
            {
                Console.WriteLine($"   Diagonal: {sq.Diagonal}");
                continue;
            }
        }
    }
}
// The example displays the following output:
//         Rectangle: area, 120; perimeter, 44
//            Is Square: False, Diagonal: 15.62
//         Square: area, 25; perimeter, 20
//            Diagonal: 7.07
//         Circle: area, 28.27; perimeter, 18.85