Udostępnij za pośrednictwem


Dziedziczenie w języku C# i .NET

W tym samouczku przedstawiono dziedziczenie w języku C#. Dziedziczenie to funkcja języków programowania zorientowanych obiektowo, które umożliwiają zdefiniowanie klasy bazowej, która zapewnia określone funkcje (dane i zachowanie) oraz definiowanie klas pochodnych, które dziedziczą lub zastępują tę funkcję.

Wymagania wstępne

  • Najnowszy .NET SDK
  • Edytor programu Visual Studio Code
  • Zestaw deweloperski C#

Instrukcje instalacji

W systemie Windows użyj tego pliku konfiguracyjnego WinGet , aby zainstalować wszystkie wymagane komponenty wstępne. Jeśli masz już coś zainstalowanego, usługa WinGet pominie ten krok.

  1. Pobierz plik i kliknij dwukrotnie, aby go uruchomić.
  2. Przeczytaj umowę licencyjną, wpisz yi wybierz pozycję Wprowadź po wyświetleniu monitu o zaakceptowanie.
  3. Jeśli na pasku zadań zostanie wyświetlony monit kontroli konta użytkownika (UAC), zezwól na kontynuowanie instalacji.

Na innych platformach należy zainstalować każdy z tych składników oddzielnie.

  1. Pobierz zalecany instalator ze strony pobierania zestawu SDK platformy .NET i kliknij dwukrotnie, aby go uruchomić. Strona pobierania wykrywa platformę i zaleca najnowszy instalator twojej platformy.
  2. Pobierz najnowszy instalator z strony głównej programu Visual Studio Code i kliknij dwukrotnie, aby go uruchomić. Ta strona wykrywa również platformę, a link powinien być poprawny dla twojego systemu.
  3. Kliknij przycisk "Zainstaluj" na stronie rozszerzenia C# DevKit. Spowoduje to otwarcie programu Visual Studio Code i pytanie, czy chcesz zainstalować lub włączyć rozszerzenie. Wybierz pozycję "Zainstaluj".

Uruchamianie przykładów

Aby utworzyć i uruchomić przykłady w tym samouczku, użyj narzędzia dotnet z wiersza polecenia. Wykonaj następujące kroki dla każdego przykładu:

  1. Utwórz katalog do przechowywania przykładu.

  2. Wprowadź polecenie dotnet new console w wierszu polecenia, aby utworzyć nowy projekt platformy .NET Core.

  3. Skopiuj i wklej kod z przykładu do edytora kodu.

  4. Wprowadź polecenie dotnet restore z wiersza polecenia, aby załadować lub przywrócić zależności projektu.

    Nie musisz uruchamiać dotnet restore, ponieważ jest ona uruchamiana niejawnie przez wszystkie polecenia, które wymagają przywrócenia, takie jak dotnet new, dotnet build, dotnet run, dotnet test, dotnet publishi dotnet pack. Aby wyłączyć niejawne przywracanie, użyj opcji --no-restore.

    Polecenie dotnet restore jest nadal przydatne w niektórych scenariuszach, w których jawne przywracanie pakietów ma sens, takie jak w budowaniu ciągłej integracji w środowisku Azure DevOps Services lub w systemach kompilacji, które muszą jawnie kontrolować, kiedy następuje przywracanie pakietów.

    Aby uzyskać informacje na temat zarządzania kanałami informacyjnymi NuGet, zobacz dokumentację dotnet restore.

  5. Wprowadź polecenie dotnet run, aby skompilować i wykonać przykład.

Tło: Co to jest dziedziczenie?

dziedziczenie jest jednym z podstawowych atrybutów programowania obiektowego. Umożliwia zdefiniowanie klasy podrzędnej, która dziedziczy, rozszerza lub modyfikuje zachowanie klasy nadrzędnej. Klasa, której składowe są dziedziczone, jest nazywana klasą bazową. Klasa, która dziedziczy składowe klasy bazowej, jest nazywana klasą pochodną.

