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
- Wir empfehlen Visual Studio für Windows. Sie können eine kostenlose Version von der Visual Studio-Downloadseite herunterladen. Visual Studio enthält das .NET SDK.
- Sie können auch den Visual Studio Code-Editor mit dem C# DevKit verwenden. Sie müssen das aktuelle .NET SDK separat installieren.
- Wenn Sie einen anderen Editor bevorzugen, müssen Sie das neueste .NET SDK installieren.
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:
Erstellen Sie ein Verzeichnis zum Speichern des Beispiels.
Geben Sie den Befehl dotnet new console in einer Befehlszeile ein, um ein neues .NET Core-Projekt zu erstellen.
Kopieren Sie den Code aus dem Beispiel, und fügen Sie ihn in den Code-Editor ein.
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
unddotnet 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 restore
Dokumentation.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 vonA
ableitet, undC
leitet sich vonA
ab. Das private FeldA._value
ist in „A.B“ sichtbar. Wenn Sie jedoch die Kommentare aus derC.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 namensMethod1
, und KlasseB
erbt von KlasseA
. Das Beispiel ruft dannMethod1
auf, als wäre sie eine Instanzmethode vonB
.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 einSimpleClass
-Objekt in seine Zeichenfolgendarstellung konvertiert, gibt den vollqualifizierten Typnamen zurück. In diesem Fall gibt dieToString
-Methode die Zeichenfolge „SimpleClass“ zurück.Drei Methoden, die zwei Objekte auf Gleichheit testen: die öffentliche
Equals(Object)
-Instanzmethode, die öffentliche statischeEquals(Object, Object)
-Methode und die öffentliche statischeReferenceEquals(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 denSimpleClass
-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 diePublication
-Member Methodenimplementierungen bereitstellen, oder istPublication
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 spezialisiertenPublication
-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 vonPeriodical
sein, was wiederum eine Basisklasse vonMagazine
,Journal
undNewspaper
ist.Für Ihr Beispiel verwenden Sie die flache Hierarchie einer
Publication
-Klasse und einer einzelnen abgeleiteten KlasseBook
. Sie könnten das Beispiel mühelos erweitern, um eine Reihe von zusätzlichen Klassen zu erstellen, die vonPublication
abgeleitet sind, z. B.Magazine
undArticle
.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üsselwortabstract
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. Eineabstract
-Klasse ohneabstract
-Methoden zeigt an, dass diese Klasse ein abstraktes Konzept darstellt, das von mehreren konkreten Klassen geteilt wird (z. B.Book
undJournal
).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 keineabstract
-Methoden, ist allerdings selbstabstract
.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
-Klasseabstract
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 desPublication
-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 namenstotalPages
gespeichert. Er muss eine positive Zahl sein; andernfalls wird eine ArgumentOutOfRangeException ausgelöst.Herausgeberbezogene Elemente
Zwei schreibgeschützte Eigenschaften,
Publisher
undType
. Die Werte werden ursprünglich durch den Aufruf desPublication
-Klassenkonstruktors abgerufen.Veröffentlichungsbezogene Elemente
Zwei Methoden,
Publish
undGetPublicationDate
, legen das Veröffentlichungsdatum fest und geben es zurück. DiePublish
-Methode legt ein privatespublished
-Flag auftrue
fest, wenn sie aufgerufen wird, und weist dem privatendatePublished
-Feld das ihr übergebene Datum als Argument zu. DieGetPublicationDate
-Methode gibt die Zeichenfolge „NYP“ zurück, wenn daspublished
-Flagfalse
ist, und den Wert des FeldsdatePublished
, wenn estrue
ist.Copyrightbezogene Elemente
Die Methode
Copyright
übernimmt den Namen des Urheberrechtsinhabers und das Jahr des Copyrights als Argumente und weist sie den EigenschaftenCopyrightName
undCopyrightDate
zu.Eine Überschreibung der
ToString
-MethodeWenn 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 EigenschaftTitle
zurückzugeben.
Die folgende Abbildung veranschaulicht die Beziehung zwischen Ihrer Basisklasse Publication
und der implizit geerbten Klasse Object.
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 desPublication
-Konstruktors. Der dritte ist author, der in einer öffentlichen unveränderlichenAuthor
-Eigenschaft gespeichert ist. Ein Konstruktor enthält einen ISBN-Parameter, der in der Auto-EigenschaftISBN
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 desBook
-Objekts zurückgibt, eine eindeutige 10- oder 13-stellige Nummer. Die ISBN wird einem derBook
-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 beidenBook
-Konstruktoren übergeben und in der Eigenschaft gespeichert.Zwei schreibgeschützte preisbezogene Eigenschaften,
Price
undCurrency
. Ihre Werte werden in einem Aufruf derSetPrice
-Methode als Argumente bereitgestellt. Die EigenschaftCurrency
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 derBook
-Klasse festgelegt werden.Eine
SetPrice
-Methode, die die Werte der EigenschaftenPrice
undCurrency
festlegt. Diese Werte werden von diesen selben Eigenschaften zurückgegeben.Überschreibt die
ToString
-Methode (geerbt vonPublication
) 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 zweiBook
-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 zweiBook
-Objekten gleich sind, geben Sie den Hash zurück, der durch Aufrufen der GetHashCode-Methode der von der EigenschaftISBN
zurückgegebenen Zeichenfolge berechnet wurde.
Die folgende Abbildung veranschaulicht die Beziehung zwischen der Book
-Klasse und Publication
, ihrer Basisklasse.
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