Опубликован: 25.03.2010 | Уровень: для всех | Доступ: платный
Лекция 9:

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

Применение виртуальных функций

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

А как бы сделать так, чтобы полномочия базовой ссылки автоматически устанавливались типом переданного родственного объекта. Есть такой механизм в объектно-ориентированных языках, который называется виртуальными функциями в наследовании. Его мы сейчас и рассмотрим применительно к C#.

Вернемся к нашему примеру. Посмотрим на члены в цепочке наследования классов Cylinder:Circle:Point, например на функцию Show(), которая есть в каждом слое. Для того, чтобы вызвать эту функцию из нужного слоя составного объекта с помощью адресующей этот объект ссылки, нам приходится объявлять ссылку, соответствующую типу этого слоя, присваивать ей адрес составного объекта и только потом через нее вызывать нужный член.

Есть более простой способ выполнить то же самое, если в базовом классе объявить функцию Show() виртуальной с помощью ключевого слова virtual, а в каждом классе-наследнике подтвердить новую версию этой функции с помощью ключевого слова override (переопределенная). После такого объявления компилятором будет создана таблица виртуальных функций. Она реализует механизм виртуальной адресации и позволит нам с помощью ссылки базового типа, которой будет присвоен адрес любого из объектов-родственников, вызывать переопределенные в этом объекте виртуальные функции. При этом явного (вручную) приведения ссылки базового типа к типу фактически адресуемого объекта не потребуется, среда исполнения сама извлечет из нужного слоя объекта подходящий член, определив его по типу самого объекта.

Одно важное условие нужно соблюдать при объявлении и переопределении виртуальных функций:

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

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

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

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

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

using System;
    
namespace Test
{
    class Point
    {
        string name = "Слой Point";
        public void Show0()
        {
            Console.WriteLine(name);
        }
    
        public virtual void Show()
        {
            Console.WriteLine(name);
        }
    }
    
    class Circle : Point
    {
        string name = "Слой Circle";
        new public void Show0()
        {
            Console.WriteLine(name);
        }
    
        public override void Show()
        {
            Console.WriteLine(name);
        }
    }
    
    class Cylinder : Circle
    {
        string name = "Слой Cylinder";
        new public void Show0()
        {
            Console.WriteLine(name);
        }
    
        public override void Show()
        {
            Console.WriteLine(name);
        }
    }
    
    // Вызывающая сторона
    class MyClass
    {
        public MyClass()
        {
            // Создаем объекты для всех типов 
            // иерархической цепочки наследования
            // и адресуем их базовой ссылкой
            Point[] point = {
                new Point(),
                new Circle(),
                new Cylinder()
            };
    
            // Распечатываем
            Console.WriteLine("Адресация к невиртуальной функции");
            for (int i = 0; i < point.Length; i++)
            {
                point[i].Show0();
            }
    
            Console.WriteLine("\nАдресация к виртуальной функции");
            for (int i = 0; i < point.Length; i++)
                point[i].Show();
    
            Console.WriteLine("\nТо же самое, только вручную");
            point[0].Show0();
            ((Circle)point[1]).Show0();
            ((Cylinder)point[2]).Show0();
        }
    }
    
    // Запуск
    class Program
    {
        static void Main()
        {
            // Настройка консоли
            Console.Title = "Механизм виртуальных функций";
            Console.ForegroundColor = ConsoleColor.White;
            Console.CursorVisible = false;
            Console.WindowWidth = 60;
            Console.WindowHeight = 10;
    
            new MyClass();// Чтобы сработал конструктор
    
            Console.ReadLine();
        }
    }
}
Листинг 9.14 . Иллюстрация механизма виртуальных функций

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

Результат, выдаваемый программой, будет таким


Максим Филатов
Максим Филатов

Прошел курс. Получил код Dreamspark. Ввожу код на сайте, пишет:

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

 

Как активировать код?

Денис Пашков
Денис Пашков
Россия
Татьяна Ковалюк
Татьяна Ковалюк
Украина, Киев, Киевский политехнический институт, 1974