Język C# i platforma .NET obsługują tylko pojedyncze dziedziczenie. Oznacza to, że klasa może dziedziczyć tylko z jednej klasy. Jednak dziedziczenie jest przechodnie, co umożliwia zdefiniowanie hierarchii dziedziczenia dla zestawu typów. Innymi słowy, typ D może dziedziczyć z typu C, który dziedziczy z typu B, który dziedziczy z typu klasy bazowej A. Ponieważ dziedziczenie jest przechodnie, elementy członkowskie typu A są dostępne dla typu D.

Nie wszystkie elementy członkowskie klasy bazowej są dziedziczone przez klasy pochodne. Następujący członkowie nie są dziedziczeni:

  • Konstruktory statyczne, które inicjują dane statyczne klasy.

  • Konstruktory instancji, które są wywoływane w celu utworzenia nowej instancji klasy. Każda klasa musi definiować własne konstruktory.

  • Finalizatory, które są wywoływane przez mechanizm garbage collection środowiska uruchomieniowego w celu zniszczenia instancji klasy.

Chociaż wszystkie pozostałe elementy członkowskie klasy bazowej są dziedziczone przez klasy pochodne, ich widoczność zależy od poziomu dostępu. Dostępność elementu członkowskiego wpływa na widoczność dla klas pochodnych w następujący sposób:

  • Prywatne składowe są widoczne tylko w klasach pochodnych, które są zagnieżdżone w swojej klasie bazowej. W przeciwnym razie nie są one widoczne w klasach pochodnych. W poniższym przykładzie A.B to zagnieżdżona klasa, która pochodzi z A, a C pochodzi z A. Prywatne pole A._value jest widoczne w A.B. Jeśli jednak usuniesz komentarze z metody C.GetValue i spróbujesz skompilować przykład, spowoduje to wygenerowanie błędu kompilatora CS0122: "A._value" jest niedostępny ze względu na poziom ochrony.

    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
    
  • Składowe chronione są widoczne tylko w klasach pochodnych.

  • wewnętrzne składowe są widoczne tylko w klasach pochodnych, które znajdują się w tym samym zestawie co klasa bazowa. Nie są one widoczne w klasach pochodnych znajdujących się w innym zestawie niż klasa bazowa.

  • Publiczni członkowie są widoczni w klasach pochodnych i są częścią interfejsu publicznego tych klas. Członkowie dziedziczeni publicznie mogą być wywoływani tak, jakby były zdefiniowane w klasie pochodnej. W poniższym przykładzie klasa A definiuje metodę o nazwie Method1, a klasa B dziedziczy z klasy A. W tym przykładzie wywołamy Method1 tak, jakby była to metoda wystąpienia w 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();
        }
    }
    

Klasy pochodne mogą również przesłonić dziedziczone elementy, zapewniając alternatywną implementację. Aby można było zastąpić składową, składowa w klasie bazowej musi być oznaczona jako wirtualne słowo kluczowe. Domyślnie składowe klasy bazowej nie są oznaczone jako virtual i nie mogą być zastępowane. Próba przesłonięcia niewirtualnego członka, jak w poniższym przykładzie, powoduje wygenerowanie błędu kompilatora CS0506: "<członek> nie może przesłonić dziedziczonego członka <>, ponieważ nie jest oznaczony jako wirtualny, abstrakcyjny lub przesłonięty."

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

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

W niektórych przypadkach klasa pochodna musi zastąpić implementację klasy bazowej. Składowe klasy bazowej oznaczone słowem kluczowym abstrakcyjnym wymagają, aby klasy pochodne je zastępowały. Próba skompilowania poniższego przykładu generuje błąd kompilatora CS0534, "<klasa> nie implementuje dziedziczonej abstrakcyjnej składowej <składowe>", ponieważ klasa B nie zapewnia implementacji dla A.Method1.

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

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

Dziedziczenie dotyczy tylko klas i interfejsów. Inne kategorie typów (struktury, delegacje i wyliczenia) nie obsługują dziedziczenia. Ze względu na te reguły próba skompilowania kodu, takiego jak w poniższym przykładzie, powoduje wygenerowanie błędu kompilatora CS0527: "Typ "ValueType" na liście interfejsów nie jest interfejsem". Komunikat o błędzie wskazuje, że chociaż można zdefiniować interfejsy implementujące strukturę, dziedziczenie nie jest obsługiwane.

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

Niejawne dziedziczenie

