Freigeben über


Vererbung in C# und .NET

In diesem Lernprogramm wird die Vererbung in C# vorgestellt. Die Vererbung ist ein Feature objektorientierter Programmiersprachen, mit der Sie eine Basisklasse definieren können, die bestimmte Funktionen (Daten und Verhalten) bereitstellt, und abgeleitete Klassen definieren, die diese Funktionalität erben oder überschreiben.

Voraussetzungen

Installationsanweisungen

Unter Windows diese WinGet-Konfigurationsdatei zum Installieren aller erforderlichen Komponenten. Wenn Sie bereits etwas installiert haben, überspringt WinGet diesen Schritt.

  1. Laden Sie die Datei herunter, und doppelklicken Sie, um sie auszuführen.
  2. Lesen Sie den Lizenzvertrag, geben Sie yein, und wählen Sie Geben Sie ein, wenn Sie zur Annahme aufgefordert werden.
  3. Wenn Sie in Ihrer Taskleiste eine blinkende Eingabeaufforderung für die Benutzerkontensteuerung (User Account Control, UAC) erhalten, können Sie die Installation fortsetzen.

Auf anderen Plattformen müssen Sie jede dieser Komponenten separat installieren.

  1. Laden Sie das empfohlene Installationsprogramm von der .NET SDK-Downloadseite herunter , und doppelklicken Sie, um es auszuführen. Die Downloadseite erkennt Ihre Plattform und empfiehlt das neueste Installationsprogramm für Ihre Plattform.
  2. Laden Sie das neueste Installationsprogramm von der Visual Studio Code Startseite herunter, und doppelklicken Sie, um es auszuführen. Diese Seite erkennt auch Ihre Plattform, und der Link sollte für Ihr System korrekt sein.
  3. Klicken Sie auf der Erweiterungsseite C# DevKit auf die Schaltfläche "Installieren". Dadurch wird Visual Studio-Code geöffnet und gefragt, ob Sie die Erweiterung installieren oder aktivieren möchten. Wählen Sie "Installieren" aus.

Ausführen der Beispiele

Um die Beispiele in diesem Lernprogramm zu erstellen und auszuführen, verwenden Sie das Dotnet-Hilfsprogramm über die Befehlszeile. Führen Sie die folgenden Schritte für jedes Beispiel aus:

  1. Erstellen Sie ein Verzeichnis zum Speichern des Beispiels.

  2. Geben Sie den neuen Konsolenbefehl dotnet an einer Eingabeaufforderung 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 " über die 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. Zu diesen zählen 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 " ein, um das Beispiel zu kompilieren und auszuführen.

Hintergrund: Was ist Vererbung?

Die Vererbung ist eines der grundlegenden Attribute 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 Mitglieder geerbt werden, wird als die sogenannte Basisklassebezeichnet. Die Klasse, die die Mitglieder der Basisklasse erbt, wird als abgeleitete Klassebezeichnet.

C# und .NET unterstützen nur die einzelne Vererbung . Das heißt, eine Klasse kann nur von einer einzigen Klasse erben. Die Vererbung ist jedoch transitiv, sodass Sie eine Vererbungshierarchie für einen Satz von Typen definieren können. Mit anderen Worten: Der Typ D kann vom Typ Cerben, der vom Typ Berbt, der vom Basisklassentyp Aerbt. Da die Vererbung transitiv ist, sind die Mitglieder des Typs A für den Typ D verfügbar.

