Опубликован: 02.12.2009 | Уровень: специалист | Доступ: свободно | ВУЗ: Тверской государственный университет
Лекция 5:

Отношения между классами. Клиенты и наследники

Статический контроль типов и динамическое связывание

Рассмотрим семейство классов A1, A2, ... An, связанных отношением наследования. Класс Ak+1 является прямым потомком класса Ak. Пусть создана последовательность объектов x1, x2, ... xn, где xk - объект класса Ak. Пусть в классе A1 создан метод M с модификатором virtual, переопределяемый всеми потомками, так что в рамках семейства классов метод M существует в n формах, каждая из которых задает реализацию метода, выбранную соответствующим потомком. Рассмотрим основную операцию, инициирующую объектные вычисления - вызов объектом метода класса:

x1.M(arg1, arg2, … argN)

Контролем типов называется проверка каждого вызова, удостоверяющая, что:

  • в классе A1 объекта x1 действительно имеется метод M ;
  • у метода М действительно N формальных аргументов;
  • список фактических аргументов в точке вызова соответствует по числу и типам списку формальных аргументов метода M, заданного в классе A1.

Язык C#, как и большинство других языков программирования, позволяет выполнить эту проверку еще на этапе компиляции и в случае нарушений выдать сообщение об ошибке. Контроль типов, выполняемый на этапе компиляции, называется статическим контролем типов. Языки программирования, называемые динамическими, например, язык Smalltalk, производят этот контроль динамически - в ходе работы программы непосредственно перед выполнением метода. Понятно, что ошибки, обнаруживаемые при динамическом контроле типов, трудно исправимы и потому приводят к более тяжелым последствиям. В таких случаях остается уповать на то, что система тщательно отлажена, иначе непонятно, что будет делать конечный пользователь, получивший сообщение о том, что вызываемого метода вообще нет в классе данного объекта. У динамических языков есть свои преимущества - прежде всего, простота.

Перейдем к рассмотрению связывания. И снова рассмотрим основную операцию - вызов

x1.M(arg1, arg2, … argN);

Предположим, что статический контроль типов для этого вызова успешно выполнен. Рассмотрим еще один аспект, связанный с этим вызовом. Вспомним, что x1 - это ссылка, которая связана с объектом, расположенным в динамической памяти. Всегда ли совпадает тип объекта и тип ссылки? Другими словами, всегда ли объект, созданный в динамической памяти, принадлежит классу A1? Ответ: нет, не всегда.

Предположим, что вызову метода M объектом x1 предшествует присваивание

x1 = y;

Это ссылочное присваивание, поскольку x1 - ссылка. Присваивание является допустимым, если объект y принадлежит классу, являющемуся потомком класса объекта x1. Таким образом, в точке вызова ссылка x1 может быть связана с объектом любого класса нашего семейства A1, …AN. В каждом из этих классов существует метод M с одной и той же сигнатурой. Метод M полиморфен: имея одно и то же имя и сигнатуру, он существует в разных формах - для каждого класса задана собственная реализация метода. Возникает естественный вопрос, какой же метод M следует вызывать - метод класса A1 (метод ссылки) или метод того класса, которому принадлежит реальный объект в динамической памяти (метод объекта). Возможны два решения этого вопроса, и оба варианта используются на практике.

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

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

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

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

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

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

Три механизма, обеспечивающие полиморфизм

Под полиморфизмом в ООП понимают способность одного и того же программного текста

x1.M(arg1, arg2, … argN);

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

  • Одностороннее присваивание объектов внутри семейства классов. Сущность, базовым классом которой является класс предка, можно связать с объектом любого из потомков. Другими словами, для введенной нами последовательности объектов xk присваивание xi = xj допустимо для всех j >=i.
  • Переопределение потомком метода, наследованного от родителя. Благодаря переопределению в семействе классов существует совокупность полиморфных методов с одинаковым именем и одинаковой сигнатурой, но разной реализацией.
  • Динамическое связывание, позволяющее в момент выполнения вызывать метод, принадлежащий объекту, с которым связана сущность в момент вызова.