Z wyjątkiem typów, które mogą dziedziczyć przez pojedyncze dziedziczenie, wszystkie typy w systemie typów platformy .NET niejawnie dziedziczą z Object lub typu pochodnego. Typowe funkcje Object są dostępne dla dowolnego typu.

Aby zobaczyć, co oznacza niejawne dziedziczenie, zdefiniujmy nową klasę, SimpleClass, która jest po prostu pustą definicją klasy:

public class SimpleClass
{ }

Następnie można użyć refleksji (co umożliwia sprawdzenie metadanych typu w celu uzyskania informacji o tym typie), aby uzyskać listę członków należących do typu SimpleClass. Mimo że nie zdefiniowano żadnych elementów członkowskich w klasie SimpleClass, dane wyjściowe z przykładu wskazują, że rzeczywiście ma dziewięć elementów członkowskich. Jednym z tych elementów członkowskich jest konstruktor bez parametrów (lub domyślny), który jest automatycznie dostarczany przez kompilator języka C# dla typu SimpleClass. Pozostałych osiem to członkowie Object, typ, z którego ostatecznie niejawnie dziedziczy się wszystkie klasy i interfejsy w systemie typów platformy .NET.

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

Niejawne dziedziczenie z klasy Object udostępnia te metody dla klasy SimpleClass:

  • Publiczna metoda ToString, która konwertuje obiekt SimpleClass na jego reprezentację ciągu, zwraca w pełni kwalifikowaną nazwę typu. W tym przypadku metoda ToString zwraca ciąg "SimpleClass".

  • Trzy metody, które testują równość dwóch obiektów: metoda instancji publicznej Equals(Object), metoda statyczna publiczna Equals(Object, Object) i metoda statyczna publiczna ReferenceEquals(Object, Object). Domyślnie te metody testują równość referencyjną. Oznacza to, że aby być równe, dwie zmienne obiektu muszą odwoływać się do tego samego obiektu.

  • Metoda publiczna GetHashCode, która oblicza wartość pozwalającą na użycie wystąpienia typu w kolekcjach haszowanych.

  • Publiczna metoda GetType, która zwraca obiekt Type reprezentujący typ SimpleClass.

  • Chroniona metoda Finalize, która jest przeznaczona do zwalniania niezarządzanych zasobów, zanim pamięć obiektu zostanie odzyskana przez mechanizm odśmiecania pamięci.

  • Chroniona metoda MemberwiseClone, która tworzy płytki klon bieżącego obiektu.

Ze względu na niejawne dziedziczenie można wywołać dowolny dziedziczony element członkowski z obiektu SimpleClass tak, jakby był on rzeczywiście elementem członkowskim zdefiniowanym w klasie SimpleClass. Na przykład, podany przykład wywołuje metodę SimpleClass.ToString, którą SimpleClass dziedziczy 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

W poniższej tabeli wymieniono kategorie typów, które można utworzyć w języku C# i typy, z których niejawnie dziedziczą. Każdy typ podstawowy udostępnia inny zestaw elementów członkowskich za pośrednictwem dziedziczenia dla typów niejawnie pochodnych.

Kategoria typów Niejawnie dziedziczy z
klasa Object
struktura ValueType, Object
typ wyliczeniowy Enum, , ValueTypeObject
delegować MulticastDelegate, , DelegateObject

Dziedziczenie i relacja "jest"

Zwykle dziedziczenie służy do wyrażania relacji "jest" między klasą bazową a co najmniej jedną klasą pochodną, gdzie klasy pochodne są wyspecjalizowanymi wersjami klasy bazowej; klasa pochodna jest typem klasy bazowej. Na przykład klasa Publication reprezentuje publikację dowolnego rodzaju, a klasy Book i Magazine reprezentują określone typy publikacji.

Uwaga

Klasa lub struktura może implementować co najmniej jeden interfejs. Chociaż implementacja interfejsu jest często przedstawiana jako obejście pojedynczego dziedziczenia lub sposób używania dziedziczenia ze strukturami, ma na celu wyrażenie innego rodzaju relacji ('może zrobić') między interfejsem a typem, który go implementuje, niż w przypadku dziedziczenia. Interfejs definiuje podzbiór funkcji (takich jak możliwość testowania równości, porównywania lub sortowania obiektów albo obsługi analizowania i formatowania wrażliwego na kulturę), który interfejs udostępnia swoim typom implementowania.

