Урок 18. Наследование C#


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

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

Наследование является одним из наиболее важных и наиболее мощных понятий объектно-ориентированного программирования. Используя наследование, один класс может быть получен из другого. Производный класс, также известный как дочерний класс или подкласс, наследует функциональные возможности от базового класса, также известного как родительский класс или суперкласс. Подкласс может добавлять методы и свойства или изменять функциональные возможности, которые он унаследовал, чтобы обеспечить более специализированную версию базового класса.

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

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

Полиморфизм

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

Этот тип полиморфизма сильно зависит от принципа инкапсуляции. Поскольку метод, использующий объект транспортного средства, осведомлен только об общественном интерфейсе класса транспортного средства, а не о его внутренней работе, легко заменить объект автомобиля, потому что автомобили - это просто специальный тип транспортного средства.

Одиночное и множественное наследование

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

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

Демонстрация наследования

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

Для начала создайте новое консольное приложение с именем "InheritanceDemo". Создайте файл класса с именем "транспортное средство" и добавьте следующий код к новому классу, чтобы предоставить свойство скорости и методы ускорения и замедления для всех транспортных средств:

private int _speed; public int Speed { get { return _speed; } } public void Accelerate(int mph) { _speed += mph; } public void Decelerate(int mph) { _speed -= mph; } Новый класс транспортных средств может быть создан и испытан сам по себе. Чтобы убедиться, что класс работает правильно, мы можем изменить основной метод программы для проверки методов и свойства:

static void Main(string[] args)
{
    Vehicle v = new Vehicle();
    Console.WriteLine("Speed: {0}mph", v.Speed);    // "Speed 0mph"
    v.Accelerate(25);
    Console.WriteLine("Speed: {0}mph", v.Speed);    // "Speed 25mph"
    v.Decelerate(15);
    Console.WriteLine("Speed: {0}mph", v.Speed);    // "Speed 10mph"
}
Создание производного класса

Синтаксис

Синтаксис для объявления производного класса аналогичен синтаксису для любого другого стандартного класса. Чтобы указать базовый класс для наследования, его имя добавляется к объявлению подкласса, разделенному двоеточием (:) следующим образом:

подкласс класса: baseclass
Класс Мотороллеров

Теперь мы можем создать класс для представления механических транспортных средств, таких как автомобили, автобусы и т.д. Поскольку этот класс является специализированным транспортным средством, которое включает в себя все функциональные возможности класса транспортных средств, мы можем вывести этот класс из транспортного средства. Добавьте новый файл класса в проект и назовите его "MotorVehicle". Измените стандартное объявление класса, чтобы добавить отношение наследования.

class MotorVehicle : Vehicle
{
}
Хотя новый класс не имеет кода, содержащегося в его блоке кода, он уже включает свойства и методы, определенные в его родительском классе. Мы можем легко продемонстрировать это, изменив основной метод программы следующим образом:

static void Main(string[] args)
{
    MotorVehicle v = new MotorVehicle();
    Console.WriteLine("Speed: {0}mph", v.Speed);    // "Speed 0mph"
    v.Accelerate(25);
    Console.WriteLine("Speed: {0}mph", v.Speed);    // "Speed 25mph"
    v.Decelerate(15);
    Console.WriteLine("Speed: {0}mph", v.Speed);    // "Speed 10mph"
}
Новый метод main по существу совпадает с предыдущим примером, за исключением того, что используется экземпляр класса MotorVehicle, а не объект транспортного средства. Выходные данные программы идентичны предыдущему коду, поскольку используемая функциональность была унаследована без изменений.

Для последующего использования в этой статье создайте другой файл класса для велосипедов с именем "Bicycle". Велосипеды также могут ускоряться и замедляться, поэтому измените класс, чтобы он также унаследовал свою функциональность от класса транспортного средства. Вы можете протестировать этот класс с помощью простой модификации метода Main.

class Bicycle : Vehicle
{
}
Добавление своей функциональности в производный класс

До сих пор классы мотоциклов и велосипедов по существу идентичны базовому классу транспортных средств. Значение специализации, обеспечиваемой иерархиями наследования, заключается в том, что производные классы могут реализовывать функции добавления. В случае автотранспортных средств используется топливо. Поэтому мы можем добавить свойства, чтобы определить, сколько галлонов топлива присутствует в транспортном средстве и насколько велик топливный бак. Мы также можем создать метод вызова, когда автомобиль заправляется топливом. Эта функциональность будет присутствовать для автомобилей, но не велосипедов.

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

private float _fuelRemaining;
private float _tankSize;
 
public float FuelRemaining
{
    get { return _fuelRemaining; }
}
 
public float TankSize
{
    get { return _tankSize; }
    set { _tankSize = value; }
}
 
public float Refuel()
{
    float fuelRequired = _tankSize - _fuelRemaining;
    _fuelRemaining = _tankSize;
    return fuelRequired;
}
Чтобы дать классу велосипедов дополнительную функциональность, давайте добавим метод, который звонит в колокол. Чтобы указать, что Гонщик использовал колокол, мы просто выведем строку на консоль. Добавьте следующий метод к классу велосипедов:

public void RingBell()
{
    Console.WriteLine("Ring!");
}
Два производных класса теперь включают все функциональные возможности класса транспортного средства и добавленные элементы добавления. Чтобы продемонстрировать использование двух подклассов, измените основной метод программы следующим образом:

MotorVehicle car = new MotorVehicle();
car.TankSize = 11;
Console.WriteLine("Fuel: {0}g", car.FuelRemaining); // "0g"
car.Refuel();
Console.WriteLine("Fuel: {0}g", car.FuelRemaining); // "11g"
 
Bicycle bike = new Bicycle();
bike.RingBell();                                     // "Ring!"
Переопределение функциональности базового класса

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

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

Добавьте следующий метод к классу транспортных средств:

public virtual void Indicate(bool turningLeft)
{
    if (turningLeft)
        Console.WriteLine("Turning left");
    else
        Console.WriteLine("Turning right");
}
В классе MotorVehicle основное выводимое сообщение будет переопределено. Это достигается путем создания метода с той же областью действия, именем и параметрами или сигнатурой , что и метод базового класса, и префиксом возвращаемого типа метода с ключевым словом override.

Добавьте следующий метод в класс MotorVehicle:

public override void Indicate(bool turningLeft)
{
    if (turningLeft)
        Console.WriteLine("Flashing left indicator");
    else
        Console.WriteLine("Flashing right indicator");
}
Для велосипедов, велосипедист поднимет свою руку, чтобы указать, что они поворачиваются, поэтому добавьте следующий код к классу велосипедов:

public override void Indicate(bool turningLeft)
{
    if (turningLeft)
        Console.WriteLine("Raising left arm");
    else
        Console.WriteLine("Raising right arm");
}
Два подкласса теперь совместно используют один и тот же метод указания как часть их общего интерфейса. Однако внутренняя функциональность этих двух методов отличается, как можно увидеть, выполнив следующий код:

MotorVehicle car = new MotorVehicle();
car.Indicate(true);                     // "Flashing left indicator"
 
Bicycle bike = new Bicycle();
bike.Indicate(true);                    // "Raising left arm"
Вызов функциональности базового класса

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

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

public override void Indicate(bool turningLeft)
{
    base.Indicate(turningLeft);
 
    if (turningLeft)
        Console.WriteLine("Raising left arm");
    else
        Console.WriteLine("Raising right arm");
}
Вызов модифицированного метода показывает результаты обеих операций:

Bicycle bike = new Bicycle();
bike.Indicate(true);
 
/* 
 
Turning left
Raising left arm
 
*/
Полиморфизм и наследование

Полиморфизм является важной концепцией объектно-ориентированного программирования. Полиморфизм - это способность объекта изменять свой публичный интерфейс в соответствии с тем, как он используется. При использовании наследования полиморфизм достигается, когда объект производного класса заменяется там, где ожидается экземпляр его родительского класса. Это использует процесс, известный как upcasting. При восходящей передаче объект более специализированного класса неявно приводится к требуемому типу базового класса. В случае класса мотоциклов и велосипедов экземпляры любого типа могут храниться в переменной транспортного средства или передаваться в качестве параметра метода транспортного средства. Все пользователи объекта будут иметь доступ только к открытому интерфейсу, определяемому базовым классом, но при обращении к этим членам будут выполняться базовые члены производного класса. Таким образом, если мы требуем, чтобы стандартная процедура работала на любом типе транспортного средства, мы можем использовать переменную транспортного средства в знании, что внутри функциональность подкласса будет работать. Чтобы продемонстрировать это, мы создадим некоторые основные переменные транспортного средства, но назначим им объекты MotorVehicle или Bicycle. Затем мы можем наблюдать результаты, как в следующем модифицированном основном методе:

Vehicle car = new MotorVehicle();
Vehicle bike = new Bicycle();
car.Indicate(true);
bike.Indicate(true);
 
/* 
 
Flashing left indicator
Turning left
Raising left arm
 
*/
Другой способ продемонстрировать такое поведение полиморфизма - использовать метод в классе программ. Метод, показанный ниже, принимает параметр транспортного средства, который может быть заменен мотоциклом или велосипедом.

static void Main(string[] args)
{
    Vehicle car = new MotorVehicle();
    Vehicle bike = new Bicycle();
 
    IndicateLeft(car);
    IndicateLeft(bike);
 
    /* 
 
    Flashing left indicator
    Turning left
    Raising left arm
 
    */
}
 
static void IndicateLeft(Vehicle vehicle)
{
    vehicle.Indicate(true);
}
Как видно из предыдущих примеров, преобразование между дочерним классом и его родительским элементом выполняется неявно. Обратное неверно. Если, например, переменная транспортного средства содержит объект MotorVehicle, она не может быть назначена непосредственно переменной MotorVehicle. В этом случае явное приведение будет необходимо, как показано ниже:

