Sdílet prostřednictvím


Dědičnost v jazyce C# a technologii .NET

Tento kurz vás seznámí s dědičností v jazyce C#. Dědičnost je funkce objektově orientované programovací jazyky, která umožňuje definovat základní třídu, která poskytuje specifické funkce (data a chování) a definovat odvozené třídy, které dědí nebo přepisují tuto funkci.

Požadavky

  • Doporučujeme Visual Studio pro Windows. Bezplatnou verzi si můžete stáhnout ze stránky pro stažení sady Visual Studio. Visual Studio obsahuje sadu .NET SDK.
  • Editor editoru Visual Studio Code můžete použít také s jazykem C# DevKit. Budete muset nainstalovat nejnovější sadu .NET SDK samostatně.
  • Pokud dáváte přednost jinému editoru, musíte nainstalovat nejnovější sadu .NET SDK.

Spuštění příkladů

K vytvoření a spuštění příkladů v tomto kurzu použijete nástroj dotnet z příkazového řádku. Pro každý příklad postupujte takto:

  1. Vytvořte adresář pro uložení příkladu.

  2. Zadáním příkazu dotnet new console na příkazovém řádku vytvořte nový projekt .NET Core.

  3. Zkopírujte a vložte kód z příkladu do editoru kódu.

  4. Zadejte příkaz dotnet restore z příkazového řádku, který načte nebo obnoví závislosti projektu.

    Nemusíte spouštětdotnet restore, protože se spouští implicitně všemi příkazy, které vyžadují obnovení, například dotnet new, , dotnet build, dotnet run, dotnet testdotnet publisha dotnet pack. Pokud chcete zakázat implicitní obnovení, použijte tuto --no-restore možnost.

    Příkaz dotnet restore je stále užitečný v určitých scénářích, kdy explicitní obnovení dává smysl, například sestavení kontinuální integrace ve službě Azure DevOps Services nebo v systémech sestavení, které potřebují explicitně řídit, kdy dojde k obnovení.

    Informace o správě informačních kanálů NuGet najdete v dotnet restore dokumentaci.

  5. Zadejte příkaz dotnet run pro kompilaci a spuštění příkladu.

Pozadí: Co je dědičnost?

Dědičnost je jedním ze základních atributů objektově orientovaného programování. Umožňuje definovat podřízenou třídu, která znovu používá (dědí), rozšiřuje nebo upravuje chování nadřazené třídy. Třída, jejíž členy jsou zděděné, se nazývá základní třída. Třída, která dědí členy základní třídy, se nazývá odvozená třída.

Jazyk C# a .NET podporují pouze jedno dědičnost . To znamená, že třída může dědit pouze z jedné třídy. Dědičnost je však tranzitivní, což umožňuje definovat hierarchii dědičnosti pro sadu typů. Jinými slovy, typ D může dědit z typu C, který dědí z typu B, který dědí ze základní třídy typ A. Vzhledem k tomu, že dědičnost je tranzitivní, jsou členy typu A k dispozici pro typ D.

Ne všechny členy základní třídy jsou zděděné odvozenými třídami. Následující členy nejsou zděděné:

  • Statické konstruktory, které inicializují statická data třídy.

  • Konstruktory instance, které voláte k vytvoření nové instance třídy. Každá třída musí definovat vlastní konstruktory.

  • Finalizační metody, které volají uvolňování paměti modulu runtime ke zničení instancí třídy.

Zatímco všechny ostatní členy základní třídy jsou zděděné odvozenými třídami, ať už jsou viditelné nebo ne, závisí na jejich přístupnosti. Přístupnost člena má vliv na viditelnost odvozených tříd následujícím způsobem:

  • Soukromé členy jsou viditelné pouze v odvozených třídách, které jsou vnořené do jejich základní třídy. Jinak nejsou viditelné v odvozených třídách. V následujícím příkladu je vnořená třída, A.B která je odvozena od Aa C odvozena od A. Soukromé A._value pole je viditelné v A.B. Pokud však odeberete komentáře z C.GetValue metody a pokusíte se zkompilovat příklad, dojde k chybě kompilátoru CS0122: "A._value" je nedostupný z důvodu jeho úrovně ochrany.

    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
    
  • Chráněné členy jsou viditelné pouze v odvozených třídách.

  • Interní členy jsou viditelné pouze v odvozených třídách, které jsou umístěny ve stejném sestavení jako základní třída. Nejsou viditelné v odvozených třídách umístěných v jiném sestavení než základní třída.

  • Veřejné členy jsou viditelné v odvozených třídách a jsou součástí veřejného rozhraní odvozené třídy. Veřejné zděděné členy lze volat stejně jako v případě, že jsou definovány v odvozené třídě. V následujícím příkladu třída A definuje metodu s názvem Method1a třída B dědí z třídy A. Příklad pak volá, Method1 jako by se jednalo o metodu instance na 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();
        }
    }
    