Należy pamiętać, że wyrażenie "is a" również wyraża relację między typem a konkretnym wystąpieniem tego typu. W poniższym przykładzie Automobile jest klasą, która ma trzy unikatowe właściwości tylko do odczytu: Make, producent samochodów; Model, rodzaj samochodu; i Year, jego rok produkcji. Klasa Automobile ma również konstruktor, którego argumenty są przypisywane do wartości właściwości, i zastępuje metodę Object.ToString w celu utworzenia ciągu, który jednoznacznie identyfikuje wystąpienie Automobile, a nie klasę 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}";
}

W takim przypadku nie należy polegać na dziedziczeniu w celu reprezentowania określonych modeli i samochodów. Na przykład nie trzeba definiować typu Packard do reprezentowania samochodów produkowanych przez Packard Motor Car Company. Zamiast tego można je przedstawić, tworząc obiekt Automobile z odpowiednimi wartościami przekazanymi do konstruktora klasy, jak w poniższym przykładzie.

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

Relacja "jest-a" oparta na dziedziczeniu najlepiej zastosowana jest do klasy bazowej oraz klas pochodnych, które dodają dodatkowych członków do klasy bazowej lub wymagają dodatkowej funkcjonalności, nieobecnej w klasie bazowej.

Projektowanie klas bazowych i klas pochodnych

Przyjrzyjmy się procesowi projektowania klasy bazowej i jej klas pochodnych. W tej sekcji zdefiniujesz klasę bazową, Publication, która reprezentuje publikację dowolnego rodzaju, taką jak książka, magazyn, gazeta, dziennik, artykuł itp. Zdefiniujesz również klasę Book, która pochodzi z Publication. Można łatwo rozszerzyć przykład, aby zdefiniować inne klasy pochodne, takie jak Magazine, Journal, Newspaperi Article.

Klasa bazowa publikacji