Nicht alle Mitglieder einer Basisklasse werden von abgeleiteten Klassen vererbt. Die folgenden Mitglieder werden nicht vererbt.

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

  • Instanzkonstruktoren, die Sie aufrufen, um eine neue Instanz der Klasse zu erstellen. Jede Klasse muss eigene 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 handelt es sich um eine geschachtelte Klasse, A.B, die von A abgeleitet wird, und C, das von A abgeleitet wird. Das private A._value Feld ist in A.B sichtbar. Wenn Sie jedoch die Kommentare aus der C.GetValue Methode entfernen und versuchen, das Beispiel zu kompilieren, erzeugt es Compilerfehler CS0122: "'A._value' ist aufgrund seiner Schutzebene nicht zugänglich."

    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 derselben Assembly wie die Basisklasse befinden. Sie sind in abgeleiteten Klassen, die sich in einer anderen Assembly befinden, nicht sichtbar als die Basisklasse.

  • Ö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 die Klasse A eine Methode mit dem Namen Method1und die Klasse B erbt von der Klasse A. Das Beispiel ruft dann so auf Method1 , als wäre es eine Instanzmethode für 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 ein Element außer Kraft setzen zu können, muss das Element in der Basisklasse mit dem virtuellen Schlüsselwort gekennzeichnet werden. 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 Implementierung der Basisklasse überschreiben. Basisklassenmitglieder, die mit dem abstract Schlüsselwort gekennzeichnet sind, erfordern, dass abgeleitete Klassen sie überschreiben. Beim Kompilieren des folgenden Beispiels wird der Compilerfehler CS0534 generiert: "<Klasse> implementiert das geerbte abstrakte Mitglied <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.
    }
}

Die Vererbung gilt nur für Klassen und Schnittstellen. Andere Typkategorien (Strukturen, Delegate und Enumerationen) unterstützen keine Vererbung. Aufgrund dieser Regeln erzeugt der Versuch, Code wie das folgende Beispiel zu kompilieren, den Compilerfehler CS0527: "Typ 'ValueType' in der Schnittstellenliste ist keine Schnittstelle." Die Fehlermeldung gibt an, dass die Vererbung nicht unterstützt wird, obwohl Sie die Schnittstellen definieren können, die eine Struktur implementiert.

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

Implizite Vererbung

Neben den Typen, die sie durch Einzelvererbung erben können, erben alle Typen im .NET-Typsystem implizit von Object oder einem davon abgeleiteten Typ. Die allgemeine Funktionalität von Object steht allen Typen zur Verfügung.

Um zu sehen, was implizite Vererbung bedeutet, definieren wir eine neue Klasse, SimpleClassd. h. einfach eine leere Klassendefinition:

public class SimpleClass
{ }

Anschließend können Sie die Spiegelung verwenden (mit der Sie die Metadaten eines Typs überprüfen können, um Informationen zu diesem Typ abzurufen), um eine Liste der Mitglieder abzurufen, die zum SimpleClass Typ gehören. Obwohl Sie keine Member in Ihrer SimpleClass Klasse definiert haben, gibt die Ausgabe aus dem Beispiel an, dass sie tatsächlich neun Elemente enthält. Eines dieser Member ist ein parameterloser (oder Standard)-Konstruktor, der automatisch vom C#-Compiler für den SimpleClass Typ bereitgestellt 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

Die implizite Vererbung von der Object Klasse macht folgende Methoden für die 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 die Gleichheit von zwei Objekten testen: die öffentliche Instanzmethode Equals(Object) , die öffentliche statische Equals(Object, Object) Methode und die öffentliche statische ReferenceEquals(Object, Object) Methode. Standardmäßig testen diese Methoden die Referenzgleichheit; d. h. zwei Objektvariablen müssen auf dasselbe Objekt verweisen.

  • Die öffentliche GetHashCode Methode, die einen Wert berechnet, mit dem eine Instanz des Typs in Hashsammlungen verwendet werden kann.

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

  • Die geschützte Finalize Methode, die dazu dient, nicht verwaltete Ressourcen freizugeben, bevor der Speicher eines Objekts durch den Garbage Collector zurückgewonnen wird.

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

Aufgrund der impliziten Vererbung können Sie jedes geerbte Element von einem SimpleClass Objekt so aufrufen, als wäre es tatsächlich ein Element, das in der SimpleClass Klasse definiert wurde. Im folgenden Beispiel wird die Methode SimpleClass.ToString aufgerufen, die von SimpleClassObject geerbt wird.

public class EmptyClass
{ }

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

In der folgenden Tabelle sind die Kategorien von Typen aufgeführt, 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
Klasse Object
Struktur ValueType, Object
Enumeration Enum, ValueTypeObject
delegieren MulticastDelegate, DelegateObject