Odvozené třídy mohou také přepsat zděděné členy poskytnutím alternativní implementace. Aby bylo možné přepsat člena, musí být člen základní třídy označen virtuálním klíčovým slovem. Ve výchozím nastavení nejsou členy základní třídy označené jako virtual a nelze je přepsat. Pokus o přepsání jiného než virtuálního člena, jak je znázorněno v následujícím příkladu, generuje chybu kompilátoru CS0506: Člen<> nemůže přepsat zděděný <člen>, protože není označen jako virtuální, abstraktní nebo přepsání.

public class A
{
    public void Method1()
    {
        // Do something.
    }
}

public class B : A
{
    public override void Method1() // Generates CS0506.
    {
        // Do something else.
    }
}

V některých případech musí odvozená třída přepsat implementaci základní třídy. Členy základní třídy označené abstraktním klíčovým slovem vyžadují, aby je odvozené třídy přepsaly. Pokus o kompilaci následujícího příkladu generuje chybu kompilátoru CS0534, "<třída neimplementuje zděděný abstraktní člen člen<>", protože třída B neposkytuje žádnou implementaci pro A.Method1> .

public abstract class A
{
    public abstract void Method1();
}

public class B : A // Generates CS0534.
{
    public void Method3()
    {
        // Do something.
    }
}

Dědičnost se vztahuje pouze na třídy a rozhraní. Jiné kategorie typů (struktury, delegáty a výčty) nepodporují dědičnost. Z těchto pravidel se při pokusu o kompilaci kódu jako v následujícím příkladu vytvoří chyba kompilátoru CS0527: Typ ValueType v seznamu rozhraní není rozhraní. Chybová zpráva indikuje, že i když můžete definovat rozhraní, která implementuje struktura, dědičnost není podporována.

public struct ValueStructure : ValueType // Generates CS0527.
{
}

Implicitní dědičnost

Kromě všech typů, které mohou dědit z prostřednictvím jediné dědičnosti, všechny typy v systému typů .NET implicitně dědí z Object nebo typ odvozený z něj. Běžné funkce Object jsou k dispozici pro libovolný typ.

Abychom viděli, co implicitní dědičnost znamená, pojďme definovat novou třídu, SimpleClasscož je jednoduše prázdná definice třídy:

public class SimpleClass
{ }

Pak můžete použít reflexi (která umožňuje zkontrolovat metadata typu a získat informace o daném typu) a získat seznam členů, kteří patří k danému SimpleClass typu. I když jste ve třídě SimpleClass nedefinoval žádné členy, výstup z příkladu znamená, že má ve skutečnosti devět členů. Jedním z těchto členů je konstruktor bez parametrů (nebo výchozí), který je automaticky zadaný pro SimpleClass typ kompilátorem jazyka C#. Zbývající osm jsou členy Object, typ, ze kterého všechny třídy a rozhraní v systému typů .NET nakonec implicitně dědí.

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

Implicitní dědičnost třídy Object zpřístupňuje tyto metody pro SimpleClass třídu:

  • Veřejná ToString metoda, která převede SimpleClass objekt na řetězcové vyjádření, vrátí plně kvalifikovaný název typu. V tomto případě ToString metoda vrátí řetězec "SimpleClass".

  • Tři metody, které testují rovnost dvou objektů: metoda veřejné instance Equals(Object) , veřejná statická Equals(Object, Object) metoda a veřejná statická ReferenceEquals(Object, Object) metoda. Ve výchozím nastavení tyto metody testují rovnost odkazů; to znamená, že aby byly stejné, musí dvě proměnné objektu odkazovat na stejný objekt.

  • Veřejná GetHashCode metoda, která vypočítá hodnotu, která umožňuje použití instance typu v kolekcích hash.

  • Veřejná GetType metoda, která vrací Type objekt, který představuje SimpleClass typ.

  • Chráněná Finalize metoda, která je navržená tak, aby uvolnila nespravované prostředky před uvolněním paměti objektu uvolňováním paměti.

  • Chráněná MemberwiseClone metoda, která vytvoří mělký klon aktuálního objektu.

