Поделиться через


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

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

Предпосылки

Инструкции по установке

В Windows этот файл конфигурации WinGet используется для установки всех необходимых компонентов. Если у вас уже установлено что-то, WinGet пропустит этот шаг.

  1. Скачайте файл и дважды щелкните его, чтобы запустить его.
  2. Прочитайте лицензионное соглашение, введите и, и выберите ввод при появлении запроса на принятие.
  3. Если на панели задач появится мигающий запрос контроля учетных записей пользователей (UAC), разрешите установку продолжить.

На других платформах необходимо установить каждый из этих компонентов отдельно.

  1. Скачайте рекомендуемый установщик на странице загрузки пакета SDK для .NET и дважды щелкните его, чтобы запустить его. Страница загрузки обнаруживает платформу и рекомендует последний установщик для вашей платформы.
  2. Скачайте последнюю версию установщика на домашней странице Visual Studio Code и дважды щелкните его, чтобы запустить его. Эта страница также обнаруживает платформу, а ссылка должна быть правильной для вашей системы.
  3. Нажмите кнопку "Установить" на странице расширения C# DevKit. Откроется код Visual Studio и запрашивается, нужно ли установить или включить расширение. Выберите "Установить".

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

Чтобы создать и запустить примеры в этом руководстве, используйте утилиту 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 или системах сборки, где требуется явно контролировать время восстановления.

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

  5. Введите команду dotnet run для компиляции и выполнения примера.

Контекст: Что такое наследование?

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

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

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

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

  • Закрытые члены доступны только в производных классах, которые вложены в свой базовый класс. В противном случае они не отображаются в производных классах. В следующем примере A.B является вложенным классом, производным от A, а C производным от A. В A.B отображается частное поле A._value. Однако если удалить комментарии из метода 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 и не могут быть переопределены. Попытка переопределить невиртуационный член, как и в следующем примере, создает ошибку компилятора CS0506: "<член> не может переопределить унаследованный член <член>, так как он не помечен как виртуальный, абстрактный или переопределенный".

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

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

В некоторых случаях производный класс должен переопределить реализацию базового класса. Члены базового класса, помеченные ключевым словом абстрактный, требуют, чтобы производные классы их переопределяли. При попытке скомпилировать следующий пример возникает ошибка компилятора CS0534, "<класс> не реализует унаследованный абстрактный элемент <член>", так как класс 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# и типы, от которых они неявно наследуются. Каждый базовый тип делает другой набор элементов доступным через наследование для неявно производных типов.

Тип категории Неявно наследуется от
класс Object
Структура ValueType, Object
перечисление Enum, ValueType, Object
делегат MulticastDelegate, Delegate, Object

Наследование и отношение "это"

Обычно наследование используется для выражения "является" связью между базовым классом и одним или несколькими производными классами, где производные классы являются специализированными версиями базового класса; Производный класс — это тип базового класса. Например, класс Publication представляет публикацию любого вида, а классы Book и Magazine представляют определенные типы публикаций.

Примечание.

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

Обратите внимание, что "является" также выражает связь между типом и конкретным экземпляром этого типа. В следующем примере 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.

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

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

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

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

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

  • Является ли производный класс конечным классом в иерархии наследования и сам по себе не может использоваться в качестве базового класса для дополнительных производных классов. По умолчанию любой класс может служить базовым классом. Можно применить ключевое слово , чтобы указать, что класс не может служить базовым классом для других классов. Попытка наследовать от запечатанного класса вызвала ошибку компилятора 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;
}
  • Конструктор

    Так как класс Publicationabstract, он не может быть создан непосредственно из кода, как показано в следующем примере:

    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", если флаг publishedfalse, а значение поля datePublished, если оно true.

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

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

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

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

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

классы объектов и публикации

Класс 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 используют три общих параметра. Два параметра: заголовок и издательсоответствуют параметрам конструктора Publication. Третий — это автор, который хранится в общедоступном неизменяемом Author свойстве. Конструктор включает параметр isbn, который сохраняется в автоматическом свойстве ISBN.

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

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

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

  • Свойство 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, когда свойства ISBN двух объектов Book равны, вы возвращаете хэш-код, вычисленный с помощью вызова метода GetHashCode строки, возвращаемой свойством ISBN.

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

классы публикации и книг

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