Podczas projektowania klasy Publication należy podjąć kilka decyzji projektowych:

  • Składowe, które należy uwzględnić w klasie podstawowej Publication i czy składowe Publication zapewniają implementacje metod, czy też Publication jest abstrakcyjną klasą bazową, która służy jako szablon dla jej klas pochodnych.

    W takim przypadku klasa Publication zapewni implementacje metod. Sekcja Projektowanie abstrakcyjnych klas bazowych i ich klas pochodnych zawiera przykład, który używa abstrakcyjnej klasy bazowej do definiowania metod, które klasy pochodne muszą zastąpić. Klasy pochodne mogą swobodnie dostarczać dowolną implementację, która jest odpowiednia dla ich typu.

    Możliwość ponownego użycia kodu (czyli wielu klas pochodnych współużytkuje deklarację i implementację metod klasy bazowej i nie trzeba ich zastępować) jest zaletą nie abstrakcyjnych klas bazowych. W związku z tym należy dodać członków do Publication, jeśli ich kod prawdopodobnie zostanie udostępniony przez niektóre lub większość wyspecjalizowanych typów Publication. Jeśli nie uda ci się wydajnie zapewnić implementacji klas bazowych, musisz zapewnić w dużej mierze identyczne implementacje składowe w klasach pochodnych, a nie pojedynczą implementację w klasie bazowej. Potrzeba utrzymania zduplikowanego kodu w wielu lokalizacjach jest potencjalnym źródłem usterek.

    Zarówno w celu zmaksymalizowania ponownego użycia kodu, jak i utworzenia logicznej i intuicyjnej hierarchii dziedziczenia, chcesz mieć pewność, że uwzględnisz w klasie Publication tylko dane i funkcje wspólne dla wszystkich lub większości publikacji. Klasy pochodne implementują następnie elementy członkowskie unikatowe dla rodzajów publikacji, które reprezentują.

  • Jak daleko rozszerzyć hierarchię klas. Czy chcesz opracować hierarchię trzech lub więcej klas, a nie po prostu klasę bazową i co najmniej jedną klasę pochodną? Na przykład Publication może być klasą bazową Periodical, która z kolei jest klasą bazową Magazine, Journal i Newspaper.

    Na przykład użyjesz małej hierarchii klasy Publication i pojedynczej klasy pochodnej, Book. Można łatwo rozszerzyć przykład, aby utworzyć wiele dodatkowych klas, które pochodzą z Publication, takich jak Magazine i Article.

  • Czy sensowne jest utworzenie wystąpienia klasy bazowej. Jeśli nie, należy zastosować słowo kluczowe abstrakcyjne do klasy. W przeciwnym razie klasę Publication można utworzyć, wywołując konstruktor klasy. Jeśli podjęto próbę utworzenia wystąpienia klasy oznaczonej słowem kluczowym abstract przez bezpośrednie wywołanie konstruktora klasy, kompilator języka C# generuje błąd CS0144" "Nie można utworzyć wystąpienia klasy abstrakcyjnej lub interfejsu". Jeśli podjęto próbę utworzenia wystąpienia klasy przy użyciu odbicia, metoda odbicia zgłasza MemberAccessException.

    Domyślnie klasę bazową można utworzyć, wywołując konstruktor klasy. Nie trzeba jawnie definiować konstruktora klasy. Jeśli nie ma go w kodzie źródłowym klasy bazowej, kompilator języka C# automatycznie udostępnia domyślny (bez parametrów) konstruktor.

    Oznaczysz klasę Publication jako abstrakcyjną , aby nie można było z niej utworzyć wystąpienia do swojego przykładu. Klasa abstract bez żadnych metod abstract wskazuje, że ta klasa reprezentuje abstrakcyjną koncepcję współdzieloną między kilka klas (takich jak Book, Journal).

  • Czy klasy pochodne muszą dziedziczyć implementację klasy bazowej określonych elementów członkowskich, czy mają możliwość zastąpienia implementacji klasy bazowej, czy też muszą zapewnić implementację. Używasz abstrakcyjnego słowa kluczowego , aby wymusić, że klasy pochodne muszą zapewnić implementację. Aby umożliwić klasom pochodnym zastąpienie metody klasy bazowej, należy użyć słowa kluczowego virtual. Domyślnie metody zdefiniowane w klasie bazowej nie są zastępowalne.

    Klasa Publication nie ma żadnych metod abstract, ale sama klasa jest abstract.

  • Czy klasa pochodna reprezentuje końcową klasę w hierarchii dziedziczenia i nie może być używana jako klasa bazowa dla dodatkowych klas pochodnych. Domyślnie każda klasa może służyć jako klasa bazowa. Możesz zastosować zapieczętowane słowo kluczowe, aby wskazać, że klasa nie może służyć jako klasa bazowa dla żadnych dodatkowych klas. Próba dziedziczenia z klasy oznaczonej jako zapieczętowana spowodowała błąd kompilatora CS0509: "Nie można dziedziczyć po typie zapieczętowanym <typeName>."

    Na przykład oznaczysz klasę pochodną jako sealed.

Poniższy przykład przedstawia kod źródłowy klasy Publication, a także wyliczenie PublicationType zwracane przez właściwość Publication.PublicationType. ** Oprócz członków, które dziedziczy z Object, klasa Publication definiuje następujące unikalne elementy i przesłonięcia elementów:


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

    Ponieważ klasa Publication jest abstract, nie można stworzyć jej obiektu bez użycia kodu, jak w poniższym przykładzie:

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

    Jednak konstruktor instancji może być wywoływany bezpośrednio z konstruktorów klas pochodnych, co pokazuje kod źródłowy klasy Book.

  • Dwie właściwości związane z publikacją

    Title jest właściwością String tylko do odczytu, której wartość jest dostarczana przez wywołanie konstruktora Publication.

    Pages jest właściwością Int32 odczytu i zapisu, która wskazuje, ile stron zawiera publikacja. Wartość jest przechowywana w polu prywatnym o nazwie totalPages. Musi to być liczba dodatnia albo zostanie rzucony ArgumentOutOfRangeException.

  • Członkowie związani z wydawcą

    Dwie właściwości tylko do odczytu, Publisher i Type. Wartości są pierwotnie dostarczane przez wywołanie konstruktora klasy Publication.

  • Członkowie związani z publikowaniem

    Dwie metody, Publish i GetPublicationDate, ustawiają i zwracają datę publikacji. Metoda Publish ustawia prywatną flagę published na true, gdy jest wywoływana, i przypisuje datę przekazaną jako argument tej metodzie do prywatnego pola datePublished. Metoda GetPublicationDate zwraca ciąg "NYP", jeśli flaga published jest false, a wartość pola datePublished, jeśli jest true.

  • Członkowie związani z prawami autorskimi

    Metoda Copyright przyjmuje nazwę właściciela praw autorskich i rok praw autorskich jako argumenty i przypisuje je do właściwości CopyrightName i CopyrightDate.

  • Zastąpienie metody ToString

    Jeśli typ nie zastępuje metody Object.ToString, zwraca w pełni kwalifikowaną nazwę typu, która jest mało używana w różnicowaniu jednego wystąpienia z innego. Klasa Publication zastępuje Object.ToString, aby zwrócić wartość właściwości Title.