Vererbung und eine „ist ein“-Beziehung

Normalerweise wird Vererbung verwendet, um eine "ist-eine"-Beziehung zwischen einer Basisklasse und einer oder mehreren abgeleiteten Klassen auszudrücken. Dabei sind die abgeleiteten Klassen spezialisierte Versionen der Basisklasse; die abgeleitete Klasse ist ein Typ der Basisklasse. Beispielsweise stellt die Klasse Publication eine Publikation beliebiger Art dar, und die Klassen Book und Magazine stellen bestimmte Arten 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 von Funktionen (z. B. die Möglichkeit zum Testen der Gleichheit, zum Vergleichen oder Sortieren von Objekten oder zur Unterstützung von kultursensitiver Analyse und Formatierung), die die Schnittstelle für die Implementierungstypen zur Verfügung stellt.

Beachten Sie, dass "is a" auch die Beziehung zwischen einem Typ und einer bestimmten Instanziierung dieses Typs ausdrückt. Im folgenden Beispiel ist Automobile eine Klasse mit drei einzigartigen, schreibgeschützten Eigenschaften: Make, der Hersteller des Automobils; Model, die Art des Automobils; und Year, das Jahr der Herstellung. Ihre Automobile Klasse verfügt auch über einen Konstruktor, dessen Argumente den Eigenschaftswerten zugewiesen sind, und überschreibt die Object.ToString Methode, um eine Zeichenfolge zu erzeugen, die die Automobile Instanz eindeutig identifiziert, anstatt die Automobile Klasse.

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 Autohersteller und -modelle darzustellen. Sie müssen beispielsweise keinen Packard Typ definieren, der Automobile repräsentiert, die von der Packard Motor Car Company hergestellt werden. Stattdessen können Sie sie darstellen, indem Sie wie im folgenden Beispiel ein Automobile Objekt mit den entsprechenden Werten erstellen, die an den Klassenkonstruktor übergeben werden.

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 Beziehung wird am besten auf eine Basisklasse und auf abgeleitete Klassen angewendet, die der Basisklasse zusätzliche Elemente hinzufügen oder zusätzliche Funktionen erfordern, die in der Basisklasse nicht vorhanden sind.

Entwerfen der Basisklasse und abgeleiteter Klassen

Sehen wir uns den Prozess des Entwerfens einer Basisklasse und der abgeleiteten Klassen an. In diesem Abschnitt definieren Sie eine Basisklasse, Publication, die eine Publikation jeglicher Art darstellt, z. B. ein Buch, eine Zeitschrift, eine Zeitung, ein Journal, einen Artikel usw. Darüber hinaus definieren Sie eine Klasse Book, die von Publication abgeleitet ist. Sie können das Beispiel ganz einfach erweitern, um andere abgeleitete Klassen zu definieren, wie z. B. Magazine, Journal, Newspaper und Article.

Die Basisklasse „Publication“

