Наследование в C# и .NET

В этом руководстве вы познакомитесь с концепцией наследования в C#. Наследование является ключевой функцией объектно-ориентированных языков программирования. Оно позволяет определить базовый класс для определенных функций (доступа к данным или действий), а затем создавать производные классы, которые наследуют или переопределяют функции базового класса.

Необходимые компоненты

  • Мы рекомендуем Visual Studio для Windows. Вы можете скачать бесплатную версию на странице скачивания Visual Studio. Visual Studio включает пакет SDK для .NET.
  • Вы также можете использовать редактор Visual Studio Code с C# DevKit. Вам потребуется установить последний пакет SDK для .NET отдельно.
  • Если вы предпочитаете другой редактор, необходимо установить последний пакет SDK для .NET.

Выполнение примеров

Чтобы создать и запустить примеры, представленные в этом руководстве, используйте служебную программу dotnet, выполняемую из командной строки. Выполните следующие действия для каждого примера.

  1. Создайте каталог для хранения примера.

  2. Введите в командной строке команду dotnet new console, чтобы создать новый проект .NET Core.

  3. Скопируйте код примера и вставьте его в файл с помощью редактора кода.

  4. Введите в командной строке команду dotnet restore, чтобы загрузить или восстановить зависимости проекта.

    Вам не нужно выполнять команду dotnet restore, так как она выполняется неявно всеми командами, которые требуют восстановления, например dotnet new, dotnet build, dotnet run, dotnet test, dotnet publish и dotnet pack. Чтобы отключить неявное восстановление, используйте параметр --no-restore.

    Команду dotnet restore по-прежнему удобно использовать в некоторых сценариях, где необходимо явное восстановление, например в сборках с использованием непрерывной интеграции в Azure DevOps Services или системах сборки, где требуется явно контролировать время восстановления.

    Сведения об управлении веб-каналами NuGet см. в документации по dotnet restore.

  5. Введите команду dotnet run, чтобы скомпилировать и выполнить пример.

Справочная информация. Что такое наследование?

Наследование является одним из фундаментальных атрибутов объектно-ориентированного программирования. Оно позволяет определить дочерний класс, который использует (наследует), расширяет или изменяет возможности родительского класса. Класс, члены которого наследуются, называется базовым классом. Класс, который наследует члены базового класса, называется производным классом.

C# и .NET поддерживают только одиночное наследование. Это означает, что каждый класс может наследовать члены только одного класса. Но зато поддерживается транзитивное наследование, которое позволяет определить иерархию наследования для набора типов. Другими словами, тип D может наследовать возможности типа C, который в свою очередь наследует от типа B, который наследует от базового класса A. Благодаря транзитивности наследования члены типа A будут доступны для типа D.

Не все члены базового класса наследуются производными классами. Следующие члены не наследуются.

Все остальные члены базового класса наследуются производными классами, но их видимость не зависит от доступности. Доступность членов влияет на видимость для производных классов следующим образом.

  • Закрытые члены являются видимыми только в производных классах, которые вложены в базовый класс. Для других производных классов они невидимы. В следующем примере класс A.B является вложенным и производным от A, а C является производным от A. Частное A._value поле отображается в A.B. Однако если удалить комментарии из C.GetValue метода и попытаться скомпилировать пример, он создает ошибку компилятора CS0122: "A._value" недоступна из-за его уровня защиты".

    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
    
  • Защищенные члены являются видимыми только в производных классах.

  • Внутренние члены являются видимыми только в производных классах, которые находятся в той же сборке, что и базовый класс. Они не будут видимыми в производных классах, расположенных в других сборках.

  • Открытые члены являются видимыми в производных классах, а также входят в общедоступный интерфейс производных классов. Унаследованные открытые члены можно вызывать так же, как если бы они были определены в самом производном классе. В следующем примере класс A определяет метод с именем Method1, а класс B наследует от класса A. В нашем примере Method1 вызывается так, как если бы это был метод класса 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();
        }
    }
    

Производные классы могут также переопределять унаследованные члены, то есть предоставлять альтернативную реализацию. Переопределить можно только те члены, которые в базовом классе отмечены ключевым словом virtual (виртуальный). По умолчанию нельзя переопределять члены базового класса, не отмеченные ключевым словом virtual. Попытка переопределить не виртуальный член, как это делается в следующем примере, создает ошибку компилятора CS0506: "<член> не может переопределить унаследованный <член> , так как он не помечен как виртуальный, абстрактный или переопределенный".

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

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

