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

Uruchamianie przykładów

Aby utworzyć i uruchomić przykłady w tym samouczku, użyj narzędzia dotnet z poziomu 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 trzeba uruchamiaćdotnet restore, ponieważ jest ona uruchamiana niejawnie przez wszystkie polecenia, które wymagają przywrócenia, takie jak dotnet new, , dotnet build, dotnet rundotnet test, , dotnet publish, i dotnet pack. Aby wyłączyć niejawne przywracanie, użyj --no-restore opcji .

    Polecenie dotnet restore jest nadal przydatne w niektórych scenariuszach, w których jawne przywracanie ma sens, takie jak kompilacje ciągłej integracji w usługach Azure DevOps Services lub w systemach kompilacji, które muszą jawnie kontrolować, kiedy nastąpi przywracanie.

    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 ponownie (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ć po typie C, który dziedziczy z typu B, który dziedziczy z typu Aklasy bazowej . 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ące elementy członkowskie nie są dziedziczone:

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

  • Konstruktory wystąpień, które są wywoływane w celu utworzenia nowego wystąpienia klasy. Każda klasa musi definiować własne konstruktory.

  • Finalizatory, które są wywoływane przez moduł odśmieceń pamięci środowiska uruchomieniowego w celu zniszczenia wystąpień klasy.

Chociaż wszystkie inne elementy członkowskie klasy bazowej są dziedziczone przez klasy pochodne, niezależnie od tego, czy są widoczne, czy nie, zależą od ich ułatwień dostępu. Ułatwienia dostępu elementu członkowskiego wpływają na widoczność klas pochodnych w następujący sposób:

  • Prywatne elementy członkowskie są widoczne tylko w klasach pochodnych, które są zagnieżdżone w klasie bazowej. W przeciwnym razie nie są one widoczne w klasach pochodnych. W poniższym przykładzie A.B jest zagnieżdżona klasa, która pochodzi z Aklasy , i C pochodzi z Aklasy . Pole prywatne A._value jest widoczne w usłudze A.B. Jeśli jednak usuniesz komentarze z C.GetValue metody 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
    
  • Chronione elementy członkowskie są widoczne tylko w klasach pochodnych.

  • Składowe wewnętrzne 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.

  • Publiczne składowe są widoczne w klasach pochodnych i są częścią interfejsu publicznego klasy pochodnej. Elementy członkowskie dziedziczone publicznie mogą być wywoływane tak samo, jak w przypadku, gdy są 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łano Method1 metodę wystąpienia w metodzie 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ż zastąpić dziedziczone elementy członkowskie, zapewniając alternatywną implementację. Aby można było zastąpić składową, element członkowski w klasie bazowej musi być oznaczony wirtualnym słowem kluczowym. Domyślnie składowe klasy bazowej nie są oznaczone jako virtual i nie mogą być zastępowane. Próba zastąpienia niewirtualnego elementu członkowskiego, jak w poniższym przykładzie, powoduje wygenerowanie błędu kompilatora CS0506: "<członek> nie może zastąpić dziedziczonego elementu> członkowskiego<, ponieważ nie jest oznaczony jako wirtualny, abstrakcyjny lub zastępowany".

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 abstrakcyjnym słowem kluczowym wymagają zastąpienia ich przez klasy pochodne. Próba skompilowania poniższego przykładu powoduje wygenerowanie błędu kompilatora CS0534" "<klasa nie implementuje dziedziczonej składowej abstrakcyjnej>< składowej", ponieważ klasa>B nie zapewnia implementacji dla A.Method1elementu .

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

Oprócz wszystkich typów, które mogą dziedziczyć za pośrednictwem pojedynczego dziedziczenia, wszystkie typy w systemie typów platformy .NET niejawnie dziedziczą lub Object typ pochodzący z niego. Typowe funkcje Object programu są dostępne dla dowolnego typu.

Aby zobaczyć, co oznacza niejawne dziedziczenie, zdefiniujmy nową klasę , SimpleClassczyli po prostu pustą definicję klasy:

public class SimpleClass
{ }

Następnie można użyć odbicia (co umożliwia sprawdzenie metadanych typu w celu uzyskania informacji o tym typie) w celu uzyskania listy elementów członkowskich należących do SimpleClass typu. Mimo że nie zdefiniowano żadnych elementów członkowskich w SimpleClass klasie, 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 dla SimpleClass typu przez kompilator języka C#. Pozostałe osiem to elementy członkowskie Objecttypu , z którego wszystkie klasy i interfejsy w systemie typów platformy .NET ostatecznie niejawnie dziedziczą.

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 Object klasy sprawia, że te metody są dostępne dla SimpleClass klasy:

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

  • Trzy metody, które testuje równość dwóch obiektów: metoda wystąpienia Equals(Object) publicznego, publiczna metoda statyczna Equals(Object, Object) i publiczna metoda statyczna ReferenceEquals(Object, Object) . Domyślnie te metody testowane pod kątem równości referencyjnej; 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ść, która umożliwia użycie wystąpienia typu w kolekcjach skrótów.

  • Metoda publiczna GetType , która zwraca Type obiekt reprezentujący SimpleClass typ.

  • Finalize Chroniona metoda, która jest przeznaczona do zwalniania niezarządzanych zasobów przed odzyskaniem pamięci obiektu przez moduł odśmiecanie pamięci.

  • Metoda chroniona 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 SimpleClass obiektu tak, jakby był on rzeczywiście elementem członkowskim zdefiniowanym SimpleClass w klasie. Na przykład poniższy przykład wywołuje metodę SimpleClass.ToString , która SimpleClass dziedziczy z Objectklasy .

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 do niejawnie pochodnych typów.

Kategoria typów Niejawnie dziedziczy z
class Object
struktura ValueType, Object
wyliczenie Enum, , ValueTypeObject
delegate MulticastDelegate, , DelegateObject

Dziedziczenie i relacja "is a"

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 Publication klasa reprezentuje publikację dowolnego rodzaju, a Book klasy 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 innej relacji (relacja "może zrobić" między interfejsem a jego typem implementowania niż dziedziczenie. 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 "jest" również wyraża relację między typem a określonym wystąpieniem tego typu. W poniższym przykładzie jest to klasa, Automobile 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 Object.ToString metodę w celu utworzenia ciągu, który jednoznacznie identyfikuje Automobile wystąpienie, 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ć Packard typu reprezentującego samochody produkowane przez Packard Motor Car Company. Zamiast tego można je przedstawić, tworząc Automobile obiekt 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 oparta na dziedziczeniu jest najlepiej stosowana do klasy bazowej i do klas pochodnych, które dodają dodatkowe elementy członkowskie do klasy bazowej lub które wymagają dodatkowych funkcji, które nie są obecne 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ą , Publicationktóra reprezentuje publikację dowolnego rodzaju, taką jak książka, magazyn, gazeta, dziennik, artykuł itp. Zdefiniujesz również klasę pochodzącą Book z klasy Publication. Można łatwo rozszerzyć przykład, aby zdefiniować inne klasy pochodne, takie jak Magazine, Journal, Newspaperi Article.

Klasa publikacji podstawowej

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

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

    W tym przypadku Publication klasa będzie dostarczać implementacje metod. Sekcja Projektowanie abstrakcyjnych klas bazowych i ich klas pochodnych zawiera przykład, który używa abstrakcyjnej klasy bazowej do zdefiniowania metod, które klasy pochodne muszą zastąpić. Klasy pochodne są wolne od zapewnienia dowolnej implementacji, która jest odpowiednia dla typu pochodnego.

    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 elementu , jeśli ich kod prawdopodobnie będzie współużytkowany przez niektóre lub najbardziej wyspecjalizowane Publication typy. 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 Publication klasie tylko dane i funkcje wspólne dla wszystkich lub do większości publikacji. Klasy pochodne następnie implementują elementy członkowskie, które są unikatowe dla określonego rodzaju 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 to być klasa Periodicalbazowa klasy , która z kolei jest klasą Magazinebazową klasy , Journal i Newspaper.

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

  • Czy warto utworzyć wystąpienie klasy bazowej. Jeśli tak nie jest, należy zastosować abstrakcyjne słowo kluczowe 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 abstract słowem kluczowym 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 wyjątek 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.

    Na przykład oznaczysz klasę Publication jako abstrakcyjną , aby nie można było utworzyć jej wystąpienia. Klasa abstract bez żadnych abstract metod wskazuje, że ta klasa reprezentuje abstrakcyjną koncepcję współdzieloną między kilkoma Bookkonkretnymi klasami (takimi jak , 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ę. Słowo kluczowe abstrakcyjne służy do wymuszania klas pochodnych w celu zapewnienia implementacji. Użyj wirtualnego słowa kluczowego, aby umożliwić klasom pochodnym zastąpienie metody klasy bazowej. Domyślnie metody zdefiniowane w klasie bazowej niezastępowalne.

    Klasa Publication nie ma żadnych abstract metod, ale sama klasa to 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 wyprowadzenia z zapieczętowanej klasy wygenerowanego błędu kompilatora CS0509 "nie może pochodzić z zapieczętowanego typu typeName<>".

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

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


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 Ponieważ klasa to abstract, nie można utworzyć wystąpienia bezpośrednio z kodu, takiego jak w poniższym przykładzie:

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

    Jednak konstruktor wystąpienia może być wywoływany bezpośrednio z konstruktorów klasy pochodnej Book , jak pokazuje kod źródłowy klasy.

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

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

    Pages to właściwość read-write Int32 , która wskazuje, ile stron zawiera publikacja. Wartość jest przechowywana w polu prywatnym o nazwie totalPages. Musi to być liczba dodatnia ArgumentOutOfRangeException lub jest zgłaszana.

  • Członkowie związani z wydawcą

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

  • Członkowie związani z publikowaniem

    Dwie metody i PublishGetPublicationDate, ustawiają i zwracają datę publikacji. Metoda Publish ustawia flagę prywatną published na true po wywołaniu i przypisuje datę przekazaną do niej jako argument do pola prywatnego datePublished . Metoda GetPublicationDate zwraca ciąg "NYP", jeśli flaga published to false, a wartość datePublished pola to 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 CopyrightName właściwości i CopyrightDate .

  • Zastąpienie ToString metody

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

Na poniższej ilustracji przedstawiono relację między klasą bazową Publication a niejawnie dziedziczą klasą Object .

Klasy Object i Publication

Klasa Book

Klasa Book reprezentuje książkę jako wyspecjalizowany typ publikacji. W poniższym przykładzie pokazano 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 składowych, które dziedziczy z Publicationklasy , Book klasa definiuje następujące unikatowe składowe i przesłonięcia składowe:

  • Dwa konstruktory

    Oba Book konstruktory mają trzy typowe parametry. Dwa, tytuł i wydawca, odpowiadają parametrom konstruktora Publication . Trzeci to autor, który jest przechowywany w publicznej właściwości niezmiennej Author . Jeden konstruktor 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 podstawowego słowa kluczowego, 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ść tylko do ISBN odczytu, która zwraca Book numer międzynarodowej książki standardowej obiektu, unikatowy numer 10- lub 13-cyfrowy. IsBN jest dostarczany jako argument do jednego z Book konstruktorów. IsBN jest przechowywany w prywatnym polu zaplecza, które jest generowane automatycznie przez kompilator.

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

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

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

  • Zastępuje metodę ToString (dziedziczona z Publicationklasy ) i Object.Equals(Object) metod i GetHashCode (dziedziczona z Objectmetody ).

    Jeśli nie zostanie on zastąpiony, Object.Equals(Object) metoda sprawdza 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. Book W klasie, z drugiej strony, dwa Book obiekty powinny być równe, jeśli mają ten sam ISBN.

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

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

Klasy publikacji i książki

Teraz można utworzyć Book wystąpienie obiektu, wywołać zarówno jego unikatowe, jak i odziedziczone elementy członkowskie, a następnie 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 definiujący 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 za pomocą abstrakcyjnego słowa kluczowego, ponieważ nie ma sensu tworzenia wystąpienia Publication obiektu, chociaż klasa zapewniała implementacje funkcji 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ą abstrakcyjnego słowa kluczowego każdy element członkowski wystąpienia jest również oznaczony abstrakcyjnym słowem kluczowym. W takim przypadku Shape zastępuje również metodę Object.ToString , aby zwrócić nazwę typu, a nie jej 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 tych reprezentujących 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 kształtu, który reprezentują.

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 Shapeelementu . Tworzy wystąpienie tablicy obiektów pochodzących z Shape klasy i wywołuje metody Shape statyczne klasy, która opakowuje zwracane Shape wartości właściwości. Środowisko uruchomieniowe pobiera wartości z zastąpionych właściwości typów pochodnych. Przykład rzutuje również każdy Shape obiekt 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