Delen via


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

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:

  1. Maak een map om het voorbeeld op te slaan.

  2. Voer de opdracht dotnet new console in bij een opdrachtprompt om een nieuw .NET Core-project te maken.

  3. Kopieer en plak de code uit het voorbeeld in uw code-editor.

  4. 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, zoals dotnet new, dotnet build, , dotnet run, dotnet test, , en dotnet 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.

  5. 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 van Aen C afgeleid is van A. Het privéveld A._value is zichtbaar in A.B. Als u echter de opmerkingen uit de C.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 naam Method1en klasse B neemt deze over van klasse A. In het voorbeeld wordt vervolgens aangeroepen Method1 alsof het een instantiemethode was op B.

    public class A
    {
        public void Method1()
        {
            // Method implementation.
        }
    }
    
    public class B : A
    { }
    
    public class Example
    {
        public static void Main()
        {
            B b = new ();
            b.Method1();
        }
    }
    

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.Method1biedt.

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. SimpleClassDit 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 een SimpleClass object wordt geconverteerd naar de tekenreeksweergave, retourneert de volledig gekwalificeerde typenaam. In dit geval retourneert de ToString methode de tekenreeks 'SimpleClass'.

  • Drie methoden die testen op gelijkheid van twee objecten: de methode openbaar exemplaar Equals(Object) , de openbare statische Equals(Object, Object) methode en de openbare statische ReferenceEquals(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 het SimpleClass 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, Publicationdie 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 basispublicatieklasse

Bij het ontwerpen van uw Publication klas moet u verschillende ontwerpbeslissingen nemen:

  • Welke leden moeten worden opgenomen in uw basisklasse Publication en of de Publication leden methode-implementaties bieden of dat Publication 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 gespecialiseerde Publication 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 van Periodical, die op zijn beurt een basisklasse is van Magazine, Journal en Newspaper.

    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 van Publication, zoals Magazine en Article.

  • 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 het abstract 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. Een abstract klasse zonder methoden abstract geeft aan dat deze klasse een abstract concept vertegenwoordigt dat wordt gedeeld tussen verschillende concrete klassen (zoals een Book, 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 geen abstract methoden, maar de klasse zelf is abstract.

  • 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 is abstract, 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 de Publication 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 naam totalPages. 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 de Publication klasseconstructor opgegeven.

  • Publicatiegerelateerde leden

    Twee methoden, Publish en GetPublicationDatestel de publicatiedatum in en retourneer deze. Met Publish de methode wordt een privévlag published true ingesteld op het moment dat deze wordt aangeroepen en wordt de datum toegewezen die eraan is doorgegeven als argument voor het privéveld datePublished . De GetPublicationDate methode retourneert de tekenreeks 'NYP' als de published vlag is falseen de waarde van het datePublished veld als dit het is true.

  • Copyrightgerelateerde leden

    De Copyright methode krijgt de naam van de auteursrechthouder en het jaar van het auteursrecht als argumenten en wijst deze toe aan de CopyrightName en CopyrightDate eigenschappen.

  • Een onderdrukking van de ToString methode

    Als 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 de Title eigenschap te retourneren.

In de volgende afbeelding ziet u de relatie tussen uw basisklasse Publication en de impliciet overgenomen Object klasse.

De klassen Object en Publicatie

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 de Publication constructor. De derde is auteur, die is opgeslagen in een openbare onveranderbare Author eigenschap. Eén constructor bevat een isbn-parameter , die wordt opgeslagen in de ISBN 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 het Book object retourneert, een uniek getal van 10 of 13 cijfers. Het ISBN wordt geleverd als een argument aan een van de Book 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 zowel Book constructors als wordt opgeslagen in de eigenschap.

  • Twee alleen-lezen prijsgerelateerde eigenschappen en Price Currency. De waarden worden opgegeven als argumenten in een SetPrice methode-aanroep. De Currency 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 de Book klasse.

  • Een SetPrice methode waarmee de waarden van de Price en Currency eigenschappen worden ingesteld. Deze waarden worden geretourneerd door dezelfde eigenschappen.

  • Onderdrukkingen voor de ToString methode (overgenomen van Publication) 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 twee Book 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 twee Book objecten gelijk zijn, retourneert u de hashcode die wordt berekend door de methode van de GetHashCode tekenreeks aan te roepen die door de ISBN eigenschap wordt geretourneerd.

In de volgende afbeelding ziet u de relatie tussen de Book klasse en Publicationde basisklasse.

Publicatie- en boekklassen

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 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 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, Rectangleen 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