В совокупности это и называется полиморфизмом семейства классов. Целевую сущность часто называют полиморфной сущностью, вызываемый метод - полиморфным методом, сам вызов - полиморфным вызовом.

Полиморфизм семейства классов Found и Derived

Вернемся к нашему примеру с классами Found и Derived. В классе Found определены два виртуальных метода. Один из них - виртуальный метод VirtMethod - определен в самом классе, другой - виртуальный метод ToString - наследован от родительского класса object и переопределен в классе Found. Потомок класса Found - класс Derived переопределяет оба метода, соблюдая контракт, заключаемый в этом случае между родителем и потомком. При переопределении виртуального метода сохраняется имя метода и его сигнатура, изменяется лишь реализация:

public override string ToString()
        {
            string s = "Поля: name = {0}, Basecredit = {1}" + 
                "credit = {2}, debit = {3}";
            return String.Format(s, name, base.credit, 
                credit, debit);
        }
        public override string VirtMethod()
        {
            return "Derived: " + this.ToString(); 
        }

В классе Found определены два не виртуальных метода NonVirtMethod и Job, наследуемые потомком Derived без всяких переопределений. Вы ошибаетесь, если думаете, что работа этих методов полностью определяется базовым классом Found. Полиморфизм делает их работу куда более интересной. Давайте рассмотрим в деталях работу метода Job:

public string Job()
        {
            string res = "";
            res += "VirtMethod: " + 
                VirtMethod() + NL;
            res += "NonVirtMethod: " +
                NonVirtMethod() + NL;
            res += "Parse: " +
                Parse() + NL;
            return res;
        }

При компиляции метода Job будет обнаружено, что вызываемый метод VirtMethod является виртуальным, поэтому для него будет применяться динамическое связывание. Это означает, что вопрос о вызове метода откладывается до момента, когда метод Job будет вызван объектом, связанным с x. Объект может принадлежать как классу Found, так и классам Derived и ChildDerived, и в зависимости от класса объекта и будет вызван метод этого класса.

Для вызываемых методов NonVirtMethod и Parse, не являющихся виртуальными, будет применено статическое связывание, так что метод Job всегда будет вызывать методы, принадлежащие классу Found. Однако и здесь не все просто. Метод NonVirtMethod

public string NonVirtMethod()
        {
            return "Found: " + this.ToString();
        }

в процессе своей работы вызывает виртуальный метод ToString. Опять-таки, для метода ToString будет применяться динамическое связывание, и в момент выполнения будет вызываться метод объекта, а не метод ссылки.

Что же касается метода Parse, определенного в каждом классе, то всегда в процессе работы Job будет вызываться только родительский метод разбора из-за стратегии статического связывания.

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

Класс Found, создающий метод Job, говорит примерно следующее: "Я предоставляю этот метод своим потомкам. Потомок, вызвавший этот метод, должен иметь VirtMethod, выполняющий специфическую для потомка часть работы; конечно, потомок может воспользоваться и моей реализацией, но допустима и его собственная реализация. Затем часть работы выполняю я сам, но выдача информации об объекте определяется самим объектом. Заключительную часть работы, связанную с анализом, я потомкам не доверяю и делаю ее сам".

Пример работы с полиморфным семейством классов

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

Теперь в статике уже нельзя предсказать, как будут выполняться методы этих классов. На рис. 4.7 показана работа с объектами этих классов.

Полиморфизм семейства классов Found и Derived

Рис. 4.7. Полиморфизм семейства классов Found и Derived

В окне результатов отражены данные, полученные в результате вызова метода Job объектами этих классов. Несмотря на то, что метод Job, определенный в классе Found, не является виртуальным методом, потомок Derived наследовал этот метод без всякого его изменения, результаты выполнения для обоих объектов различны. Причина проста: не виртуальный метод родителя содержит вызовы виртуальных методов. Этого достаточно для включения всей мощи механизма полиморфизма.

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

Федор Антонов
Федор Антонов

Здравствуйте!

Записался на ваш курс, но не понимаю как произвести оплату.

Надо ли писать заявление и, если да, то куда отправлять?

как я получу диплом о профессиональной переподготовке?

Илья Ардов
Илья Ардов

Добрый день!

Я записан на программу. Куда высылать договор и диплом?