Z důvodu implicitní dědičnosti můžete volat libovolný zděděný člen z objektu SimpleClass stejně, jako kdyby byl skutečně členem definovaným SimpleClass ve třídě. Například následující příklad volá metodu SimpleClass.ToString , která SimpleClass dědí z 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

Následující tabulka uvádí kategorie typů, které můžete vytvořit v jazyce C# a typy, ze kterých implicitně dědí. Každý základní typ zpřístupňuje jinou sadu členů prostřednictvím dědičnosti pro implicitně odvozené typy.

Kategorie typu Implicitně dědí z
class Object
struct ValueType, Object
enum Enum, , ValueTypeObject
delegát MulticastDelegate, , DelegateObject

Dědičnost a vztah "je a"

Dědičnost se obvykle používá k vyjádření vztahu "je" mezi základní třídou a jednou nebo více odvozenými třídami, kde odvozené třídy jsou specializované verze základní třídy; odvozená třída je typ základní třídy. Třída například Publication představuje publikaci libovolného druhu a Book třídy Magazine představují konkrétní typy publikací.

Poznámka:

Třída nebo struktura může implementovat jedno nebo více rozhraní. I když se implementace rozhraní často prezentuje jako alternativní řešení pro jednoduchou dědičnost nebo jako způsob použití dědičnosti s strukturami, je určen k vyjádření jiné relace (relace "může udělat") mezi rozhraním a jeho implementačním typem než dědičnost. Rozhraní definuje podmnožinu funkcí (například schopnost testovat rovnost, porovnávat nebo řadit objekty nebo podporovat parsování a formátování citlivé na jazykovou verzi), kterou rozhraní zpřístupňuje svým implementovacím typům.

Všimněte si, že výraz "is a" také vyjadřuje vztah mezi typem a konkrétní instancí tohoto typu. V následujícím příkladu je třída, Automobile která má tři jedinečné vlastnosti jen pro čtení: Make, výrobce automobilu; Model, druh automobilu; a Yearjeho rok výroby. Třída Automobile má také konstruktor, jehož argumenty jsou přiřazeny k hodnotám vlastnosti, a přepíše metodu Object.ToString tak, aby vytvořil řetězec, který jednoznačně identifikuje Automobile instanci místo Automobile třídy.

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}";
}

V takovém případě byste se neměli spoléhat na dědičnost, která představuje konkrétní modely a modely automobilů. Například nemusíte definovat Packard typ, který představuje automobily vyrobené společností Packard Motor Car Company. Místo toho je můžete reprezentovat vytvořením objektu Automobile s příslušnými hodnotami předanými do konstruktoru třídy, jak je znázorněno v následujícím příkladu.

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

Relace založená na dědičnosti je nejvhodnější použít na základní třídu a na odvozené třídy, které přidávají další členy do základní třídy nebo které vyžadují další funkce, které nejsou přítomné v základní třídě.

Návrh základní třídy a odvozených tříd

Pojďme se podívat na proces návrhu základní třídy a jejích odvozených tříd. V této části definujete základní třídu, Publicationkterá představuje publikaci jakéhokoli druhu, jako je kniha, časopis, noviny, deník, článek atd. Definujete také Book třídu, která je odvozena od Publication. Příklad můžete snadno rozšířit tak, aby definoval další odvozené třídy, například Magazine, Journal, Newspapera Article.

Základní třída publikace