Na poniższej ilustracji przedstawiono relację między klasą podstawową Publication a klasą Object, która jest niejawnie odziedziczona.

Klasy Object i Publication

Klasa Book

Klasa Book reprezentuje książkę jako wyspecjalizowany typ publikacji. Poniższy przykład przedstawia kod źródłowy klasy 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}";
}

** Oprócz członków, które dziedziczy z Publication, klasa Book definiuje następujące unikalne elementy i przesłonięcia elementów:

  • Dwa konstruktory

    Dwa konstruktory Book mają trzy typowe parametry. Dwa elementy, tytuł i wydawca, odpowiadają parametrom konstruktora Publication. Trzeci to autor, który jest przechowywany w niezmiennej właściwości publicznej Author. Jeden z konstruktorów zawiera parametr isbn, który jest przechowywany we właściwości automatycznej ISBN.

    Pierwszy konstruktor używa tego słowa kluczowego do wywołania innego konstruktora. Łańcuch konstruktorów jest typowym wzorcem definiowania konstruktorów. Konstruktory z mniejszą liczbą parametrów zapewniają wartości domyślne podczas wywoływania konstruktora z największą liczbą parametrów.

    Drugi konstruktor używa słowa kluczowego base, aby przekazać tytuł i nazwę wydawcy do konstruktora klasy bazowej. Jeśli nie wykonasz jawnego wywołania konstruktora klasy bazowej w kodzie źródłowym, kompilator języka C# automatycznie dostarcza wywołanie domyślnego lub bez parametrów konstruktora klasy bazowej.

  • Właściwość ISBN tylko do odczytu, która zwraca Międzynarodowy Standardowy Numer Książki (ISBN) obiektu Book, będący unikatowym numerem 10- lub 13-cyfrowym. ISBN jest dostarczany jako argument do jednego z konstruktorów Book. ISBN jest przechowywany w prywatnym polu pomocniczym, które jest generowane automatycznie przez kompilator.

  • Właściwość Author, tylko do odczytu. Nazwa autora jest przekazywana jako argument do obu konstruktorów Book i zapisywana we właściwości.

  • Dwie tylko do odczytu właściwości związane z ceną, Price i Currency. Ich wartości są podawane jako argumenty w wywołaniu metody SetPrice. Właściwość Currency jest trzycyfrowym symbolem waluty ISO (na przykład USD dla dolara amerykańskiego). Symbole waluty ISO można pobrać z właściwości ISOCurrencySymbol. Obie te właściwości są tylko do odczytu zewnętrznego, ale oba te właściwości można ustawić za pomocą kodu w klasie Book.

  • Metoda SetPrice, która ustawia wartości właściwości Price i Currency. Te wartości są zwracane przez te same właściwości.

  • Zastępuje metodę ToString (dziedziczona z Publication) oraz metody Object.Equals(Object) i GetHashCode (dziedziczone z Object).

    Jeśli nie zostanie zastąpiona, metoda Object.Equals(Object) sprawdza, czy następuje równość odwołań. Oznacza to, że dwie zmienne obiektu są uważane za równe, jeśli odwołują się do tego samego obiektu. Z drugiej strony, w klasie Book dwa obiekty Book powinny być równe, jeśli mają ten sam ISBN.

    Po zastąpieniu metody Object.Equals(Object) należy również zastąpić metodę GetHashCode, która zwraca wartość, którą środowisko uruchomieniowe używa do przechowywania elementów w kolekcjach z haszowaniem w celu wydajnego pobierania. Kod skrótu powinien zwracać wartość zgodną z testem równości. Ponieważ nadpisałeś Object.Equals(Object), aby zwracać true, jeśli właściwości ISBN dwóch obiektów Book są równe, zwracasz kod skrótu obliczony przez wywołanie metody GetHashCode na ciągu zwróconym przez właściwość ISBN.

