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

Pokyny k instalaci

Ve Windows použijte tento konfigurační soubor WinGet k instalaci všech předpokladů. Pokud už máte něco nainstalovaného, WinGet tento krok přeskočí.

  1. Stáhněte soubor a poklikáním ho spusťte.
  2. Přečtěte si licenční smlouvu, zadejte ya po zobrazení výzvy k přijetí vyberte Enter.
  3. Pokud se na hlavním panelu zobrazí výzva řízení uživatelských účtů (UAC), povolte instalaci pokračovat.

Na jiných platformách je potřeba nainstalovat každou z těchto komponent samostatně.

  1. Stáhněte si doporučený instalační program ze stránky pro stažení .NET SDK a poklikáním ho spusťte. Stránka pro stažení zjistí vaši platformu a doporučí nejnovější instalační program pro vaši platformu.
  2. Stáhněte si nejnovější instalační program z domovské stránky editoru Visual Studio Code a dvojitým kliknutím ho spusťte. Tato stránka také zjistí vaši platformu a odkaz by měl být pro váš systém správný.
  3. Na stránce rozšíření C# DevKit klikněte na tlačítko Nainstalovat. Tím se otevře Visual Studio Code a zobrazí se dotaz, jestli chcete rozšíření nainstalovat nebo povolit. Vyberte "nainstalovat".

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. Zadejte příkaz dotnet new console na příkazovém řádku a 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, který zkompiluje a spustí příklad.

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 jednoduchou 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í z typu základní třídy A. Vzhledem k tomu, že dědičnost je tranzitivní, jsou členy typu A k dispozici pro typ D.

Ne všechny členové základní třídy jsou přejímány 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.

  • Finalizéry, které jsou volány garbage collectorem modulu runtime ke zničení instancí třídy.

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

  • Privátní členové jsou viditelné pouze v odvozených třídách, které jsou vnořené do své základní třídy. Jinak nejsou viditelné v odvozených třídách. V následujícím příkladu je A.B vnořená třída odvozená od Aa C je odvozena z A. Soukromé pole A._value 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ý kvůli úrovni 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í členové jsou viditelní pouze v odvozených třídách.

  • Vnitřní č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é členové jsou viditelní v odvozených třídách a tvoří součást veřejného rozhraní odvozené třídy. Veřejně zděděné členy lze používat, jako kdyby byly 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 a poskytnout alternativní implementaci. Aby bylo možné přepsat člena, musí být člen základní třídy označen klíčovým slovem virtual. Ve výchozím nastavení nejsou členy základní třídy označeny jako virtual a nelze je přepsat. Pokus o přepsání nevirtuá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řepsaný.“

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řídy> 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ě typů, které mohou dědit prostřednictvím jednoduché dědičnosti, všechny typy v systému typů .NET implicitně dědí z Object nebo z typu odvozeného od 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, SimpleClass, což 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ů, které patří do typu SimpleClass. I když jste v SimpleClass třídě 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 typ SimpleClass kompilátorem jazyka C#. Zbývajících osm jsou členy Object, typu, 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 z třídy Object zpřístupňuje třídě SimpleClass tyto metody:

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

  • Tři metody, které testují rovnost dvou objektů: veřejná instance Equals(Object) metoda, 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 typ SimpleClass.

  • Chráněná metoda Finalize, která je navržena k uvolnění nespravovaných prostředků předtím, než je paměť objektu znovu použita systémem garbage collector.

  • 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 v SimpleClass třídě. Například následující příklad volá metodu SimpleClass.ToString, kterou 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.

Typ kategorie Implicitně dědí z
třída Object
struktura ValueType, Object
výčet Enum, , ValueTypeObject
delegovat 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. Například třída Publication představuje publikaci libovolného druhu a třídy Book a Magazine představují konkrétní typy publikací.

Poznámka:

Třída nebo struktura může implementovat jedno nebo více rozhraní. Ačkoli se implementace rozhraní často prezentuje jako náhrada za jednoduchou dědičnost nebo jako způsob použití dědičnosti se strukturami, je určena k vyjádření jiného vztahu (vztahu "schopnosti") 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 Automobile třída, která má tři jedinečné vlastnosti jen pro čtení: Make, výrobce automobilu; Model, druh automobilu; a Year, jeho rok výroby. Vaše Automobile třída má také konstruktor, jehož argumenty jsou přiřazeny k hodnotám vlastností, a přepíše metodu Object.ToString, aby vytvořila řetězec, který jednoznačně identifikuje instanci Automobile namísto třídy Automobile.

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í značky a modely automobilů. Například nemusíte definovat typ Packard představující 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, Publication, která představuje publikaci jakéhokoli druhu, jako je kniha, časopis, noviny, deník, článek atd. Definujete také třídu Book, která je odvozena z Publication. Příklad můžete snadno rozšířit tak, aby definoval další odvozené třídy, jako jsou Magazine, Journal, Newspapera Article.

Základní třída Publication

Při navrhování třídy Publication je potřeba provést několik rozhodnutí:

  • Jaké členy mají být zahrnuty do základní Publication třídy a zda členové Publication 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ě bude třída Publication poskytovat implementace metod. Návrh abstraktních základních tříd a jejich odvozených tříd oddíl obsahuje příklad, který používá abstraktní základní třídu k definování metod, které odvozené třídy musí přepsat. Odvozené třídy mají volnost 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 do Publication, pokud jejich kód budou pravděpodobně sdílet některé nebo většina specializovaných typů Publication. 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 Publication zahrnete 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? Například Publication může být základní třídou Periodical, která je zase základní třídou Magazine, Journal a Newspaper.

    Například použijete malou hierarchii třídy Publication a jednu odvozenou třídu Book. Příklad můžete snadno rozšířit a vytvořit řadu dalších tříd odvozených z Publication, například Magazine a Article.

  • Zda dává smysl vytvořit instanci základní třídy. Pokud tomu tak není, měli byste u třídy použít abstraktní klíčové slovo . Jinak je možné vytvořit instanci třídy Publication voláním 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á MemberAccessException.

    Ve výchozím nastavení lze vytvořit instanci základní třídy zavoláním jejího konstruktoru. 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ů).

    V tomto příkladu označíte třídu Publication jako abstraktní , aby nešlo vytvořit její instanci. Třída abstract bez jakýchkoli metod abstract označuje, že tato třída představuje abstraktní koncept, který je sdílen mezi několika konkrétními třídami (například Book, Journal).

  • 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. Pomocí klíčového slova abstraktní přinutíte odvozené třídy poskytnout implementaci. Pomocí klíčového slova virtuální umožníte odvozeným třídám přepsat metodu základní třídy. Ve výchozím nastavení jsou metody definované v základní třídě nejsou 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. Můžete použít zapečetěné klíčové slovo indikovat, že třída nemůže sloužit jako základní třída pro žádné další třídy. Při pokusu o odvození ze zapečetěné třídy generuje kompilátor chybu CS0509, "nelze odvodit ze 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 třídy Publication a také výčtu PublicationType, který je vrácen vlastností Publication.PublicationType. Kromě členů, které dědí z Object, třída Publication definuje následující jedinečné členy a přetížení č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

    Protože třída Publication je typu abstract, nelze ji vytvořit přímo z kódu podle následujícího příkladu:

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

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

  • Dvě vlastnosti související s publikací

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

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

  • Členové související s vydavateli

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

  • Členové související s publikováním

    Dvě metody, Publish a GetPublicationDate, nastavují a vracejí datum publikace. Metoda Publish nastaví privátní příznak published na true při jeho zavolání a přiřadí mu datum předané jako argument privátnímu poli datePublished. Metoda GetPublicationDate vrátí řetězec "NYP", pokud je příznak publishedfalse, a vrátí hodnotu pole datePublished, 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řadí je CopyrightName a CopyrightDate vlastnosti.

  • Přepsání metody ToString

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

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 třídu Book.