Beim Entwerfen Ihrer Publication Klasse müssen Sie mehrere Designentscheidungen treffen:

  • Welche Member in Ihre Basisklasse Publication einbezogen werden sollen und ob die Publication Member Methodenimplementierungen bereitstellen oder ob es Publication sich um eine abstrakte Basisklasse handelt, die als Vorlage für die abgeleiteten Klassen dient.

    In diesem Fall stellt die Publication Klasse Methodenimplementierungen bereit. Der Abschnitt "Entwerfen abstrakter Basisklassen" und deren abgeleiteter Klassen enthält ein Beispiel, das eine abstrakte Basisklasse verwendet, um die Methoden zu definieren, die abgeleitete Klassen überschreiben müssen. Abgeleitete Klassen können jede Implementierung bereitstellen, die für den abgeleiteten Typ geeignet ist.

    Die Möglichkeit, Code wiederzuverwenden (d. h. mehrere abgeleitete Klassen teilen die Deklaration und Implementierung von Basisklassenmethoden und müssen sie nicht überschreiben), ist ein Vorteil nicht abstrakter Basisklassen. Daher sollten Sie Mitglieder zu Publication hinzufügen, wenn ihr Code wahrscheinlich von einigen oder den meisten spezialisierten Publication Typen geteilt wird. Wenn Sie Basisklassenimplementierungen nicht effizient bereitstellen, müssen Sie letztendlich weitgehend identische Memberimplementierungen in abgeleiteten Klassen bereitstellen, anstatt eine einzelne Implementierung in der Basisklasse. Die Notwendigkeit, doppelten Code an mehreren Speicherorten beizubehalten, ist eine potenzielle Quelle von Fehlern.

    Um die Wiederverwendung von Code zu maximieren und eine logische und intuitive Vererbungshierarchie zu erstellen, sollten Sie sicherstellen, dass Sie nur die Daten und Funktionen, die für alle oder für die meisten Publikationen gelten, in die Publication Klasse aufnehmen. Abgeleitete Klassen implementieren dann Elemente, die für die jeweiligen Publikationsarten eindeutig sind, die sie darstellen.

  • Wie weit Ihre Klassenhierarchie erweitert werden soll. Möchten Sie eine Hierarchie von drei oder mehr Klassen entwickeln, anstatt einfach nur eine Basisklasse und eine oder mehrere abgeleitete Klassen? Beispielsweise könnte Publication eine Basisklasse von Periodical sein, die wiederum eine Basisklasse von Magazine, Journal und Newspaper ist.

    Beispielsweise verwenden Sie die kleine Hierarchie einer Publication Klasse und eine einzelne abgeleitete Klasse. Book Sie können das Beispiel ganz einfach erweitern, um mehrere zusätzliche Klassen zu erstellen, die von Publication abgeleitet sind, wie Magazine und Article.

  • Ob es sinnvoll ist, die Basisklasse zu instanziieren. Wenn dies nicht der Fall ist, sollten Sie das Schlüsselwort abstract auf die Klasse anwenden. Andernfalls kann Ihre Publication Klasse durch Aufrufen des Klassenkonstruktors instanziiert werden. Wenn versucht wird, eine Klasse zu instanziieren, die durch einen direkten Aufruf des Klassenkonstruktors mit dem abstract Schlüsselwort gekennzeichnet ist, generiert der C#-Compiler fehler CS0144, "Eine Instanz der abstrakten Klasse oder Schnittstelle kann nicht erstellt werden." Wenn versucht wird, die Klasse mithilfe der Spiegelung instanziieren zu können, löst die Spiegelungsmethode eine MemberAccessException.

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

    Beispielsweise markieren Sie die Publication Klasse als abstrakt , damit sie nicht instanziiert werden kann. Eine abstract Klasse ohne abstract Methoden gibt an, dass diese Klasse ein abstraktes Konzept darstellt, das von mehreren konkreten Klassen (z. B. einem Book, ) Journalgemeinsam genutzt wird.

  • Gibt an, ob abgeleitete Klassen die Basisklassenimplementierung bestimmter Member erben müssen, unabhängig davon, ob sie die Basisklassenimplementierung überschreiben können oder ob sie eine Implementierung bereitstellen müssen. Sie verwenden das abstract-Schlüsselwort, damit abgeleitete Klassen eine Implementierung bereitstellen. Sie verwenden das virtuelle Schlüsselwort, um abgeleiteten Klassen das Überschreiben einer Basisklassenmethode zu ermöglichen. Standardmäßig sind in der Basisklasse definierte Methoden nicht überschreibbar.

    Die Publication Klasse hat abstract keine Methoden, aber die Klasse selbst ist abstract.

  • Gibt an, ob eine abgeleitete Klasse die endgültige Klasse in der Vererbungshierarchie darstellt und nicht selbst als Basisklasse für zusätzliche abgeleitete Klassen verwendet werden kann. Standardmäßig kann jede Klasse als Basisklasse dienen. Sie können das versiegelte Schlüsselwort anwenden, um anzugeben, dass eine Klasse nicht als Basisklasse für weitere Klassen dienen kann. Der Versuch, von einer versiegelten Klasse abzuleiten, generierte Compilerfehler CS0509, "kann nicht von versiegeltem Typ <typeName> abgeleitet werden."

    In Ihrem Beispiel markieren Sie ihre abgeleitete Klasse als sealed.

