Notitie
Voor toegang tot deze pagina is autorisatie vereist. U kunt proberen u aan te melden of de directory te wijzigen.
Voor toegang tot deze pagina is autorisatie vereist. U kunt proberen de mappen te wijzigen.
In deze handleiding maakt u kennis met overerving in C#. Overname is een functie van objectgeoriënteerde programmeertalen waarmee u een basisklasse kunt definiëren die specifieke functionaliteit (gegevens en gedrag) biedt en afgeleide klassen definieert die deze functionaliteit overnemen of overschrijven.
Benodigdheden
- De nieuwste .NET SDK-
- Visual Studio Code-editor
- De C# DevKit
Installatie-instructies
Op Windows wordt dit WinGet-configuratiebestand gebruikt om alle vereisten te installeren. Als u al iets hebt geïnstalleerd, slaat WinGet die stap over.
- Download het bestand en dubbelklik erop om het uit te voeren.
- Lees de gebruiksrechtovereenkomst, typ yen selecteer Enter wanneer u wordt gevraagd om te accepteren.
- Als u een knipperende UAC-prompt (User Account Control) krijgt op de taakbalk, staat u de installatie toe om door te gaan.
Op andere platforms moet u elk van deze onderdelen afzonderlijk installeren.
- Download het aanbevolen installatieprogramma op de downloadpagina van de .NET SDK en dubbelklik erop om het uit te voeren. De downloadpagina detecteert uw platform en raadt het meest recente installatieprogramma voor uw platform aan.
- Download het meest recente installatieprogramma van de Visual Studio Code startpagina en dubbelklik erop om het uit te voeren. Deze pagina detecteert ook uw platform en de koppeling moet juist zijn voor uw systeem.
- Klik op de knop Installeren op de pagina C# DevKit extensie. Hiermee opent u Visual Studio-code en wordt gevraagd of u de extensie wilt installeren of inschakelen. Selecteer 'installeren'.
De voorbeelden uitvoeren
Als u de voorbeelden in deze tutorial wilt maken en uitvoeren, gebruikt u het hulpprogramma dotnet vanaf de command line. Volg deze stappen voor elk voorbeeld:
Maak een map om het voorbeeld op te slaan.
Voer de opdracht dotnet new console in bij een opdrachtprompt om een nieuw .NET Core-project te maken.
Kopieer en plak de code uit het voorbeeld in uw code-editor.
Voer de dotnet restore commando uit vanaf de terminal om de afhankelijkheden van het project te laden of te herstellen.
U hoeft
dotnet restoreniet uit te voeren omdat deze impliciet wordt uitgevoerd door alle opdrachten waarvoor een herstelbewerking is vereist, zoalsdotnet new,dotnet build,dotnet run,dotnet test,dotnet publishendotnet pack. Als u impliciet herstellen wilt uitschakelen, gebruikt u de optie--no-restore.De
dotnet restoreopdracht is nog steeds nuttig in bepaalde scenario's waarin expliciet herstellen zinvol is, zoals builds voor continue integratie in Azure DevOps Services of in buildsystemen die expliciet moeten worden beheerd wanneer het herstellen plaatsvindt.Zie de
dotnet restoredocumentatievoor meer informatie over het beheren van NuGet-feeds.Voer de dotnet-opdracht uit om het voorbeeld te compileren en uit te voeren.
Achtergrond: Wat is erfenis?
Overname is een van de fundamentele kenmerken van objectgeoriënteerd programmeren. Hiermee kunt u een onderliggende klasse definiëren die het gedrag van een bovenliggende klasse opnieuw gebruikt (overneemt), uitbreidt of wijzigt. De klasse waarvan de leden worden overgenomen, wordt de basisklassegenoemd. De klasse die de leden van de basisklasse over neemt, wordt de afgeleide klassegenoemd.
C# en .NET bieden alleen ondersteuning voor enkele overname. Dat wil gezegd, een klasse kan slechts overnemen van één klasse. Overname is echter transitief, waarmee u een overnamehiërarchie voor een set typen kunt definiëren. Met andere woorden, type D kan erven van type C, dat erft van type B, dat erft van de basis klasse type A. Omdat overname transitief is, zijn de leden van het type A beschikbaar om Dte typen.
Niet alle leden van een basisklasse worden overgenomen door afgeleide klassen. De volgende leden worden niet overgenomen:
statische constructors, waarmee de statische gegevens van een klasse worden geïnitialiseerd.
Instantieconstructors, die u aanroept om een nieuw exemplaar van de klasse te maken. Elke klasse moet zijn eigen constructoren definiëren.
Finalizers, die worden aangeroepen door de afvalverzamelaar van de runtime om instanties van een klasse te kunnen vernietigen.
Hoewel alle overige leden van een basisklasse door afgeleide klassen worden overgenomen, hangt het ervan af of ze zichtbaar zijn of niet, en dat hangt af van hun toegankelijkheid. De toegankelijkheid van een lid is als volgt van invloed op de zichtbaarheid van afgeleide klassen:
Privéleden zijn alleen zichtbaar in afgeleide klassen die binnen hun basisklasse genest zijn. Anders zijn ze niet zichtbaar in afgeleide klassen. In het volgende voorbeeld is
A.Been geneste klasse die is afgeleid vanAenCis afgeleid vanA. HetA._valueprivéveld is zichtbaar in A.B. Als u echter de opmerkingen uit deC.GetValuemethode verwijdert en probeert het voorbeeld te compileren, produceert het compilerfout CS0122: ''A._value' is niet toegankelijk vanwege het beveiligingsniveau.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: // 10Beveiligde-leden zijn alleen zichtbaar in afgeleide klassen.
Interne-leden zijn alleen zichtbaar in afgeleide klassen die zich in dezelfde assembly bevinden als de basisklasse. Ze zijn niet zichtbaar in afgeleide klassen die zich in een andere assembly bevinden dan de basisklasse.
Openbare leden zijn zichtbaar in afgeleide klassen en maken deel uit van de openbare interface van de afgeleide klasse. Geërfde openbare leden kunnen worden aangeroepen alsof ze in de afgeleide klasse zijn gedefinieerd. In het volgende voorbeeld definieert klasse
Aeen methode met de naamMethod1en neemt de klasseBover van klasseA. In het voorbeeld wordt vervolgensMethod1aangeroepen alsof het een instantiemethode was opB.public class A { public void Method1() { // Method implementation. } } public class B : A { } public class Example { public static void Main() { B b = new (); b.Method1(); } }
Afgeleide klassen kunnen ook geërfde leden overschrijven door een alternatieve implementatie te geven. Als u een lid wilt kunnen overschrijven, moet het lid in de basisklasse worden gemarkeerd met het trefwoord virtuele. Standaard zijn basisklasseleden niet gemarkeerd met virtual en kunnen ze niet worden overschreven. Als u probeert een niet-virtueel lid te overschrijven, genereert het volgende voorbeeld compilerfout CS0506: "<lid> kan overgenomen lid niet overschrijven <lid> omdat deze niet is gemarkeerd als virtueel, abstract of overschrijven."
public class A
{
public void Method1()
{
// Do something.
}
}
public class B : A
{
public override void Method1() // Generates CS0506.
{
// Do something else.
}
}
In sommige gevallen moet een afgeleide klasse-de basisklasse-implementatie overschrijven. Basisklasseleden die zijn gemarkeerd met het abstracte trefwoord vereisen dat afgeleide klassen deze overschrijven. Bij het compileren van het volgende voorbeeld wordt compilerfout CS0534 gegenereerd, "<klasse> implementeert geen overgenomen abstract lid <lid>", omdat klasse B geen implementatie biedt voor A.Method1.
public abstract class A
{
public abstract void Method1();
}
public class B : A // Generates CS0534.
{
public void Method3()
{
// Do something.
}
}
Overname is alleen van toepassing op klassen en interfaces. Andere typecategorieën (structs, delegates en enums) bieden geen ondersteuning voor overerving. Vanwege deze regels produceert het compileren van code zoals in het volgende voorbeeld compilerfout CS0527: 'Type 'ValueType' in interfacelijst is geen interface. Het foutbericht geeft aan dat, hoewel u de interfaces kunt definiëren die door een struct worden geïmplementeerd, overname niet wordt ondersteund.
public struct ValueStructure : ValueType // Generates CS0527.
{
}
Impliciete overname
Naast alle typen die ze kunnen overnemen via één overname, nemen alle typen in het .NET-typesysteem impliciet over van Object of een type dat ervan is afgeleid. De algemene functionaliteit van Object is beschikbaar voor elk type.
Als u wilt zien wat impliciete overname betekent, gaan we een nieuwe klasse definiëren, SimpleClass, dat is gewoon een lege klassedefinitie:
public class SimpleClass
{ }
Vervolgens kunt u reflectie gebruiken (waarmee u de metagegevens van een type kunt inspecteren om informatie over dat type op te halen) om een lijst op te halen met de leden die deel uitmaken van het SimpleClass type. Hoewel u geen leden hebt gedefinieerd in uw SimpleClass-klasse, geeft uitvoer uit het voorbeeld aan dat het daadwerkelijk negen leden heeft. Een van deze leden is een parameterloze (of standaard) constructor die automatisch wordt opgegeven voor het SimpleClass type door de C#-compiler. De overige acht zijn leden van Object, het type waaruit alle klassen en interfaces in het .NET-typesysteem uiteindelijk impliciet overnemen.
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
Impliciete overname van de Object-klasse maakt deze methoden beschikbaar voor de SimpleClass-klasse:
De openbare
ToStringmethode, waarmee eenSimpleClassobject wordt geconverteerd naar de tekenreeksweergave, retourneert de volledig gekwalificeerde typenaam. In dit geval retourneert de methodeToStringde tekenreeks 'SimpleClass'.Drie methoden die testen op gelijkheid van twee objecten: het openbare exemplaar
Equals(Object)methode, de openbare statischeEquals(Object, Object)methode en de openbare statischeReferenceEquals(Object, Object)methode. Deze methoden testen standaard op referentie gelijkheid; dat wil gezegd dat twee objectvariabelen naar hetzelfde object moeten verwijzen.De openbare
GetHashCodemethode, waarmee een waarde wordt berekend waarmee een exemplaar van het type kan worden gebruikt in gehashte verzamelingen.De openbare
GetTypemethode, die een Type-object retourneert dat hetSimpleClasstype vertegenwoordigt.De beveiligde Finalize-methode, die is ontworpen om onbeheerde bronnen vrij te geven voordat het geheugen van een object wordt vrijgemaakt door de vuilnisophaler.
De beveiligde MemberwiseClone methode, waarmee een ondiepe kloon van het huidige object wordt gemaakt.
Vanwege impliciete overname kunt u een overgenomen lid aanroepen van een SimpleClass-object, net zoals het daadwerkelijk een lid was dat is gedefinieerd in de SimpleClass-klasse. In het volgende voorbeeld wordt bijvoorbeeld de SimpleClass.ToString methode aangeroepen, die SimpleClass over neemt van Object.
public class EmptyClass
{ }
public class ClassNameExample
{
public static void Main()
{
EmptyClass sc = new();
Console.WriteLine(sc.ToString());
}
}
// The example displays the following output:
// EmptyClass
De volgende tabel bevat de categorieën typen die u in C# kunt maken en de typen waaruit ze impliciet overnemen. Elk basistype stelt een verschillende set van leden beschikbaar via overerving voor impliciet afgeleide typen.
| Typecategorie | Impliciet overgenomen van |
|---|---|
| klasse | Object |
| Struct | ValueType, Object |
| enumeratie | Enum, , ValueTypeObject |
| delegeren | MulticastDelegate, , DelegateObject |
Overerving en een 'is a'-relatie
Normaal gesproken wordt overname gebruikt om een 'is a'-relatie tussen een basisklasse en een of meer afgeleide klassen uit te drukken, waarbij de afgeleide klassen gespecialiseerde versies van de basisklasse zijn; de afgeleide klasse is een type van de basisklasse. De klasse Publication vertegenwoordigt bijvoorbeeld een publicatie van elk soort, en de Book- en Magazine klassen vertegenwoordigen specifieke typen publicaties.
Notitie
Een klasse of struct kan een of meer interfaces implementeren. Hoewel interface-implementatie vaak wordt voorgesteld als tijdelijke oplossing voor enkelvoudige overerving of als een manier om overerving met structs te gebruiken, is het bedoeld om een andere relatie (een 'can do'-relatie) tussen een interface en het bijbehorende implementatietype uit te drukken dan bij overerving. Een interface definieert een subset van functionaliteit (zoals de mogelijkheid om te testen op gelijkheid, om objecten te vergelijken of te sorteren, of om cultuurgevoelige parsering en opmaak te ondersteunen) die de interface beschikbaar maakt voor de implementatietypen.
Houd er rekening mee dat 'is a' ook de relatie uitdrukt tussen een type en een specifieke instantie van dat type. In het volgende voorbeeld is Automobile een klasse met drie unieke alleen-lezen eigenschappen: Make, de fabrikant van de auto; Model, het soort auto; en Year, zijn jaar van productie. Uw Automobile-klasse heeft ook een constructor waarvan de argumenten zijn toegewezen aan de eigenschapswaarden en overschrijft de Object.ToString methode om een tekenreeks te maken waarmee de Automobile instantie uniek wordt geïdentificeerd in plaats van de 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 dit geval moet u niet vertrouwen op overname om specifieke auto-merken en modellen te vertegenwoordigen. U hoeft bijvoorbeeld geen Packard type te definiëren om auto's te vertegenwoordigen die zijn vervaardigd door de Packard Motor Car Company. In plaats daarvan kunt u deze voorstellen door een Automobile object te maken met de juiste waarden die zijn doorgegeven aan de klasseconstructor, zoals in het volgende voorbeeld.
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
Een is-a-relatie op basis van overname wordt het beste toegepast op een basisklasse en op afgeleide klassen die extra leden toevoegen aan de basisklasse of waarvoor aanvullende functionaliteit is vereist die niet aanwezig is in de basisklasse.
De basisklasse en afgeleide klassen ontwerpen
Laten we eens kijken naar het proces van het ontwerpen van een basisklasse en de afgeleide klassen. In deze sectie definieert u een basisklasse, Publication, die een publicatie van elk soort vertegenwoordigt, zoals een boek, een tijdschrift, een krant, een dagboek, een artikel, enzovoort. U definieert ook een Book klasse die is afgeleid van Publication. U kunt het voorbeeld eenvoudig uitbreiden om andere afgeleide klassen te definiëren, zoals Magazine, Journal, Newspaperen Article.
De basis-klasse Publicatie
Bij het ontwerpen van uw Publication-klasse moet u verschillende ontwerpbeslissingen nemen:
Welke leden moeten worden opgenomen in uw basisklasse
Publicationen of dePublicationleden methode-implementaties bieden of datPublicationeen abstracte basisklasse is die fungeert als sjabloon voor de afgeleide klassen.In dit geval biedt de klasse
Publicationmethode-implementaties. De Abstracte basisklassen en hun afgeleide klassen ontwerpen sectie bevat een voorbeeld waarin een abstracte basisklasse wordt gebruikt om de methoden te definiëren die afgeleide klassen moeten overschrijven. Afgeleide klassen zijn gratis om elke implementatie te bieden die geschikt is voor het afgeleide type.De mogelijkheid om code opnieuw te gebruiken (dat wil gezegd: meerdere afgeleide klassen delen de declaratie en implementatie van basisklassemethoden en hoeven deze niet te overschrijven) is een voordeel van niet-abstracte basisklassen. Daarom moet u leden toevoegen aan
Publicationals hun code waarschijnlijk wordt gedeeld door sommige of meest gespecialiseerdePublicationtypen. Als u niet efficiënt basisklasse-implementaties biedt, moet u uiteindelijk grotendeels identieke lid-implementaties bieden in afgeleide klassen in plaats van één implementatie in de basisklasse. De noodzaak om dubbele code op meerdere locaties te onderhouden, is een mogelijke bron van bugs.Zowel voor het maximaliseren van het hergebruik van code als voor het maken van een logische en intuïtieve overnamehiërarchie wilt u ervoor zorgen dat u alleen de gegevens en functionaliteit van de
Publicationklasse opneemt die voor alle of voor de meeste publicaties gebruikelijk zijn. Afgeleide klassen implementeren vervolgens leden die uniek zijn voor de specifieke soorten publicatie die ze vertegenwoordigen.Hoe ver u uw klashiërarchie kunt uitbreiden. Wilt u een hiërarchie van drie of meer klassen ontwikkelen in plaats van een basisklasse en een of meer afgeleide klassen?
Publicationkan bijvoorbeeld een basisklasse vanPeriodicalzijn, die op zijn beurt een basisklasse vanMagazine,JournalenNewspaperis.Voor uw voorbeeld gebruikt u de kleine hiërarchie van een
Publication-klasse en één afgeleide klasse,Book. U kunt het voorbeeld eenvoudig uitbreiden om een aantal extra klassen te maken die zijn afgeleid vanPublication, zoalsMagazineenArticle.Of het zinvol is om de basisklasse te instantiëren. Als dat niet het geval is, moet u het abstracte trefwoord toepassen op de klasse. Anders kan uw
Publication-klasse worden geïnstantieerd door de klasseconstructor aan te roepen. Als er een poging wordt gedaan om een klasse te instantiëren die is gemarkeerd met het trefwoordabstractdoor een directe aanroep naar de klasseconstructor, genereert de C#-compiler fout CS0144, 'Kan geen exemplaar van de abstracte klasse of interface maken'. Als er een poging wordt gedaan om de klasse te instantiëren met behulp van reflectie, genereert de reflectiemethode een MemberAccessException.Standaard kan een basisklasse worden geïnstantieerd door de klasseconstructor aan te roepen. U hoeft geen klasseconstructor expliciet te definiëren. Als deze niet aanwezig is in de broncode van de basisklasse, biedt de C#-compiler automatisch een standaardconstructor (parameterloos).
In uw voorbeeld markeert u de
Publicationklasse als abstracte, zodat deze niet kan worden geïnstantieerd. Eenabstractklasse zonderabstractmethoden geeft aan dat deze klasse een abstract concept vertegenwoordigt dat wordt gedeeld tussen verschillende concrete klassen (zoals eenBook,Journal).Of afgeleide klassen de basisklasse-implementatie van bepaalde leden moeten overnemen, of ze de mogelijkheid hebben om de basisklasse-implementatie te overschrijven of of ze een implementatie moeten bieden. U gebruikt het abstracte trefwoord om afgeleide klassen af te dwingen om een implementatie te bieden. U gebruikt het virtuele trefwoord om afgeleide klassen toe te staan een basisklassemethode te overschrijven. Standaard zijn methoden die zijn gedefinieerd in de basisklasse niet overschrijfbaar.
De
Publicationklasse heeft geenabstractmethoden, maar de klasse zelf isabstract.Of een afgeleide klasse de uiteindelijke klasse in de overnamehiërarchie vertegenwoordigt en niet zelf kan worden gebruikt als basisklasse voor aanvullende afgeleide klassen. Standaard kan elke klasse fungeren als basisklasse. U kunt het verzegelde trefwoord toepassen om aan te geven dat een klasse niet kan fungeren als basisklasse voor eventuele extra klassen. Poging om van een verzegelde klasse af te leiden genereerde compilerfout CS0509, "kan niet worden afgeleid van verzegeld type <typeName>."
In uw voorbeeld markeert u uw afgeleide klasse als
sealed.
In het volgende voorbeeld ziet u de broncode voor de Publication-klasse, evenals een PublicationType opsomming die wordt geretourneerd door de eigenschap Publication.PublicationType. Naast de leden die worden overgenomen van Object, definieert de Publication klasse de volgende unieke leden en overschrijvingen van leden:
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;
}
Een constructor
Omdat de klasse
Publicationisabstract, kan deze niet rechtstreeks vanuit code worden geïnstantieerd, zoals in het volgende voorbeeld:var publication = new Publication("Tiddlywinks for Experts", "Fun and Games", PublicationType.Book);De instantieconstructor kan echter rechtstreeks vanuit afgeleide klasseconstructors worden aangeroepen, zoals de broncode voor de
Bookklasse laat zien.Twee publicatie-gerelateerde eigenschappen
Titleis een alleen-lezen String eigenschap waarvan de waarde wordt opgegeven bij het aanroepen van dePublicationconstructor.Pagesis een eigenschap voor lezen/schrijven Int32 die aangeeft hoeveel pagina's de publicatie in totaal heeft. De waarde wordt opgeslagen in een privéveld met de naamtotalPages. Het moet een positief getal zijn of er wordt een ArgumentOutOfRangeException gegenereerd.Uitgever-gerelateerde leden
Twee alleen-lezen eigenschappen,
PublisherenType. De waarden worden oorspronkelijk door de aanroep naar dePublicationklasseconstructor opgegeven.Publicatiegerelateerde leden
Twee methoden,
PublishenGetPublicationDate, stellen de publicatiedatum in en geven deze terug. Met de methodePublishwordt een privépublishedvlag ingesteld optruewanneer deze wordt aangeroepen en wordt de doorgegeven datum als argument aan het privé velddatePublishedtoegewezen. De methodeGetPublicationDateretourneert de tekenreeks 'NYP' als depublishedvlag isfalseen de waarde van hetdatePublishedveld als deze istrue.Copyrightgerelateerde leden
De methode
Copyrightkrijgt de naam van de auteursrechthouder en het jaar van het auteursrecht als argumenten en wijst deze toe aan deCopyrightNameenCopyrightDateeigenschappen.Een overschrijving van de methode
ToStringAls een type de methode Object.ToString niet overschrijft, wordt de volledig gekwalificeerde naam van het type geretourneerd, wat weinig wordt gebruikt bij het onderscheiden van het ene exemplaar van een ander exemplaar. De
Publicationklasse overschrijft Object.ToString om de waarde van de eigenschapTitlete retourneren.
In de volgende afbeelding ziet u de relatie tussen uw basisklasse Publication en de impliciet overgenomen Object klasse.
De Book klasse
De Book klasse vertegenwoordigt een boek als een gespecialiseerd type publicatie. In het volgende voorbeeld ziet u de broncode voor de klasse Book.
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}";
}
Naast de leden die worden overgenomen van Publication, definieert de Book klasse de volgende unieke leden en overschrijvingen van leden:
Twee constructors
De twee
Bookconstructors delen drie algemene parameters. Twee, titel en uitgever, komen overeen met parameters van dePublicationconstructor. De derde is auteur van, die wordt opgeslagen als een onveranderbare openbare eigenschapAuthor. Eén constructor bevat een isbn parameter, die is opgeslagen in deISBNauto-eigenschap.De eerste constructor gebruikt de dit trefwoord om de andere constructor aan te roepen. Constructor chaining is een gebruikelijk patroon bij het definiëren van constructors. Constructors met minder parameters bieden standaardwaarden bij het aanroepen van de constructor met het grootste aantal parameters.
De tweede constructor gebruikt het basis- trefwoord om de titel en de naam van de uitgever door te geven aan de constructor van de basisklasse. Als u geen expliciete aanroep maakt naar een basisklasseconstructor in uw broncode, levert de C#-compiler automatisch een aanroep naar de standaard- of parameterloze constructor van de basisklasse.
Een alleen-lezen
ISBN-eigenschap die het Internationale Standaard Boeknummer van hetBook-object retourneert, een uniek nummer van 10 of 13 cijfers. Het ISBN wordt geleverd als argument voor een van deBookconstructors. De ISBN wordt opgeslagen in een privé-backingveld, dat automatisch wordt gegenereerd door de compiler.Een alleen uitleesbare
Author-eigenschap. De naam van de auteur wordt als argument doorgegeven aan zowel deBook-constructors en wordt opgeslagen in de eigenschap.Twee alleen voor lezen prijsgerelateerde eigenschappen,
PriceenCurrency. Hun waarden worden opgegeven als argumenten in eenSetPricemethode-aanroep. De eigenschapCurrencyis het ISO-valutasymbool van drie cijfers (bijvoorbeeld USD voor de Amerikaanse dollar). ISO-valutasymbolen kunnen worden opgehaald uit de eigenschap ISOCurrencySymbol. Beide eigenschappen zijn van buitenaf alleen-lezen, maar beide kunnen worden ingesteld door code in deBook-klasse.Een
SetPricemethode waarmee de waarden van de eigenschappenPriceenCurrencyworden ingesteld. Die waarden worden door dezelfde eigenschappen teruggegeven.Overschrijft de methode
ToString(overgenomen vanPublication) en de Object.Equals(Object)- en GetHashCode-methoden (overgenomen van Object).Tenzij dit wordt overschreven, wordt de Object.Equals(Object) methode getest op referentie gelijkheid. Dat wil gezegd: twee objectvariabelen worden als gelijk beschouwd als ze naar hetzelfde object verwijzen. In de klasse
Bookdaarentegen moeten tweeBookobjecten gelijk zijn als ze hetzelfde ISBN hebben.Wanneer u de methode Object.Equals(Object) overschrijft, moet u ook de methode GetHashCode overschrijven. Deze methode retourneert een waarde die door de runtime wordt gebruikt voor het opslaan van items in gehashte verzamelingen voor efficiënt ophalen. De hash-code moet een waarde retourneren die consistent is met de test voor gelijkheid. Omdat u Object.Equals(Object) hebt overschreven zodat
truewordt geretourneerd als de ISBN-eigenschappen van tweeBook-objecten gelijk zijn, retourneert u de hashcode die berekend wordt door de GetHashCode-methode aan te roepen van de tekenreeks die wordt geretourneerd door deISBN-eigenschap.
In de volgende afbeelding ziet u de relatie tussen de Book klasse en Publication, de basisklasse.
U kunt nu een Book-object instantiëren, zowel de unieke als overgenomen leden aanroepen en deze doorgeven als een argument aan een methode die een parameter van het type Publication of van het type Bookverwacht, zoals in het volgende voorbeeld wordt weergegeven.
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
Abstracte basisklassen en hun afgeleide klassen ontwerpen
In het vorige voorbeeld hebt u een basisklasse gedefinieerd die een implementatie voor een aantal methoden biedt, zodat afgeleide klassen code kunnen delen. In veel gevallen wordt echter niet verwacht dat de basisklasse een implementatie levert. In plaats daarvan is de basisklasse een abstracte klasse die abstracte methodendeclareert; het fungeert als een sjabloon die de leden definieert die elke afgeleide klasse moet implementeren. Doorgaans in een abstracte basisklasse is de implementatie van elk afgeleid type uniek voor dat type. U hebt de klasse gemarkeerd met het abstracte trefwoord, omdat het niet zinvol was om een Publication-object te instantiëren, hoewel de klasse implementaties van functionaliteit biedt die gebruikelijk zijn voor publicaties.
Elke gesloten tweedimensionale geometrische vorm bevat bijvoorbeeld twee eigenschappen: gebied, de binnenste omvang van de vorm; en omtrek, of de afstand langs de randen van de shape. De manier waarop deze eigenschappen worden berekend, is echter volledig afhankelijk van de specifieke vorm. De formule voor het berekenen van de omtrek (of omtrek) van een cirkel is bijvoorbeeld anders dan die van een vierkant. De klasse Shape is een abstract klasse met abstract methoden. Dit geeft aan dat afgeleide klassen dezelfde functionaliteit delen, maar deze afgeleide klassen implementeren die functionaliteit anders.
In het volgende voorbeeld wordt een abstracte basisklasse gedefinieerd met de naam Shape waarmee twee eigenschappen worden gedefinieerd: Area en Perimeter. Naast het markeren van de klasse met het abstracte trefwoord, wordt elk objectlid gemarkeerd met het abstracte trefwoord. In dit geval overschrijft Shape ook de methode Object.ToString om de naam van het type te retourneren, in plaats van de volledig gekwalificeerde naam. En het definieert twee statische leden, GetArea en GetPerimeter, waarmee bellers eenvoudig het gebied en de perimeter van een exemplaar van een afgeleide klasse kunnen ophalen. Wanneer u een exemplaar van een afgeleide klasse doorgeeft aan een van deze methoden, roept de runtime de methode-overschrijving van de afgeleide klasse aan.
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;
}
Vervolgens kunt u enkele klassen afleiden uit Shape die specifieke vormen vertegenwoordigen. In het volgende voorbeeld worden drie klassen, Square, Rectangleen Circlegedefinieerd. Elke formule maakt gebruik van een formule die uniek is voor die specifieke shape om het gebied en de omtrek te berekenen. Sommige afgeleide klassen definiëren ook eigenschappen, zoals Rectangle.Diagonal en Circle.Diameter, die uniek zijn voor de vorm die ze vertegenwoordigen.
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;
}
In het volgende voorbeeld worden objecten gebruikt die zijn afgeleid van Shape. Er wordt een array geïnstantieerd van objecten die zijn afgeleid van Shape en de statische methoden van de Shape-klasse worden aangeroepen, die de retourwaarde-eigenschappen van Shape omvatten. De runtime haalt waarden op uit de overschreven eigenschappen van de afgeleide typen. Het voorbeeld cast ook elk Shape object in de array naar het afgeleide type en, als de cast slaagt, haalt het eigenschappen van die specifieke subklasse van Shape op.
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