В некоторых случаях производный класс обязан переопределять реализацию базового класса. Члены базового класса, отмеченные ключевым словом abstract (абстрактный), обязательно должны переопределяться в производных классах. При попытке компиляции следующего примера возникнет ошибка компилятора CS0534, "<class> не реализует наследуемый абстрактный член <member>", поскольку класс B не предоставляет реализации для A.Method1.

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

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

Наследование применяется только для классов и интерфейсов. Другие категории типов (структуры, делегаты и перечисления) не поддерживают наследование. Из-за этих правил попытка компиляции кода, как в следующем примере, приводит к ошибке компилятора CS0527: "Тип "ValueType" в списке интерфейсов не является интерфейсом". Сообщение об ошибке указывает, что, хотя можно определить интерфейсы, реализующие структуру, наследование не поддерживается.

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

Неявное наследование

Помимо тех типов, которые наследуются через механизм одиночного наследования, все типы в системе типов .NET неявно наследуются от типа Object или его производного типа. Общие функции Object доступны любому типу.

Чтобы продемонстрировать неявное наследование, давайте определим новый класс SimpleClass, определение которого будет пустым.

public class SimpleClass
{ }

После этого с помощью отражения (которое позволяет проверить метаданные типа для получения сведений о нем) мы получим список членов, принадлежащих типу SimpleClass. Выходные данные этого примера возвращают нам девять членов класса SimpleClass, хотя мы не определяли ни один из них. Один из членов является вызываемым без параметров конструктором по умолчанию, который автоматически предоставляется для типа SimpleClass компилятором C#. Остальные восемь являются членами типа Object, от которого неявным образом наследуются все классы и интерфейсы в системе типов .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

Неявное наследование от класса Object делает доступными для класса SimpleClass следующие методы.

  • Открытый метод ToString, который преобразует объект SimpleClass в строковое представление, возвращает полное имя типа. В нашем примере метод ToString возвращает строку SimpleClass.

  • Три метода, которые проверяют равенство двух объектов: открытый метод экземпляра Equals(Object), открытый статический метод Equals(Object, Object) и открытый статический метод ReferenceEquals(Object, Object). По умолчанию эти методы проверяют ссылочное равенство. Это означает, что две переменные, содержащие объекты, должны ссылаться на один и тот же объект, чтобы считаться равными.

  • Открытый метод GetHashCode, который вычисляет значение, позволяющее использовать экземпляр типа в хэшированных коллекциях.

  • Открытый метод GetType, который возвращает объект Type, представляющий тип SimpleClass.

  • Защищенный метод Finalize, который должен освобождать неуправляемые ресурсы перед тем, как сборщик мусора освободит память объекта.

  • Защищенный метод MemberwiseClone, который создает неполную копию текущего объекта.

Неявное наследование позволяет вызвать любой наследуемый член объекта SimpleClass точно так же, как если бы он был определен в самом классе SimpleClass. Например, следующий пример вызывает метод SimpleClass.ToString, который SimpleClass наследует от 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

В следующей таблице перечислены категории типов, которые можно создавать на языке C#, и указаны типы, от которых они неявно наследуют. Каждый из базовых типов предоставляет всем типам, которые неявно наследуют от него, разные наборы членов.

Категория типа Неявно наследует от
class Object
struct ValueType, Object
перечисление Enum, , ValueTypeObject
delegate MulticastDelegate, , DelegateObject

Наследование и связь "является"

Обычно наследование выражает связь вида "is a" (является) между базовым классом и одним или несколькими производными классами. Производные классы рассматриваются как специализированные версии базового класса, то есть как подтипы базового класса. Например класс Publication представляет публикации любого рода, а классы Book и Magazine представляют публикации определенных типов.

Примечание.

Класс или структура могут реализовывать один или несколько интерфейсов. Реализация интерфейсов часто рассматривается как метод для обхода ограничений одиночного наследования или для реализации наследования структур. Но его основным назначением является выражение связи другого рода между интерфейсом и реализующим его типом. Эта связь называется "can do" (может выполнять) и она отличается от связи наследования. Интерфейс определяет подмножество функций (например, проверка равенства, сравнение и сортировка объектов, или поддержка синтаксического анализа и форматирования с учетом языка и региональных параметров). Интерфейс предоставляет эти функции всем типам, которые его реализуют.

