Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
C# - лекции IntUit (Биллиг В.А.).pdf
Скачиваний:
140
Добавлен:
13.02.2015
Размер:
4.13 Mб
Скачать

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

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

Вернемся к нашему примеру. Класс Found имел в своем составе метод Analysis. Его потомок класс Derived создает свой собственный метод анализа, скрывая метод родителя:

new public void Analysis()

{

base.Analysis(); Console.WriteLine("Сложный анализ");

}

Если модификатор new опустить, он добавится по умолчанию с выдачей предупреждающего сообщения о скрытии метода родителя. Как компилятор узнает, что в этой ситуации речь идет о новом методе? Причины понятны. С одной стороны, родительский метод не имеет модификаторов virtual или abstract, поэтому речь не идет о переопределении метода. С другой стороны, в родительском классе уже есть метод с данным именем и сигнатурой, и поскольку в классе не могут существовать два метода с одинаковой сигнатурой, то речь может идти только о новом методе класса, скрывающем родительский метод. Несмотря на "интеллект" транслятора, хороший стиль программирования требует явного указания модификатора new в подобных ситуациях.

Заметьте, потомок строит свой анализ на основе метода, наследованного от родителя, вызывая первым делом скрытый родительский метод.

Рассмотрим случай, когда потомок добавляет перегруженный метод. Вот пример, когда потомок класса Derived - класс ChildDerived создает свой метод анализа, изменяя сигнатуру метода Analysis:

public void Analysis(int level)

{

base.Analysis();

Console.WriteLine("Анализ глубины {0}", level);

}

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

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

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

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

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

в классе A1 объекта x1 действительно имеется метод M;

список фактических аргументов в точке вызова соответствует по числу и типам списку формальных аргументов метода M, заданного в классе A1.

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

Перейдем к рассмотрению связывания. Напомним, что в рассматриваемом семействе классов метод M полиморфен: имея одно и то же имя и сигнатуру, он существует в разных формах - для каждого класса задана собственная реализация метода. С другой стороны, из-за возможностей, предоставляемых односторонним присваиванием, в точке вызова неясно, с объектом какого класса семейства в данный момент связана сущность x1 (вызову мог предшествовать такой оператор присваивания if(B) x1 = xk;).

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

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

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

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

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

В языке C# принята следующая стратегия связывания. По умолчанию предполагается

статическое связывание. Для того чтобы выполнялось динамическое связывание, метод родительского класса должен снабжаться модификатором virtual или abstract, а его потомки должны иметь модификатор override.

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

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

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

объектов xk присваивание xi = xj допустимо для всех j >=i;

переопределение потомком метода, наследованного от родителя. Благодаря переопределению, в семействе классов существует совокупность полиморфных методов с одним именем и сигнатурой;

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

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

полиморфным методом, сам вызов - полиморфным вызовом.

Вернемся к нашему примеру с классами Found, Derived, ChildDerived. Напомню, в родительском классе определен виртуальный метод VirtMethod и переопределен виртуальный метод ToString родительского класса object. Потомок класса Found - класс Derived переопределяет эти методы:

public override void VirtMethod()

{

Console.WriteLine("Сын: " + this.ToString());

}

public override string ToString()

{

return(String.Format("поля: name = {0},

credit = {1},debet ={2}",name, credit, debet));

}

Потомок класса Derived - класс ChildDerived не создает новых полей. Поэтому он использует во многом методы родителя. Его конструктор состоит из вызова конструктора родителя:

public ChildDerived(string name, int cred, int deb):base (name,cred, deb) {}

Нет и переопределения метода Tostring, поскольку используется реализация родителя. А вот метод VirtMethod переопределяется:

public override void VirtMethod()

{

Console.WriteLine("внук: " + this.ToString());

}

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

public void Work()

{

VirtMethod();

NonVirtMethod();

Analysis();

}

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

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

public void NonVirtMethod()

{

Console.WriteLine ("Мать: "+ this.ToString());

}

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

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

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

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

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

Классы семейства с полиморфными методами уже созданы. Давайте теперь в клиентском классе Testing напишем метод, создающий объекты наших классов и вызывающий методы классов для объектов семейства:

public void TestFoundDerivedReal()

{

Found bs = new Found ("father", 777); Console.WriteLine("Объект bs вызывает методы класса Found");

Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]