Na poniższej ilustracji przedstawiono relację między klasą Book a Publication, jej klasą bazową.

Klasy publikacji i książek

Teraz można utworzyć wystąpienie obiektu Book, wywołać jego unikalne i odziedziczone człony oraz przekazać go jako argument do metody, która oczekuje parametru typu Publication lub typu Book, jak pokazano w poniższym przykładzie.

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

Projektowanie abstrakcyjnych klas bazowych i ich klas pochodnych

W poprzednim przykładzie zdefiniowano klasę bazową, która dostarczyła implementację dla wielu metod, aby umożliwić klasom pochodnym udostępnianie kodu. Jednak w wielu przypadkach klasa bazowa nie powinna zapewniać implementacji. Zamiast tego klasa bazowa jest klasą abstrakcyjną , która deklaruje metody abstrakcyjne ; służy jako szablon, który definiuje składowe, które muszą implementować każda klasa pochodna. Zazwyczaj w abstrakcyjnej klasie bazowej implementacja każdego typu pochodnego jest unikatowa dla tego typu. Klasa została oznaczona jako abstrakcyjna słowem kluczowym, ponieważ nie ma sensu tworzyć wystąpienia obiektu Publication, chociaż klasa udostępnia implementacje funkcjonalności wspólnych dla publikacji.

Na przykład każdy zamknięty dwuwymiarowy kształt geometryczny zawiera dwie właściwości: obszar, wewnętrzny zakres kształtu; i obwód, lub odległość wzdłuż krawędzi kształtu. Sposób obliczania tych właściwości zależy jednak całkowicie od określonego kształtu. Formuła obliczania obwodu (lub obwodu) okręgu, na przykład, różni się od kwadratu. Klasa Shape jest klasą abstract z metodami abstract. Oznacza to, że klasy pochodne współużytkują tę samą funkcjonalność, ale te klasy pochodne implementują tę funkcjonalność inaczej.

W poniższym przykładzie zdefiniowano abstrakcyjną klasę bazową o nazwie Shape, która definiuje dwie właściwości: Area i Perimeter. Oprócz oznaczania klasy za pomocą słowa kluczowego abstrakcyjnego, każdy element członkowski wystąpienia jest również oznaczony słowem kluczowym abstrakcyjnym. W tym przypadku Shape również zastępuje metodę Object.ToString, aby zwrócić nazwę typu, a nie jego w pełni kwalifikowaną nazwę. Definiuje ona dwa statyczne elementy członkowskie, GetArea i GetPerimeter, które umożliwiają obiektom wywołującym łatwe pobieranie obszaru i obwodu wystąpienia dowolnej klasy pochodnej. Po przekazaniu wystąpienia klasy pochodnej do jednej z tych metod środowisko uruchomieniowe wywołuje zastąpienie metody klasy pochodnej.

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

Następnie można utworzyć niektóre klasy z Shape, które reprezentują określone kształty. W poniższym przykładzie zdefiniowano trzy klasy, Square, Rectanglei Circle. Każdy z nich używa formuły unikatowej dla tego konkretnego kształtu do obliczenia obszaru i obwodu. Niektóre klasy pochodne definiują również właściwości, takie jak Rectangle.Diagonal i Circle.Diameter, które są unikatowe dla reprezentowanego kształtu.

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

W poniższym przykładzie użyto obiektów pochodzących z Shape. Tworzy instancję tablicy obiektów wywodzących się z Shape i wywołuje statyczne metody klasy Shape, która opakowuje wartości zwracane przez właściwości Shape. Środowisko uruchomieniowe pobiera wartości z właściwości, które zostały zastąpione w typach pochodnych. Przykład rzutuje również każdy obiekt Shape w tablicy na typ pochodny i, jeśli rzutowanie powiedzie się, pobiera właściwości tej konkretnej podklasy 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