Обратите внимание, что связь "является" выражает также связь между типом и конкретным экземпляром этого типа. В следующем примере представлен класс Automobile с тремя уникальными свойствами только для чтения: Make определяет производителя автомобиля, Model определяет тип автомобиля, а Year — год выпуска. Этот класс Automobile также содержит конструктор, аргументы которого назначаются значениям свойств. Еще в нем переопределен метод Object.ToString, который теперь возвращает строку, однозначно определяющую экземпляр Automobile, а не класс 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}";
}

В этом примере не следует использовать наследование для представления определенных производителей и моделей автомобилей. Например, вам не нужно определять тип Packard, который будет представлять автомобили, произведенные компанией Packard Motor Car. Для этого представления вы создадите объект Automobile и передадите конструктору этого класса соответствующие значения, как показано в следующем примере.

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

Основанную на наследовании связь "является" лучше всего использовать для базовых классов и производных классов, которые добавляют дополнительные члены или используют дополнительные функции, отсутствующие в базовом классе.

Разработка базового класса и его производных классов

Давайте рассмотрим процесс создания базового класса и его производных классов. В этом разделе вы определите базовый класс, Publicationкоторый представляет публикацию любого вида, например книгу, журнал, газету, журнал, журнал, журнал, журнал, журнал, статью и т. д. Вы также определите класс, производный Book от Publication. Этот пример легко расширить, определив другие производные классы, например Magazine, Journal, Newspaper и Article.

Базовый класс Publication

При разработке класса Publication нужно принять несколько решений по его структуре.

  • Какие члены следует включить в базовый класс Publication и будут ли члены Publication реализовывать нужные методы? Или же базовый класс Publication лучше сделать абстрактным, то есть шаблоном для производных классов?

    В нашем примере класс Publication будет предоставлять реализации методов. Раздел Разработка абстрактных базовых классов и их производных классов содержит пример, в котором абстрактный базовый класс определяет методы, переопределяемые в производных классах. Производные классы могут использовать любую реализацию, применимую для конкретного производного типа.

    Важным преимуществом неабстрактных базовых классов является возможность повторно использовать код (несколько производных классов используют объявления и реализации методов из базового класса, и могут не переопределять их). Таким образом, в Publication следует включать такие члены, которые с высокой долей вероятности будут использоваться в неизменном виде несколькими специализированными типами Publication. Если вам не удастся эффективно предоставить реализации базового класса, вам придется создавать идентичные реализации членов в производных классах вместо того, чтобы использовать одну реализацию в базовом классе. Необходимость поддерживать несколько копий идентичного кода в нескольких местах станет потенциальным источником ошибок.

    Чтобы оптимизировать повторное использование кода и создать логичную и интуитивно понятную иерархию наследования, необходимо включать в класс Publication только такие данные и функции, которые используются для большинства публикаций. Затем в производных классах реализуются уникальные члены для каждого вида публикаций, которые они представляют.

  • Насколько глубокой будет иерархия классов? Хотите ли вы включить в иерархию три или больше уровней классов или обойдетесь одним базовым классом с несколькими производными? Например Publication может являться базовым классом для Periodical, от которого будут наследовать классы Magazine, Journal и Newspaper.

    В вашем примере вы будете использовать небольшую иерархию, состоящую из класса Publication и одного производного класса Book. Вы можете с легкостью расширить пример, включив несколько дополнительных производных от Publication, например Magazine и Article.

  • Нужны ли нам экземпляры базового класса? Если нет, то для этого класса следует указать ключевое слово abstract. В противном случае можно будет создать экземпляр класса Publication, вызвав конструктор этого класса. Если предпринята попытка создать экземпляр класса, abstract помеченного ключевое слово прямым вызовом конструктора класса, компилятор C# создает ошибку CS0144, "Не удается создать экземпляр абстрактного класса или интерфейса". Если выполняется попытка создать экземпляр класса с помощью отражения, метод отражения создает исключениеMemberAccessException.

    По умолчанию есть возможность создать экземпляр, вызвав конструктор базового класса. Конструктор класса необязательно определять явным образом. Если конструктор отсутствует в исходном коде базового класса, компилятор C# автоматически предоставляет конструктор по умолчанию (без параметров).

    В вашем примере следует обозначить класс Publication как абстрактный, и для него нельзя будет создавать экземпляры. Класс abstract без методов abstract указывает, что этот класс представляет абстрактное понятие, которое является общим для нескольких конкретных классов (таких как Book, Journal).

  • Должны ли производные классы наследовать реализацию членов базового класса? Или же они могут переопределить реализацию базового класса? Или они должны предоставлять реализацию? Используйте ключевое слово abstract, чтобы производные классы принудительно предоставляли реализацию. Чтобы производные классы могли переопределять методы базового класса, используйте ключевое слово virtual. По умолчанию методы, определенные в базовом классе, переопределять нельзя.

    Класс Publication не имеет методов abstract, но сам класс помечен как abstract.

  • Будет ли производный класс последним в иерархии наследования (то есть его нельзя будет использовать в качестве базового класса для дополнительных производных классов)? По умолчанию любой класс можно использовать в качестве базового класса. Если указать ключевое слово sealed (запечатан), то класс нельзя будет использовать как базовый класс для дополнительных производных классов. Попытка наследовать от запечатанного класса, созданного ошибкой компилятора CS0509, "не может быть производным от запечатанного типа <TypeName>".

    В вашем примере обозначьте производный класс как sealed.