Při návrhu předmětu Publication musíte učinit několik rozhodnutí o návrhu:

  • Jaké členy mají být zahrnuty do základní Publication třídy a zda Publication členové poskytují implementace metod nebo zda Publication je abstraktní základní třída, která slouží jako šablona pro jeho odvozené třídy.

    V tomto případě Publication třída poskytne implementace metod. Návrh abstraktní základní třídy a jejich odvozené třídy oddíl obsahuje příklad, který používá abstraktní základní třídu definovat metody, které odvozené třídy musí přepsat. Odvozené třídy jsou zdarma poskytnout jakoukoli implementaci, která je vhodná pro odvozený typ.

    Schopnost opakovaně používat kód (tj. více odvozených tříd sdílí deklaraci a implementaci metod základní třídy a nemusí je přepsat) je výhodou ne abstraktních základních tříd. Proto byste měli přidat členy, pokud Publication je jejich kód pravděpodobně sdílen některými nebo nejvíce specializovanými Publication typy. Pokud nebudete moct efektivně poskytovat implementace základní třídy, budete muset poskytovat do značné míry identické členské implementace v odvozených třídách spíše jedinou implementaci v základní třídě. Nutnost udržovat duplicitní kód ve více umístěních je potenciálním zdrojem chyb.

    Chcete-li maximalizovat opakované použití kódu a vytvořit logickou a intuitivní hierarchii dědičnosti, chcete mít jistotu, že do třídy zahrnete Publication pouze data a funkce, které jsou společné pro všechny publikace nebo pro většinu publikací. Odvozené třídy pak implementují členy, které jsou jedinečné pro konkrétní druhy publikace, které představují.

  • Jak daleko rozšířit hierarchii tříd Chcete vytvořit hierarchii tří nebo více tříd, nikoli pouze základní třídu a jednu nebo více odvozených tříd? Může to být například Publication základní třída Periodical, která je zase základní třídou Magazine, Journal a Newspaper.

    V příkladu použijete malou hierarchii Publication třídy a jednu odvozenou třídu Book. Můžete snadno rozšířit příklad vytvořit řadu dalších tříd, které jsou odvozeny od Publication, například Magazine a Article.

  • Zda dává smysl vytvořit instanci základní třídy. Pokud tomu tak není, měli byste pro třídu použít abstraktní klíčové slovo. V opačném případě může být vaše Publication třída vytvořena voláním jeho konstruktoru třídy. Pokud se pokusí vytvořit instanci třídy označené klíčovým slovem abstract přímým voláním konstruktoru třídy, kompilátor jazyka C# vygeneruje chybu CS0144, "Nelze vytvořit instanci abstraktní třídy nebo rozhraní". Pokud se pokusí vytvořit instanci třídy pomocí reflexe, metoda reflexe vyvolá MemberAccessExceptionvýjimku .

    Ve výchozím nastavení lze vytvořit instanci základní třídy voláním jeho konstruktoru třídy. Není nutné explicitně definovat konstruktor třídy. Pokud v zdrojovém kódu základní třídy neexistuje, kompilátor jazyka C# automaticky poskytuje výchozí konstruktor (bez parametrů).

    Například třídu označíte Publication jako abstraktní , aby ji nebylo možné vytvořit instanci. abstract Třída bez jakýchkoli abstract metod označuje, že tato třída představuje abstraktní koncept, který je sdílen mezi několika konkrétními třídami (například , JournalBook).

  • Zda odvozené třídy musí dědit implementaci základní třídy konkrétních členů, zda mají možnost přepsat implementaci základní třídy, nebo zda musí poskytnout implementaci. Abstraktní klíčové slovo slouží k vynucení odvozených tříd k poskytnutí implementace. Virtuální klíčové slovo slouží k povolení odvozených tříd přepsání metody základní třídy. Ve výchozím nastavení nejsou metody definované v základní třídě přepisovatelné.

    Třída Publication nemá žádné abstract metody, ale samotná třída je abstract.

  • Zda odvozená třída představuje konečnou třídu v hierarchii dědičnosti a nelze ji použít jako základní třídu pro další odvozené třídy. Ve výchozím nastavení může každá třída sloužit jako základní třída. Zapečetěné klíčové slovo můžete použít k označení, že třída nemůže sloužit jako základní třída pro žádné další třídy. Při pokusu o odvození z zapečetěné třídy vygenerované chyby kompilátoru CS0509 nelze odvodit z zapečetěného typu typeName<>.

    V příkladu označíte odvozenou třídu jako sealed.