Das folgende Beispiel zeigt den Quellcode für die Publication Klasse sowie eine PublicationType Aufzählung, die von der Publication.PublicationType Eigenschaft 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 lautet abstract, kann sie nicht direkt aus Code instanziiert werden, wie im folgenden Beispiel:

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

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

  • Zwei publikationsbezogene Eigenschaften

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

    Pages ist eine Lese-/Schreibeigenschaft Int32 , die angibt, wie viele Seiten die Publikation hat. Der Wert wird in einem privaten Feld mit dem Namen totalPagesgespeichert. Es muss eine positive Zahl sein, sonst wird eine ArgumentOutOfRangeException ausgelöst.

  • Publisher-bezogene Mitglieder

    Zwei schreibgeschützte Eigenschaften, Publisher und Type. Die Werte werden ursprünglich vom Aufruf des Publication Klassenkonstruktors bereitgestellt.

  • Veröffentlichungsbezogene Elemente

    Zwei Methoden Publish und GetPublicationDate, festlegen und zurückgeben das Publikationsdatum. Die Publish-Methode setzt beim Aufrufen das private published-Flag auf true und weist das als Argument übergebene Datum dem privaten datePublished-Feld zu. Die GetPublicationDate Methode gibt die Zeichenfolge "NYP" zurück, wenn das published Flag auf false gesetzt ist, und den Wert des datePublished Felds, wenn dieser true ist.

  • Copyrightbezogene Elemente

    Die Copyright Methode nimmt den Namen des Copyrightinhabers und das Jahr des Urheberrechts als Argumente und weist sie an die Eigenschaften CopyrightName und CopyrightDate.

  • Eine Außerkraftsetzung der ToString Methode

    Wenn ein Typ die Object.ToString Methode nicht überschreibt, gibt er den vollqualifizierten Namen des Typs zurück, der bei der Unterscheidung einer Instanz von einer anderen nur wenig verwendet wird. Die Publication-Klasse überschreibt Object.ToString, um den Wert der Title-Eigenschaft zurückzugeben.

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

Die Klassen

Die Book-Klasse

Die Book Klasse stellt ein Buch als spezialisierten Publikationstyp 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 verwenden drei gemeinsame Parameter. Zwei, Titel und Herausgeber entsprechen 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 ISBN auto-Eigenschaft gespeichert ist.

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

    Der zweite Konstruktor verwendet das Basisschlüsselwort , um den Titel und den Herausgebernamen an den Basisklassenkonstruktor zu übergeben. Wenn Sie keinen expliziten Aufruf eines Basisklassenkonstruktors in Ihrem Quellcode ausführen, stellt der C#-Compiler automatisch einen Aufruf des Standard- 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 als Argument für einen der Book Konstruktoren bereitgestellt. Die ISBN wird in einem privaten Sicherungsfeld gespeichert, das automatisch vom Compiler generiert wird.

  • Eine schreibgeschützte Eigenschaft Author. Der Name des Autors wird als Argument für beide Book Konstruktoren angegeben und in der Eigenschaft gespeichert.

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

  • Eine SetPrice Methode, die die Werte der Price Und Currency Eigenschaften festlegt. Diese Werte werden durch dieselben Eigenschaften zurückgegeben.

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

    Sofern sie nicht außer Kraft gesetzt wird, prüft die Object.Equals(Object) Methode die Referenzgleichheit. Das heißt, zwei Objektvariablen werden als gleich angesehen, wenn sie auf dasselbe Objekt verweisen. In der Book Klasse hingegen sollten 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, die einen Wert zurückgibt, den die Laufzeit zum Speichern von Elementen in Hashauflistungen zum effizienten Abrufen verwendet. Der Hashcode sollte einen Wert zurückgeben, der mit dem Test auf Gleichheit konsistent ist. Da Sie die Methode Object.Equals(Object) überschrieben haben, um true zurückzugeben, wenn die ISBN-Eigenschaften von zwei Book-Objekten gleich sind, geben Sie den von der GetHashCode-Eigenschaft zurückgegebenen Hashcode zurück, indem Sie die ISBN-Methode der zurückgegebenen Zeichenfolge aufrufen.

