Overname in C# en .NET
In deze zelfstudie maakt u kennis met overname 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.
Vereisten
- Visual Studio voor Windows wordt aangeraden. U kunt een gratis versie downloaden op de downloadpagina van Visual Studio. Visual Studio bevat de .NET SDK.
- U kunt ook de Visual Studio Code-editor gebruiken met de C# DevKit. U moet de nieuwste .NET SDK afzonderlijk installeren.
- Als u liever een andere editor hebt, moet u de nieuwste .NET SDK installeren.
De voorbeelden uitvoeren
Als u de voorbeelden in deze zelfstudie wilt maken en uitvoeren, gebruikt u het dotnet-hulpprogramma vanaf de opdrachtregel. 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 opdracht dotnet restore vanaf de opdrachtregel in om de afhankelijkheden van het project te laden of te herstellen.
U hoeft niet uit te voeren
dotnet restore
omdat deze impliciet wordt uitgevoerd door alle opdrachten waarvoor een herstelbewerking moet worden uitgevoerd, zoalsdotnet new
,dotnet build
, ,dotnet run
,dotnet test
, , endotnet publish
.dotnet pack
Als u impliciete herstel wilt uitschakelen, gebruikt u de--no-restore
optie.De
dotnet restore
opdracht is nog steeds nuttig in bepaalde scenario's waarbij het expliciet herstellen zinvol is, zoals builds voor continue integratie in Azure DevOps Services of in buildsystemen die expliciet moeten worden beheerd wanneer de herstelbewerking plaatsvindt.Zie de
dotnet restore
documentatie voor informatie over het beheren van NuGet-feeds.Voer de opdracht dotnet run in om het voorbeeld te compileren en uit te voeren.
Achtergrond: Wat is overname?
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 basisklasse genoemd. De klasse die de leden van de basisklasse over neemt, wordt de afgeleide klasse genoemd.
C# en .NET bieden alleen ondersteuning voor één 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 kan overnemen van het type D
C
, dat overkomt van het type B
, dat overkomt van het type basisklasse A
. Omdat overname transitief is, zijn de leden van het type A
beschikbaar om te typen D
.
Niet alle leden van een basisklasse worden overgenomen door afgeleide klassen. De volgende leden worden niet overgenomen:
Statische constructors, die de statische gegevens van een klasse initialiseren.
Instantieconstructors die u aanroept om een nieuw exemplaar van de klasse te maken. Elke klasse moet zijn eigen constructors definiëren.
Finalizers, die worden aangeroepen door de garbagecollector van de runtime om instanties van een klasse te vernietigen.
Alle andere leden van een basisklasse worden overgenomen door afgeleide klassen, ongeacht of ze zichtbaar zijn of niet, zijn afhankelijk 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 zijn genest in hun basisklasse. Anders zijn ze niet zichtbaar in afgeleide klassen. In het volgende voorbeeld
A.B
is een geneste klasse die is afgeleid vanA
enC
afgeleid is vanA
. Het privéveldA._value
is zichtbaar in A.B. Als u echter de opmerkingen uit deC.GetValue
methode 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: // 10
Beveiligde 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. Openbare overgenomen leden kunnen worden aangeroepen alsof ze zijn gedefinieerd in de afgeleide klasse. In het volgende voorbeeld definieert klasse
A
een methode met de naamMethod1
en klasseB
neemt deze over van klasseA
. In het voorbeeld wordt vervolgens aangeroepenMethod1
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 overgenomen leden ook overschrijven door een alternatieve implementatie te bieden. Als u een lid wilt kunnen overschrijven, moet het lid in de basisklasse worden gemarkeerd met het virtuele trefwoord. Standaard worden basisklasseleden niet gemarkeerd als 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 omdat het 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 implementatie van de basisklasse overschrijven. Basisklasseleden die zijn gemarkeerd met het abstracte trefwoord vereisen dat afgeleide klassen deze overschrijven. Als u probeert het volgende voorbeeld te compileren, wordt compilerfout CS0534 gegenereerd, '<klasse> implementeert geen overgenomen abstract lid'<>, omdat klasse B
geen implementatie voor A.Method1
biedt.
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, gemachtigden en opsommingen) bieden geen ondersteuning voor overname. 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
Dit 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 in uw SimpleClass
klas hebt gedefinieerd, geeft uitvoer uit het voorbeeld aan dat het daadwerkelijk negen leden heeft. Een van deze leden is een constructor zonder parameters (of standaardconstructor) 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
ToString
methode, waarmee eenSimpleClass
object wordt geconverteerd naar de tekenreeksweergave, retourneert de volledig gekwalificeerde typenaam. In dit geval retourneert deToString
methode de tekenreeks 'SimpleClass'.Drie methoden die testen op gelijkheid van twee objecten: de methode openbaar exemplaar
Equals(Object)
, 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
GetHashCode
methode, waarmee een waarde wordt berekend waarmee een exemplaar van het type kan worden gebruikt in gehashte verzamelingen.De openbare
GetType
methode, die een Type object retourneert dat hetSimpleClass
type vertegenwoordigt.De beveiligde Finalize methode, die is ontworpen om onbeheerde resources vrij te geven voordat het geheugen van een object wordt vrijgemaakt door de garbagecollector.
De beveiligde MemberwiseClone methode, waarmee een ondiepe kloon van het huidige object wordt gemaakt.
Vanwege impliciete overname kunt u elk overgenomen lid aanroepen van een SimpleClass
object alsof het daadwerkelijk een lid was dat in de SimpleClass
klasse is gedefinieerd. In het volgende voorbeeld wordt bijvoorbeeld de SimpleClass.ToString
methode aangeroepen, die SimpleClass
wordt overgenomen 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 maakt een andere set leden beschikbaar via overname voor impliciet afgeleide typen.
Typecategorie | Impliciet overgenomen van |
---|---|
class | Object |
Struct | ValueType, Object |
enum | Enum, , ValueTypeObject |
Gemachtigde | MulticastDelegate, , DelegateObject |
Overname 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 vertegenwoordigt bijvoorbeeld Publication
een publicatie van elk type en de Book
en Magazine
klassen vertegenwoordigen specifieke typen publicaties.
Notitie
Een klasse of struct kan een of meer interfaces implementeren. Hoewel de implementatie van de interface vaak wordt voorgesteld als tijdelijke oplossing voor één overname of als een manier om overname met structs te gebruiken, is het bedoeld om een andere relatie (een 'can do'-relatie) tussen een interface en het bijbehorende implementatietype dan overname uit te drukken. 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 Automobile
is een klasse met drie unieke alleen-lezen eigenschappen: Make
, de fabrikant van de auto; Model
, het soort auto; en Year
, het 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 produceren die het Automobile
exemplaar uniek identificeert 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 type te definiëren om auto's weer te geven Packard
die zijn vervaardigd door het Bedrijf van De Motor Auto van De Motor. In plaats daarvan kunt u ze 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
, Newspaper
en Article
.
De basispublicatieklasse
Bij het ontwerpen van uw Publication
klas moet u verschillende ontwerpbeslissingen nemen:
Welke leden moeten worden opgenomen in uw basisklasse
Publication
en of dePublication
leden methode-implementaties bieden of datPublication
dit een abstracte basisklasse is die fungeert als sjabloon voor de afgeleide klassen.In dit geval biedt de
Publication
klasse methode-implementaties. De sectie Abstracte basisklassen ontwerpen en de bijbehorende afgeleide klassen 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
Publication
toevoegen als hun code waarschijnlijk wordt gedeeld door sommige of meest gespecialiseerdePublication
typen. 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 om het hergebruik van code te maximaliseren als om een logische en intuïtieve overnamehiërarchie te maken, wilt u ervoor zorgen dat u alleen in de
Publication
klasse de gegevens en functionaliteit opneemt die gebruikelijk zijn voor alle of voor de meeste publicaties. 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? Kan bijvoorbeeld
Publication
een basisklasse zijn vanPeriodical
, die op zijn beurt een basisklasse is vanMagazine
,Journal
enNewspaper
.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
, zoalsMagazine
enArticle
.Of het zinvol is om de basisklasse te instantiëren. Als dit 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 hetabstract
trefwoord door 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 weerspiegeling, 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
Publication
klasse als abstract , zodat deze niet kan worden geïnstantieerd. Eenabstract
klasse zonder methodenabstract
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. Methoden die in de basisklasse zijn gedefinieerd, kunnen standaard niet worden overschreven.
De
Publication
klasse heeft geenabstract
methoden, 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 als basisklasse kan fungeren voor eventuele extra klassen. Poging om af te leiden van een verzegelde klasse gegenereerde compilerfout CS0509, 'kan niet worden afgeleid van verzegelde typenaam<>'.
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 Publication.PublicationType
eigenschap. Naast de leden waaruit deze wordt overgenomen Object, definieert de Publication
klasse de volgende unieke leden en leden onderdrukkingen:
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
Publication
klasse isabstract
, 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 worden aangeroepen vanuit afgeleide klasseconstructors, zoals de broncode voor de
Book
klasse laat zien.Twee publicatie-gerelateerde eigenschappen
Title
is een alleen-lezen String eigenschap waarvan de waarde wordt opgegeven door dePublication
constructor aan te roepen.Pages
is een eigenschap 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 een ArgumentOutOfRangeException getal wordt gegenereerd.Leden met betrekking tot uitgevers
Twee alleen-lezen eigenschappen en
Publisher
Type
. De waarden worden oorspronkelijk door de aanroep aan dePublication
klasseconstructor opgegeven.Publicatiegerelateerde leden
Twee methoden,
Publish
enGetPublicationDate
stel de publicatiedatum in en retourneer deze. MetPublish
de methode wordt een privévlagpublished
true
ingesteld op het moment dat deze wordt aangeroepen en wordt de datum toegewezen die eraan is doorgegeven als argument voor het privévelddatePublished
. DeGetPublicationDate
methode retourneert de tekenreeks 'NYP' als depublished
vlag isfalse
en de waarde van hetdatePublished
veld als dit het istrue
.Copyrightgerelateerde leden
De
Copyright
methode krijgt de naam van de auteursrechthouder en het jaar van het auteursrecht als argumenten en wijst deze toe aan deCopyrightName
enCopyrightDate
eigenschappen.Een onderdrukking van de
ToString
methodeAls een type de Object.ToString methode 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
Publication
klasse overschrijft Object.ToString om de waarde van deTitle
eigenschap te 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 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}";
}
Naast de leden waaruit deze wordt overgenomen Publication
, definieert de Book
klasse de volgende unieke leden en leden onderdrukkingen:
Twee constructors
De twee
Book
constructors delen drie gemeenschappelijke parameters. Twee, titel en uitgever, komen overeen met parameters van dePublication
constructor. De derde is auteur, die is opgeslagen in een openbare onveranderbareAuthor
eigenschap. Eén constructor bevat een isbn-parameter , die wordt opgeslagen in deISBN
automatische eigenschap.De eerste constructor gebruikt dit trefwoord om de andere constructor aan te roepen. Constructorkoppeling is een gemeenschappelijk 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 basiswoord 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 International Standard Book Number van hetBook
object retourneert, een uniek getal van 10 of 13 cijfers. Het ISBN wordt geleverd als een argument aan een van deBook
constructors. De ISBN wordt opgeslagen in een privé-backingveld, dat automatisch wordt gegenereerd door de compiler.Een alleen-lezen
Author
eigenschap. De naam van de auteur wordt opgegeven als argument voor zowelBook
constructors als wordt opgeslagen in de eigenschap.Twee alleen-lezen prijsgerelateerde eigenschappen en
Price
Currency
. De waarden worden opgegeven als argumenten in eenSetPrice
methode-aanroep. DeCurrency
eigenschap is het ISO-valutasymbool van drie cijfers (bijvoorbeeld USD voor de Amerikaanse dollar). ISO-valutasymbolen kunnen worden opgehaald uit de ISOCurrencySymbol eigenschap. Beide eigenschappen zijn extern alleen-lezen, maar beide kunnen worden ingesteld door code in deBook
klasse.Een
SetPrice
methode waarmee de waarden van dePrice
enCurrency
eigenschappen worden ingesteld. Deze waarden worden geretourneerd door dezelfde eigenschappen.Onderdrukkingen voor de
ToString
methode (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 gelijkheid van verwijzingen. Dat wil gezegd: twee objectvariabelen worden als gelijk beschouwd als ze naar hetzelfde object verwijzen. Aan de andere kant moeten in de
Book
klasse tweeBook
objecten gelijk zijn als ze hetzelfde ISBN hebben.Wanneer u de Object.Equals(Object) methode overschrijft, moet u ook de GetHashCode methode overschrijven, die een waarde retourneert 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 hebt overschreven Object.Equals(Object) om te retourneren
true
als de ISBN-eigenschappen van tweeBook
objecten gelijk zijn, retourneert u de hashcode die wordt berekend door de methode van de GetHashCode tekenreeks aan te roepen die door deISBN
eigenschap wordt geretourneerd.
In de volgende afbeelding ziet u de relatie tussen de Book
klasse en Publication
de basisklasse.
U kunt nu een instantie maken van een Book
object, zowel de unieke als overgenomen leden aanroepen en dit doorgeven als een argument aan een methode die een parameter van het type Publication
of van het type Book
verwacht, 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 methoden declareert. Deze klasse fungeert als een sjabloon waarmee de leden worden gedefinieerd 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 Shape
klasse 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 die Shape
twee eigenschappen definieert: Area
en Perimeter
. Naast het markeren van de klasse met het abstracte trefwoord, wordt elk exemplaarlid ook gemarkeerd met het abstracte trefwoord. In dit geval Shape
overschrijft u ook de Object.ToString methode 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 het gebied en de perimeter van een exemplaar van een afgeleide klasse eenvoudig kunnen ophalen. Wanneer u een exemplaar van een afgeleide klasse doorgeeft aan een van deze methoden, roept de runtime de methode onderdrukking 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 van Shape
die specifieke shapes vertegenwoordigen. In het volgende voorbeeld worden drie klassen, Square
, Rectangle
en Circle
. 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 matrix geïnstitueert van objecten die zijn afgeleid van Shape
en roept de statische methoden van de Shape
klasse aan, die retoureigenschapswaarden retourneert Shape
. De runtime haalt waarden op uit de overschreven eigenschappen van de afgeleide typen. Het voorbeeld cast ook elk Shape
object in de matrix naar het afgeleide type en, als de cast slaagt, worden eigenschappen van die specifieke subklasse van Shape
.
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