Následující příklad ukazuje zdrojový kód pro Publication třídu, stejně jako PublicationType výčet, který je vrácen vlastností Publication.PublicationType . Kromě členů, které dědí z Object, Publication třída definuje následující jedinečné členy a přepsání členů:


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;
}
  • Konstruktor

    Publication Protože třída je abstract, nelze vytvořit instanci přímo z kódu, jako je následující příklad:

    var publication = new Publication("Tiddlywinks for Experts", "Fun and Games",
                                      PublicationType.Book);
    

    Jeho konstruktor instance lze však volat přímo z odvozených konstruktorů tříd, jak ukazuje zdrojový kód třídy Book .

  • Dvě vlastnosti související s publikací

    Title je vlastnost jen String pro čtení, jejíž hodnota je zadána voláním konstruktoru Publication .

    Pages je vlastnost pro čtení i zápis Int32 , která označuje, kolik stránek má publikace celkem. Hodnota je uložena v privátním poli s názvem totalPages. Musí to být kladné číslo nebo ArgumentOutOfRangeException je vyvolán.

  • Členové související s vydavateli

    Dvě vlastnosti Publisher jen pro čtení a Type. Hodnoty jsou původně zadány voláním konstruktoru Publication třídy.

  • Publikování souvisejících členů

    Dvě metody Publish a GetPublicationDatenastavení a vrácení data publikace. Metoda Publish nastaví privátní published příznak, na true který je volána a přiřadí mu datum předané jako argument soukromého datePublished pole. Metoda GetPublicationDate vrátí řetězec "NYP", pokud published je falsepříznak , a hodnotu datePublished pole, pokud je true.

  • Členové související s autorskými právy

    Metoda Copyright přebírá jméno držitele autorských práv a rok autorských práv jako argumenty a přiřazuje je k vlastnostem CopyrightName a CopyrightDate vlastnosti.

  • Přepsání ToString metody

    Pokud typ nepřepíše metodu Object.ToString , vrátí plně kvalifikovaný název typu, který se málo používá při odličení jedné instance od jiné. Třída Publication přepíše Object.ToString , aby vrátila hodnotu Title vlastnosti.

Následující obrázek znázorňuje vztah mezi základní Publication třídou a implicitně zděděnou Object třídou.

Třídy Object a Publication

Třída Book

Třída Book představuje knihu jako specializovaný typ publikace. Následující příklad ukazuje zdrojový kód pro Book třídu.

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}";
}

Kromě členů, které dědí z Publication, Book třída definuje následující jedinečné členy a přepsání členů:

  • Dva konstruktory

    Book Dva konstruktory sdílejí tři společné parametry. Dva, název a vydavatel, odpovídají parametrům konstruktoru Publication . Třetí je autor, který je uložen do veřejné neměnné Author vlastnosti. Jeden konstruktor obsahuje parametr isbn , který je uložen v ISBN automatické vlastnosti.

    První konstruktor používá toto klíčové slovo k volání druhého konstruktoru. Řetězení konstruktorů je běžný vzor při definování konstruktorů. Konstruktory s menším počtem parametrů poskytují výchozí hodnoty při volání konstruktoru s největším počtem parametrů.

    Druhý konstruktor používá základní klíčové slovo k předání názvu a názvu vydavatele konstruktoru základní třídy. Pokud ve zdrojovém kódu nevyvoláte explicitní volání konstruktoru základní třídy, kompilátor jazyka C# automaticky zadá volání výchozího konstruktoru základní třídy nebo konstruktoru bez parametrů.

  • Vlastnost určená jen pro ISBN čtení, která vrací Book číslo mezinárodního standardního knihy objektu, jedinečné 10- nebo 13místné číslo. ISBN se dodává jako argument některého z Book konstruktorů. IsBN je uložen v privátním záložním poli, které je automaticky generováno kompilátorem.

  • Vlastnost jen pro Author čtení. Jméno autora se zadává jako argument pro oba Book konstruktory a je uloženo ve vlastnosti.

  • Dvě vlastnosti Price související s cenou jen pro čtení a Currency. Jejich hodnoty se zadají jako argumenty ve SetPrice volání metody. Vlastnost Currency je tříciferný symbol měny ISO (například USD pro americký dolar). Symboly měny ISO lze z ISOCurrencySymbol vlastnosti načíst. Obě tyto vlastnosti jsou externě jen pro čtení, ale oba mohou být nastaveny kódem Book ve třídě.

  • Metoda SetPrice , která nastavuje hodnoty Price a Currency vlastnosti. Tyto hodnoty jsou vráceny stejnými vlastnostmi.

  • Přepíše metodu ToString (zděděnou z Publication) a Object.Equals(Object)GetHashCode metody (zděděné z Object).

    Pokud není přepsán, Object.Equals(Object) metoda testuje rovnost odkazů. To znamená, že dvě proměnné objektu jsou považovány za stejné, pokud odkazují na stejný objekt. Book Na druhé straně třídy by měly být dva Book objekty stejné, pokud mají stejný ISBN.

    Když přepíšete metodu Object.Equals(Object) , musíte také přepsat metodu GetHashCode , která vrací hodnotu, kterou modul runtime používá k ukládání položek v kolekcích hash pro efektivní načtení. Kód hash by měl vrátit hodnotu, která je konzistentní s testem rovnosti. Vzhledem k tomu, že jste přepsali Object.Equals(Object) , abyste vrátili true , pokud jsou vlastnosti ISBN dvou Book objektů stejné, vrátíte kód hash vypočítaný voláním GetHashCode metody řetězce vráceného ISBN vlastností.

