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.

Erforderliche Komponenten

In diesem Tutorial wird vorausgesetzt, dass Sie .NET Core installiert haben. Installationsanweisungen finden Sie im .NET Core-Installationshandbuch. Sie benötigen auch einen Code-Editor. In diesem Tutorial wird Visual Studio Code verwendet, obwohl Sie einen Code-Editor Ihrer Wahl verwenden können.

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.
  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, verursacht dies Compilerfehler CS0122: „Der Zugriff auf "A.value" ist aufgrund des Schutzgrads nicht möglich.“

    using System;
    
    public class A 
    {
       private int value = 10;
    
       public class B : A
       {
           public int GetValue()
           {
               return this.value;
           }     
       }
    }
    
    public class C : A
    {
    //    public int GetValue()
    //    {
    //        return this.value;
    //    }
    }
    
    public class Example
    {
        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 wurden. 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();
        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 einen nicht virtuellen Member zu überschreiben, verursacht den Compilerfehler CS0506: „"" : Der geerbte 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: „ implementiert den geerbten abstrakten Member nicht.“, da 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. Aus diesem Grund verursacht der Versuch, Code wie den folgenden zu kompilieren, den Compilerfehler CS0527: „Der 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.

using System;

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 <xref:System.Object> oder einem davon abgeleiteten Typ. Dadurch wird sichergestellt, dass die allgemeine Funktionalität für einen beliebigen Typ verfügbar ist.

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

public class SimpleClass
{}

Wir können dann die Reflektion (die uns 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 wir keine Member in unserer SimpleClass-Klasse definiert haben, gibt die Ausgabe des Beispiels an, dass sie tatsächlich neun Member hat. Einer davon ist ein parameterloser (oder standardmäßiger) Konstruktor, der automatisch vom C#-Compiler für den SimpleClass-Typ angegeben wird. Die verbleibenden acht sind Member von <xref:System.Object>, dem Typ, von dem alle Klassen und Schnittstellen im .NET-Typsystem letztlich implizit erben.

using System;
using System.Reflection;

public class Example
{
   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 (var 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";
         }
         var 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 <xref:System.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 <xref:System.Type> -Objekt zurückgibt, das den SimpleClass -Typ darstellt.

  • Die geschützte <xref:System.Object.Finalize%2A> -Methode, die nicht verwaltete Ressourcen freigeben soll, bevor der Speicher eines Objekts durch den Garbage Collector freigegeben wird.

  • Die geschützte <xref:System.Object.MemberwiseClone%2A> -Methode, die einen flachen Klon des aktuellen Objekts erstellt.

Aufgrund der impliziten Vererbung können wir 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 <xref:System.Object> erbt.

using System;

public class SimpleClass
{}

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

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
Klasse <xref:System.Object>
struct <xref:System.ValueType>, <xref:System.Object>
enum <xref:System.Enum>, <xref:System.ValueType>, <xref:System.Object>
delegate <xref:System.MulticastDelegate>, <xref:System.Delegate>, <xref:System.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. Unsere Automobile -Klasse verfügt auch über einen Konstruktor, dessen Argumente den Eigenschaftswerten zugewiesen werden, und er überschreibt die <xref:System.Object.ToString%2A?displayProperty=fullName> -Methode, um eine Zeichenfolge zu erzeugen, die eindeutig die Automobile -Instanz anstelle der Automobile -Klasse identifiziert.

using System;

public class Automobile
{
    public Automobile(string make, string model, int year)
    {
        if (make == null)
           throw new ArgumentNullException("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("The model cannot be null.");
        else if (String.IsNullOrWhiteSpace(make))
           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 wir uns nicht auf die Vererbung verlassen, um bestimmte Automarken und Modelle darzustellen. Wir müssen z.B. keinen Packard-Typ definieren, um Autos darzustellen, die von der Packard Motor Car Company hergestellt werden. Stattdessen können wir sie durch Erstellen eines Automobile-Objekts darstellen, wobei die entsprechenden Werten an dessen Klassenkonstruktor übergeben werden, wie es im folgenden Beispiel geschieht.

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 wir eine Basisklasse Publication, die eine Publikation jeder Art darstellt, z.B. ein Buch, eine Zeitschrift, eine Zeitung, ein Journal, einen Artikel usw. Wir definieren auch eine Book-Klasse, die von Publication abgeleitet ist. Wir könnten das Beispiel einfach erweitern, um andere abgeleitete Klassen wie Magazine, Journal, Newspaper und Article zu definieren.

Die Basisklasse „Publication“

Beim Entwurf unserer Publication-Klasse müssen wir einige Entwurfsentscheidungen treffen:

  • Welche Member sollen in unsere Basis-Publication-Klasse 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 wir Member zu Publication hinzufügen, wenn ihr Code vermutlich von einigen oder den meisten spezialisierten Publication-Typen gemeinsam genutzt wird. Wenn wir dies nicht effizient durchführen, müssen wir letztendlich weitgehend identische Implementierungen von Membern 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 Code zu maximieren als auch eine logische und intuitive Vererbungshierarchie zu erstellen, möchten wir sicher sein, dass wir in die Publication-Klasse nur die Daten und Funktionen einbeziehen, die alle bzw. die meisten Publikationen gemeinsam haben. Abgeleitete Klassen implementieren dann Member, die für die jeweiligen Publikationsarten, die sie darstellen, eindeutig sind.

  • Wie weit sollen wir unsere Klassenhierarchie ausdehnen? Möchten wir statt nur einer Basisklasse und einer oder mehreren abgeleiteten Klassen eine Hierarchie von drei oder mehr Klassen entwickeln? Beispielsweise könnte Publication eine Basisklasse von Periodical sein, was wiederum eine Basisklasse von Magazine, Journal und Newspaper ist.

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

  • Ist es sinnvoll, die Basisklasse zu instanziieren? Wenn dies nicht der Fall ist, sollten wir das Schlüsselwort abstract auf die Klasse anwenden. Wenn versucht wird, eine mit dem abstract-Schlüsselwort markierte 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 der Reflektion zu instanziieren, löst die Reflektionsmethode eine <xref:System.MemberAccessException> aus. Andernfalls kann unsere Publication-Klasse durch Aufruf ihres Klassenkonstruktors instanziiert werden.

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

    In unserem Beispiel markieren wir die Publication-Klasse als abstract, sodass sie nicht instanziiert werden kann.

  • Müssen abgeleitete Klassen die Implementierung der Basisklasse eines bestimmten Members erben, oder können sie optional die Implementierung der Basisklasse überschreiben? Um abgeleiteten Klassen zu erlauben, eine Basisklassenmethode zu überschreiben, müssen wir das virtual-Schlüsselwort verwenden. Standardmäßig können in der Basisklasse definierte Methoden nicht überschrieben werden.

  • 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. Wir 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 kann nicht abgeleitet werden“.

    In unserem Beispiel markieren wir unsere 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 <xref:System.Object> erbt, definiert die Publication-Klasse die folgenden eindeutigen Member und Memberüberschreibungen:

using System;

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 (publisher == null)
         throw new ArgumentNullException("The publisher cannot be null.");
      else if (String.IsNullOrWhiteSpace(publisher))
         throw new ArgumentException("The publisher cannot consist only of whitespace.");
      Publisher = publisher;
  
      if (title == null)
         throw new ArgumentNullException("The title cannot be null.");
      else if (String.IsNullOrWhiteSpace(title))
         throw new ArgumentException("The title cannot consist only of whitespace.");
      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("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;
      this.datePublished = datePublished;
   }

   public void Copyright(string copyrightName, int copyrightDate)
   {
      if (copyrightName == null)
         throw new ArgumentNullException("The name of the copyright holder cannot be null.");
      else if (String.IsNullOrWhiteSpace(copyrightName))
         throw new ArgumentException("The name of the copyright holder cannot consist only of whitespace.");
      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 Code 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 <xref:System.String>-Eigenschaft, deren Wert durch Aufrufen des Publication-Konstruktors bereitgestellt wird.

    Pages ist eine schreibgeschützte Eigenschaft <xref:System.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 <xref:System.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 setzt ein privates published-Flag auf true, wenn sie aufgerufen wird, und weist das ihr übergebene Datum als Argument dem privaten Feld datePublished 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 <xref:System.Object.ToString%2A?displayProperty=fullName> -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 <xref:System.Object.ToString%2A?displayProperty=fullName>, um den Wert der Eigenschaft Title zurückzugeben.

Die folgende Abbildung veranschaulicht die Beziehung zwischen unserer Basis-Publication -Klasse und der implizit geerbten <xref:System.Object> -Klasse.

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.");
        ulong nISBN = 0;
        if (! UInt64.TryParse(isbn, out nISBN))
            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("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)
   {
      Book book = obj as Book;
      if (book == null)
         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 einem privaten Feld authorName 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. Dies 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 im privaten Feld authorName gespeichert.

  • Zwei schreibgeschützte preisbezogene Eigenschaften, Price und Currency. Ihre Werte werden in einem Aufruf der SetPrice-Methode als Argumente bereitgestellt. Der Preis wird in einem privaten Feld namens bookPrice gespeichert. Die Eigenschaft Currency ist das dreistellige ISO-Währungssymbol (z.B. USD für den US-Dollar) und befindet sich im privaten Feld ISOCurrencySymbol. ISO-Währungssymbole können aus der Eigenschaft <xref:System.Globalization.RegionInfo.ISOCurrencySymbol%2A> abgerufen werden.

  • Eine SetPrice-Methode, die die Werte der Felder bookPrice und ISOCurrencySymbol festlegt. Dies sind die Rückgabewerte der Eigenschaften Price und Currency.

  • Überschreibt die ToString-Methode (geerbt von Publication) und die Methoden <xref:System.Object.Equals%28System.Object%29?displayProperty=fullName> und <xref:System.Object.GetHashCode%2A> (geerbt von <xref:System.Object>).

    Sofern sie nicht überschrieben wird, führt die Methode <xref:System.Object.Equals%28System.Object%29?displayProperty=fullName> Tests hinsichtlich der Verweisgleichheit durch. D.h., zwei Objektvariablen werden als gleich betrachtet, wenn sie auf das gleiche Objekt verweisen. Andererseits sollten im Fall der Book-Klasse zwei Book-Objekte gleich sein, wenn sie die gleiche ISBN haben.

    Wenn Sie die <xref:System.Object.Equals%28System.Object%29?displayProperty=fullName>-Methode überschreiben, müssen Sie auch die <xref:System.Object.GetHashCode%2A>-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 wir <xref:System.Object.Equals%28System.Object%29?displayProperty=fullName> überschrieben haben, sodass true zurückgegeben wird, wenn die ISBN-Eigenschaften von zwei Book-Objekten gleich sind, geben wir den Hashcode zurück, der durch Aufrufen der <xref:System.String.GetHashCode%2A>-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“

Wir 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 gezeigt.

using System;
using static System.Console;

public class Example
{
   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");
      Write($"{book.Title} and {book2.Title} are the same publication: " +
            $"{((Publication) book).Equals(book2)}");
   }

   public static void ShowPublicationInfo(Publication pub)
   {
       string pubDate = pub.GetPublicationDate();
       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 definierten wir eine Basisklasse, die eine Implementierung für eine Reihe von Methoden bereitstellte, um abgeleiteten Klassen die gemeinsame Nutzung von Code zu erlauben. In vielen Fällen wird jedoch nicht erwartet, dass die Basisklasse eine Implementierung bereitstellt. Stattdessen ist die Basisklasse eine abstrakte Klasse; sie dient als Vorlage, die die Member definiert, die jede abgeleitete Klasse implementieren muss. Im Fall einer abstrakten Basisklasse ist die Implementierung jedes abgeleiteten Typs in der Regel für diesen Typ eindeutig.

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 z.B. grundlegend von der zum Berechnen des Umfangs eines Dreiecks.

Das folgende Beispiel definiert eine abstrakte Basisklasse mit dem Namen Shape, die zwei Eigenschaften definiert: Area und Perimeter. Beachten Sie, dass zusätzlich zum Markieren der Klasse mit dem abstract-Schlüsselwort auch jeder Instanzmember mit dem abstract-Schlüsselwort markiert wird. In diesem Fall überschreibt Shape auch die <xref:System.Object.ToString%2A?displayProperty=fullName> -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 wir eine Instanz einer abgeleiteten Klasse an eine der beiden Methoden übergeben, ruft die Laufzeit die Methodenüberschreibung der abgeleiteten Klasse auf.

using System;

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 wir einige Klassen von Shape ableiten, die bestimmte Formen darstellen. Das folgende Beispiel definiert drei Klassen, Triangle, 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. Beachten Sie, dass die Laufzeit Werte aus den überschriebenen Eigenschaften der abgeleiteten Typen abruft. 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 (var shape in shapes) {
         Console.WriteLine($"{shape}: area, {Shape.GetArea(shape)}; " +
                           $"perimeter, {Shape.GetPerimeter(shape)}");
         var rect = shape as Rectangle;
         if (rect != null) {
            Console.WriteLine($"   Is Square: {rect.IsSquare()}, Diagonal: {rect.Diagonal}");
            continue;
         }
         var sq = shape as Square;
         if (sq != null) {
            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

Siehe auch

Klassen und Objekte
Vererbung (C#-Programmierhandbuch)