using System;

public sealed class Book : Publication
{
    public Book(string title, string author, string publisher) :
           this(title, string.Empty, author, publisher)
    { }

    public Book(string title, string isbn, string author, string publisher) : base(title, publisher, PublicationType.Book)
    {
        // isbn argument must be a 10- or 13-character numeric string without "-" characters.
        // We could also determine whether the ISBN is valid by comparing its checksum digit
        // with a computed checksum.
        //
        if (!string.IsNullOrEmpty(isbn))
        {
            // Determine if ISBN length is correct.
            if (!(isbn.Length == 10 | isbn.Length == 13))
                throw new ArgumentException("The ISBN must be a 10- or 13-character numeric string.");
            if (!ulong.TryParse(isbn, out _))
                throw new ArgumentException("The ISBN can consist of numeric characters only.");
        }
        ISBN = isbn;

        Author = author;
    }

    public string ISBN { get; }

    public string Author { get; }

    public decimal Price { get; private set; }

    // A three-digit ISO currency symbol.
    public string? Currency { get; private set; }

    // Returns the old price, and sets a new price.
    public decimal SetPrice(decimal price, string currency)
    {
        if (price < 0)
            throw new ArgumentOutOfRangeException(nameof(price), "The price cannot be negative.");
        decimal oldValue = Price;
        Price = price;

        if (currency.Length != 3)
            throw new ArgumentException("The ISO currency symbol is a 3-character string.");
        Currency = currency;

        return oldValue;
    }

    public override bool Equals(object? obj)
    {
        if (obj is not Book book)
            return false;
        else
            return ISBN == book.ISBN;
    }

    public override int GetHashCode() => ISBN.GetHashCode();

    public override string ToString() => $"{(string.IsNullOrEmpty(Author) ? "" : Author + ", ")}{Title}";
}

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

  • Dva konstruktory

    Dva konstruktory Book 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žena do veřejné neměnné Author vlastnosti. Jeden konstruktor obsahuje parametr isbn, který je uložen v automatické vlastnosti ISBN.

    První konstruktor používá 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á klíčové slovo base k předání názvu a jména 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 pouze pro čtení ISBN, která vrací mezinárodní standardní číslo knihy objektu Book, jedinečné 10- nebo 13-místné číslo. ISBN je dodán jako argument pro jeden z Book konstruktorů. IsBN je uložen v privátním záložním poli, které je automaticky generováno kompilátorem.

  • Vlastnost Author pouze pro čtení. Jméno autora je zadáno jako argument do obou Book konstruktorů a je uložené ve vlastnosti.

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

  • Metoda SetPrice, která nastavuje hodnoty vlastností Price a Currency. Hodnoty jsou vráceny těmito stejnými vlastnostmi.

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

    Pokud není přepsána, metoda Object.Equals(Object) testuje rovnost odkazů. To znamená, že dvě proměnné objektu jsou považovány za stejné, pokud odkazují na stejný objekt. V Book třídě by na druhé straně 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 s hašováním pro efektivní načítání. 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), vrátit true, pokud jsou vlastnosti ISBN dvou Book objektů stejné, vrátíte hashovací kód vypočítaný pomocí volání metody GetHashCode řetězcem vráceným vlastností ISBN.

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

Publikace a knižní třídy

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řídy, 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 objektu Publication, i když třída poskytovala implementace funkcí běžných 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 se například liší od vzorce pro čtverec. Třída Shape je třída abstract 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 pomocí abstraktního klíčového slova je každý člen instance označen také abstraktním klíčovým slovem. V tomto případě Shape také přepíše metodu Object.ToString tak, aby vrátila název typu, nikoli jeho plně kvalifikovaný název. Definuje dva statické členy, GetArea a GetPerimeter, 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 některé třídy z Shape, 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 třídy Shape, která zabalí návratové hodnoty vlastnosti Shape. Modul runtime načte hodnoty z překrytý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í uspěje, 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