Následující obrázek znázorňuje vztah mezi Book třídou a Publicationjejí základní třídou.

Třídy publikace a knihy

Nyní můžete vytvořit instanci objektu Book , vyvolat jeho jedinečné i zděděné členy a předat ho jako argument metodě, která očekává parametr typu Publication nebo typu Book, jak ukazuje následující příklad.

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

Návrh abstraktních základních tříd a jejich odvozených tříd

V předchozím příkladu jste definovali základní třídu, která poskytla implementaci pro řadu metod, které umožňují odvozené třídy sdílet kód. V mnoha případech se však neočekává, že základní třída poskytuje implementaci. Místo toho je základní třída abstraktní třídou , která deklaruje abstraktní metody; slouží jako šablona, která definuje členy, které musí každá odvozená třída implementovat. Obvykle v abstraktní základní třídě je implementace každého odvozeného typu jedinečná pro tento typ. Třídu jste označili abstraktním klíčovým slovem, protože nemělo smysl vytvořit instanci Publication objektu, i když třída poskytovala implementace funkcí, které jsou společné pro publikace.

Například každý uzavřený dvojrozměrný geometrický obrazec obsahuje dvě vlastnosti: oblast, vnitřní rozsah obrazce; a obvod nebo vzdálenost podél okrajů obrazce. Způsob, jakým se tyto vlastnosti počítají, ale zcela závisí na konkrétním obrazci. Vzorec pro výpočet obvodu kruhu (nebo obvodu) se například liší od kruhu čtverce. Třída Shape je abstract třída s metodami abstract . To znamená, že odvozené třídy sdílejí stejné funkce, ale tyto odvozené třídy implementují tuto funkci odlišně.

Následující příklad definuje abstraktní základní třídu s názvem Shape , která definuje dvě vlastnosti: Area a Perimeter. Kromě označení třídy abstraktním klíčovým slovem je každý člen instance označen také abstraktním klíčovým slovem. V tomto případě také přepíše metodu Object.ToString tak, Shape aby vrátila název typu, nikoli jeho plně kvalifikovaný název. Definuje dva statické členy a GetAreaGetPerimeter, které volajícím umožňují snadno načíst oblast a obvod instance jakékoli odvozené třídy. Když předáte instanci odvozené třídy některé z těchto metod, modul runtime volá přepsání metody odvozené třídy.

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;
}

Potom můžete odvodit Shape některé třídy, které představují konkrétní obrazce. Následující příklad definuje tři třídy, Square, Rectanglea Circle. Každý z nich používá k výpočtu oblasti a obvodu vzorec jedinečný pro daný obrazec. Některé odvozené třídy také definují vlastnosti, například Rectangle.Diagonal a Circle.Diameter, které jsou jedinečné pro obrazec, který představují.

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;
}

Následující příklad používá objekty odvozené z Shape. Vytvoří instanci pole objektů odvozených z Shape a volá statické metody Shape třídy, která zabalí návratové Shape hodnoty vlastností. Modul runtime načte hodnoty z přepisovaných vlastností odvozených typů. Příklad také přetypuje každý Shape objekt v poli na jeho odvozený typ a pokud přetypování proběhne úspěšně, načte vlastnosti této konkrétní podtřídy 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