In der folgenden Abbildung wird die Beziehung zwischen der Book Klasse und Publicationder Basisklasse veranschaulicht.

Die Klassen „Publication“ und „Book“

Sie können ein Book Objekt jetzt instanziieren, sowohl seine eindeutigen als auch geerbten Member aufrufen und als Argument an eine Methode übergeben, die einen Parameter vom Typ Publication oder typ Bookerwartet, wie im folgenden Beispiel gezeigt.

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 ihrer abgeleiteten Klassen

Im vorherigen Beispiel haben Sie eine Basisklasse definiert, die eine Implementierung für eine Reihe von Methoden bereitgestellt hat, damit abgeleitete Klassen Code freigeben können. In vielen Fällen wird jedoch nicht erwartet, dass die Basisklasse eine Implementierung bereitstellt. Stattdessen handelt es sich bei der Basisklasse um eine abstrakte Klasse , die abstrakte Methoden deklariert. sie dient als Vorlage, die die Member definiert, die jede abgeleitete Klasse implementieren muss. Typischerweise ist in einer abstrakten Basisklasse die Implementierung jedes abgeleiteten Typs einzigartig für diesen Typ. Sie haben die Klasse mit dem abstrakten Schlüsselwort markiert, da es nicht sinnvoll war, ein Publication Objekt zu instanziieren, obwohl die Klasse Implementierungen von Funktionen bereitstellte, die für Publikationen üblich sind.

Beispielsweise enthält jede geschlossene zweidimensionale geometrische Form zwei Eigenschaften: Fläche, das innere Ausmaß der Form; und Umrandung oder der Abstand entlang der Ränder der Form. Die Art und Weise, in der diese Eigenschaften berechnet werden, hängt jedoch vollständig von der spezifischen Form ab. Die Formel zum Berechnen des Umkreises (oder des Umfangs) eines Kreises unterscheidet sich beispielsweise von der eines Quadrats. Die Shape Klasse ist eine abstract Klasse mit abstract Methoden. Das bedeutet, dass abgeleitete Klassen dieselbe Funktionalität aufweisen, aber diese abgeleiteten Klassen implementieren diese Funktionalität anders.

Im folgenden Beispiel wird eine abstrakte Basisklasse definiert Shape , die zwei Eigenschaften definiert: Area und Perimeter. Neben der Markierung der Klasse mit dem abstract-Schlüsselwort wird jedes Instanzmitglied auch 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 es zwei statische Member, GetArea und GetPerimeter, die es Anrufern ermöglichen, den Bereich und den Umkreis einer Instanz einer abgeleiteten Klasse einfach abzurufen. Wenn Sie eine Instanz einer abgeleiteten Klasse an eine dieser 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;
}

Anschließend können Sie einige Klassen von Shape ableiten, die bestimmte Formen repräsentieren. Das folgende Beispiel definiert drei Klassen, Square, Rectangle und Circle. Jede verwendet eine für dieses bestimmte Shape eindeutige Formel, um den Bereich und den Umkreis zu berechnen. Einige der abgeleiteten Klassen definieren auch Eigenschaften, wie Rectangle.Diagonal und Circle.Diameter, die einzigartig für die Form sind, die sie darstellen.

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 Objekte verwendet, die von Shape abgeleitet sind. Es instanziiert ein Array von Objekten, die von Shape abgeleitet sind, und ruft die statischen Methoden der Shape Klasse auf, die Rückgabewerte der Shape Eigenschaften umschließen. Die Laufzeit ruft Werte aus den überschriebenen Eigenschaften der abgeleiteten Typen ab. Das Beispiel castet auch jedes Shape Objekt im Array in seinen abgeleiteten Typ und ruft, wenn die Umwandlung erfolgreich ist, die Eigenschaften der entsprechenden Unterklasse von Shape ab.

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