Vehicle car = new MotorVehicle();
MotorVehicle theCar = (MotorVehicle)car;    
Это явное приведение к более специализированному классу является противоположностью концепции восходящего преобразования, описанной ранее. Как таковой, он известен как downcasting.

Сокрытие имени

Сокрытие имен - это та же концепция, что и переопределение. При скрытии имени новая версия элемента или переменной создается в дочернем классе для замены или расширения родительской версии. Однако вместо использования ключевого слова override новое ключевое слово префиксирует объявление.

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

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

В следующем примере метод Motorvehicle Indicate заменяется с помощью скрытия имени, а не переопределения. Обратите внимание, что эффект вызова метода отличается в зависимости от типа данных переменной, в которой находится объект. Чтобы продемонстрировать, сначала измените метод индикации класса MotorVehicle следующим образом:

public new void Indicate(bool turningLeft)
{
    if (turningLeft)
        Console.WriteLine("Flashing left indicator");
    else
        Console.WriteLine("Flashing right indicator");
}
Теперь измените основной метод и выполните его, чтобы увидеть результаты. Вы можете видеть, что выходные данные отличаются в каждом вызове метода Indicate, что-то, что не происходит с переопределением метода:

Vehicle car1 = new MotorVehicle();
car1.Indicate(true);                        // "Turning left"
 
MotorVehicle car2 = new MotorVehicle();
car2.Indicate(true);                        // "Flashing left indicator"
Примечание: после тестирования приведенного выше примера, верните метод индикации для использования переопределения, а не нового.

Защищенная область

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

Другой модификатор доступа существует для использования в иерархиях наследования. Это модификатор защищенного доступа. Защищенный элемент обычно так же виден, как если бы он был частным, за исключением одного специального случая. Если элемент в базовом классе объявлен защищенным, он виден для его подклассов.

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

public int Mpg
{
    get
    {
        if (_speed == 0)
            return 0;
        else if (_speed < 20)
            return 50;
        else if (_speed < 50)
            return 40;
        else
            return 35;
    }
}
Если вы попытаетесь скомпилировать приложение, вы получите три ошибки компилятора. Каждый указывает, что переменная "speed" недоступна из-за ее уровня защиты. Другими словами, поскольку "_speed" объявлен в классе транспортного средства как частная переменная, он не может использоваться в подклассе MotorVehicle. Однако, если вы измените переменную в классе транспортного средства, подлежащего защите, она будет видна подклассу, и программа будет компилироваться правильно.

protected int _speed;        
Многоуровневые иерархии

Преимущества наследования не ограничиваются двухуровневыми иерархиями, которые были показаны в приведенных выше примерах. Можно добавить дополнительные подклассы для создания многоуровневого наследования. Это позволяет каждому наследнику добавлять более специализированную функциональность, сохраняя при этом членов всех своих родителей.

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

class Lorry : MotorVehicle
{
    private int _capacity;      /
 
    public int Capacity
    {
        get { return _capacity; }
        set { _capacity = value; }
    }
}
Поскольку класс грузовых автомобилей является производным от класса автотранспортных средств, который, в свою очередь, является подклассом транспортного средства, функциональные возможности всех трех этих классов будут доступны любому объекту грузовых автомобилей.

Предотвращение наследования

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

Для демонстрации измените объявление класса транспортного средства следующим образом:

sealed class Vehicle
Использование ключевого слова sealed предотвращает классы MotorVehicle и Bicycle от наследования функциональности от транспортного средства, поэтому приложение больше не компилируется. Вы также получаете предупреждение компилятора, подчеркивающее, что переменная "_speed" защищена. Это предупреждение появляется потому, что включение защищенной переменной в запечатанный класс предполагает, что была сделана ошибка. В любом случае защищенная переменная не может быть унаследована от запечатанного класса.

Заключение о событиях и наследовании

События, объявленные в базовом классе, по-прежнему доступны для подписки из производных классов. Однако производный класс не может вызвать событие, определенное в базовом классе. В предыдущей статье, описывающей события, было предложено создать централизованный метод, который вызывает событие, и этот метод должен быть помечен как виртуальный. Теперь вы можете видеть преимущество этого подхода, так как производный класс может вызывать этот метод и косвенно вызывать событие. Кроме того, подкласс может переопределить вызывающий событие метод, чтобы изменить способ вызова событий.
Автор этого материала - я - Пахолков Юрий. Я оказываю услуги по написанию программ на языках Java, C++, C# (а также консультирую по ним) и созданию сайтов. Работаю с сайтами на CMS OpenCart, WordPress, ModX и самописными. Кроме этого, работаю напрямую с JavaScript, PHP, CSS, HTML - то есть могу доработать ваш сайт или помочь с веб-программированием. Пишите сюда.

тегистатьи IT, си шарп, ООП




Отправляя сообщение я подтверждаю, что ознакомлен и согласен с политикой конфиденциальности данного сайта.



Уроки по C#
Урок 15. Конкатенация строк с переменными JavaScript
Урок 23. Введение в JCF Java