В следующем примере представлен исходный код для класса Publication, а также перечисление PublicationType, возвращаемое свойством Publication.PublicationType. Помимо элементов, которые он наследует от Object, класс Publication определяет и переопределяет следующие члены.


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;
}
  • Конструктор

    Поскольку класс Publication имеет обозначение abstract, для него нельзя напрямую создать экземпляр из кода, как в следующем примере:

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

    Но при этом его конструктор для создания экземпляров можно напрямую вызвать из конструкторов производных классов, как показано в исходном коде для класса Book.

  • Два свойства, относящиеся к публикации

    Свойство Title доступно только для чтения и имеет тип String. Его значение предоставляется путем вызова конструктора Publication.

    Свойство Pages доступно для чтения и записи и имеет тип Int32. Значение этого свойства показывает, сколько всего страниц имеет эта публикация. Это значение хранится в скрытом поле с именем totalPages. В качестве значения принимается положительное число. В противном случае создается исключение ArgumentOutOfRangeException.

  • Члены, связанные с издателем

    Два свойства только для чтения: Publisher и Type. Эти значения изначально предоставляются путем вызова конструктора класса Publication.

  • Элементы, связанные с публикацией

    Два метода, Publish и GetPublicationDate, которые устанавливают и возвращают дату публикации. Метод Publish задает закрытый published флаг true при вызове и назначает дату, переданную ему в качестве аргумента в частное datePublished поле. Метод GetPublicationDate возвращает строку "NYP", если флаг published имеет значение false, или значение поля datePublished, если флаг имеет значение true.

  • Члены, связанные с авторскими правами

    Метод Copyright принимает в качестве аргументов имя владельца авторских прав и год создания авторских прав, и назначает их свойствам CopyrightName и CopyrightDate.

  • Переопределение метода ToString

    Если метод Object.ToString не переопределяется в типе, он возвращает полное имя типа, которое не позволяет отличать экземпляры друг от друга. Класс Publication переопределяет метод Object.ToString, чтобы он возвращал значение свойства Title.

Следующий рисунок иллюстрирует связь между базовым классом Publication и неявно унаследованным от него классом Object.

Классы Object и Publication

Класс Book

Класс Book представляет книгу как специализированный тип публикации. В следующем примере показан исходный код класса 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}";
}

