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
- Zalecamy program Visual Studio dla systemu Windows. Bezpłatną wersję można pobrać ze strony pobierania programu Visual Studio. Program Visual Studio zawiera zestaw .NET SDK.
- Możesz również użyć edytora programu Visual Studio Code z zestawem deweloperskim języka C#. Musisz zainstalować oddzielnie najnowszy zestaw .NET SDK .
- Jeśli wolisz inny edytor, musisz zainstalować najnowszy zestaw .NET SDK.
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:
Utwórz katalog do przechowywania przykładu.
Wprowadź polecenie dotnet new console w wierszu polecenia, aby utworzyć nowy projekt platformy .NET Core.
Skopiuj i wklej kod z przykładu do edytora kodu.
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 jakdotnet new
, ,dotnet build
,dotnet run
dotnet test
, ,dotnet publish
, idotnet 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
.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 A
klasy 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 zA
klasy , iC
pochodzi zA
klasy . Pole prywatneA._value
jest widoczne w usłudze A.B. Jeśli jednak usuniesz komentarze zC.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 nazwieMethod1
, a klasaB
dziedziczy z klasyA
. W tym przykładzie wywołanoMethod1
metodę wystąpienia w metodzieB
.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.Method1
elementu .
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ę , SimpleClass
czyli 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 przypadkuToString
metoda zwraca ciąg "SimpleClass".Trzy metody, które testuje równość dwóch obiektów: metoda wystąpienia
Equals(Object)
publicznego, publiczna metoda statycznaEquals(Object, Object)
i publiczna metoda statycznaReferenceEquals(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ącySimpleClass
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ą , Publication
któ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
, Newspaper
i 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 czyPublication
składowe zapewniają implementacje metod, czyPublication
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 wyspecjalizowanePublication
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ć klasaPeriodical
bazowa klasy , która z kolei jest klasąMagazine
bazową klasy ,Journal
iNewspaper
.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ą zPublication
klasy , takich jakMagazine
iArticle
.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 oznaczonejabstract
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. Klasaabstract
bez żadnychabstract
metod wskazuje, że ta klasa reprezentuje abstrakcyjną koncepcję współdzieloną między kilkomaBook
konkretnymi 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 nie są zastępowalne.
Klasa
Publication
nie ma żadnychabstract
metod, ale sama klasa toabstract
.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 toabstract
, 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 konstruktoraPublication
.Pages
to właściwość read-write Int32 , która wskazuje, ile stron zawiera publikacja. Wartość jest przechowywana w polu prywatnym o nazwietotalPages
. 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 iType
. Wartości są pierwotnie dostarczane przez wywołanie konstruktoraPublication
klasy.Członkowie związani z publikowaniem
Dwie metody i
Publish
GetPublicationDate
, ustawiają i zwracają datę publikacji. MetodaPublish
ustawia flagę prywatnąpublished
natrue
po wywołaniu i przypisuje datę przekazaną do niej jako argument do pola prywatnegodatePublished
. MetodaGetPublicationDate
zwraca ciąg "NYP", jeśli flagapublished
tofalse
, a wartośćdatePublished
pola totrue
.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 doCopyrightName
właściwości iCopyrightDate
.Zastąpienie
ToString
metodyJeś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
Publication
Object.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 .
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 Publication
klasy , 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 konstruktoraPublication
. Trzeci to autor, który jest przechowywany w publicznej właściwości niezmiennejAuthor
. Jeden konstruktor zawiera parametr isbn , który jest przechowywany we właściwości automatycznejISBN
.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 zwracaBook
numer międzynarodowej książki standardowej obiektu, unikatowy numer 10- lub 13-cyfrowy. IsBN jest dostarczany jako argument do jednego zBook
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 obuBook
konstruktorów i jest przechowywana we właściwości .Dwie właściwości
Price
związane z ceną tylko do odczytu iCurrency
. Ich wartości są podawane jako argumenty w wywołaniuSetPrice
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 wBook
klasie .Metoda
SetPrice
, która ustawia wartościPrice
właściwości iCurrency
. Te wartości są zwracane przez te same właściwości.Zastępuje metodę
ToString
(dziedziczona zPublication
klasy ) 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, dwaBook
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óchBook
obiektów są równe, zwracasz kod skrótu obliczony przez wywołanie GetHashCode metody ciągu zwróconegoISBN
przez właściwość .
Na poniższej ilustracji przedstawiono relację między klasą a Publication
klasą Book
bazową.
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
, Rectangle
i 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 Shape
elementu . 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