Помимо элементов, которые он наследует от Publication, класс Book определяет и переопределяет следующие члены.

  • Два конструктора

    Два конструктора Book используют три общих параметра. Два из них, header и publisher, соответствуют параметрам конструктора Publication. Третий — это author, который хранится в общедоступном неизменяемом свойстве Author. Один конструктор использует параметр isbn, который хранится в автосвойстве ISBN.

    Первый конструктор использует ключевое слово this для вызова второго конструктора. Создание цепочки конструкторов — это обычный метод определения конструкторов. Конструкторы с меньшим числом параметров используют значения по умолчанию, вызывая конструкторы с большим числом параметров.

    Второй конструктор использует ключевое слово base, чтобы передать заголовок и имя издателя в конструктор базового класса. Если вы не используете явный вызов конструктора базового класса в исходном коде, компилятор C# автоматически добавляет вызов конструктора по умолчанию (без параметров) для базового класса.

  • Свойство ISBN, доступное только для чтения, которое возвращает международный стандартный номер книги (уникальное 10- или 13-значное число) для объекта Book. Номер ISBN передается в качестве аргумента одному из конструкторов Book. Он сохраняется в закрытом резервном поле, автоматически создаваемым компилятором.

  • Свойство Author, доступное только для чтения. Имя автора передается в качестве аргумента обоим конструкторам Book и сохраняется в свойстве.

  • Два свойства, Price и Currency, с информацией о цене, доступные только для чтения. Значения этих свойств передаются в качестве аргументов при вызове метода SetPrice. Свойство Currency содержит трехзначное обозначение валюты по стандарту ISO (например, USD обозначает доллар США). Обозначение валюты по стандарту ISO можно получить из свойства ISOCurrencySymbol. Оба эти свойства доступны для чтения извне, но их можно задать в коде в классе Book.

  • Метод SetPrice, который задает значения для свойств Price и Currency. Эти значения возвращаются теми же свойствами.

  • Переопределение метода ToString, унаследованного от Publication, а также методов Object.Equals(Object) и GetHashCode, унаследованных от Object.

    Если метод Object.Equals(Object) не переопределен, он проверяет ссылочное равенство. Это означает, что две объектные переменные считаются равными, если ссылаются на один и тот же объект. С другой стороны, в классе Book два объекта Book должны считаться равными, если они имеют одинаковые номера ISBN.

    Переопределив метод Object.Equals(Object), необходимо также переопределить метод GetHashCode, который возвращает значение, используемое средой выполнения для хранения элементов в хэшированных коллекциях для быстрого извлечения. Возвращаемое значение хэш-кода должно согласовываться с проверкой на равенство. Поскольку теперь новый метод Object.Equals(Object) возвращает true, если у двух объектов Book равны свойства ISBN, при расчете хэш-кода вы будете вызывать метод GetHashCode для строки, полученной из свойства ISBN.

Следующий рисунок иллюстрирует связь между классом Book и классом Publication, который является для него базовым.

Классы Publication и Book

Теперь вы можете создавать экземпляры объекта Book, вызывать его уникальные и унаследованные члены, а также передавать его в качестве аргумента в любой метод, принимающий параметры типа Publication или Book, как показано в следующем примере.

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

Разработка абстрактных базовых классов и их производных классов

В предыдущем примере вы определили базовый класс, который предоставляет реализацию нескольких методов, обеспечивая совместное использование кода в производных классах. Но во многих случаях базовый класс не должен предоставлять реализацию. Такой базовый класс будет являться абстрактным классом, который объявляет абстрактные методы. Он выступает в качестве шаблона и определяет члены, которые каждый производный класс должен реализовывать самостоятельно. При использовании абстрактного базового класса реализация каждого из производных типов обычно уникальна для конкретного типа. Вы отметили класс ключевым словом abstract, так как не имело смысла создавать экземпляр объекта Publication, хотя класс предоставлял реализации функций, общих для публикаций.

Например, каждая замкнутая геометрическая фигура в двумерном пространстве имеет два свойства: площадь внутренней поверхности и длину ее границ (периметр). Но при этом методы вычисления этих свойств полностью зависят от конкретной фигуры. Формула вычисления периметра (или окружности) окружности, например, отличается от формулы квадрата. Класс Shape является классом abstract с методами abstract. Это означает, что производные классы имеют одинаковые функции, но эти производные классы иначе реализуют эти функции.

Следующий пример определяет абстрактный базовый класс с именем Shape и два его свойства: Area и Perimeter. Ключевым словом abstract помечается не только сам класс. Каждый член экземпляра также получает метку abstract. Кроме того, в классе Shape мы снова переопределяем метод Object.ToString, чтобы он возвращал имя типа, а не полное имя типа. Еще мы определяем два статических члена GetArea и GetPerimeter, которые позволяют вызывающим объектам легко получить площадь и периметр для конкретного экземпляра любого производного класса. Когда вы передаете в любой из этих методов экземпляр производного класса, среда выполнения вызывает переопределенные методы из соответствующего производного класса.

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

Теперь вы можете создать несколько классов, производных от Shape, которые будут представлять разные геометрические фигуры. В следующем примере определяются три класса: Square, Rectangle и Circle. В каждом из них используются уникальные формулы для вычисления площади и периметра, соответствующие типу фигуры. Также некоторые из производных классов определяют дополнительные свойства, например Rectangle.Diagonal и Circle.Diameter, которые уникальны для фигуры, представляемой этим классом.

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

Следующий пример использует объекты, производные от Shape. Он создает массив объектов, производных от Shape, и вызывает статические методы для класса Shape, которые служат оболочкой для обращения к значениям свойств Shape. Среда выполнения извлекает значения переопределенных свойств для производных типов. Также в этом примере каждый объект Shape из созданного массива приводится к производному типу. Если это приведение выполняется успешно, выполняется обращение к свойствам, определенным для конкретного подкласса базового класса 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