Неформальное введение в объектно-ориентированное программирование на платформе Net.Framework

Продолжение. Начало в КГ №№ 13, 14, 16, 17, 20, 2324

Теперь давайте вместе прогуляемся по процессу создания объекта omon1. Там есть несколько любопытных моментов. На первом шаге вы оказываетесь в заголовке конструктора с параметром. Второй шаг отбросит вас вверх, на строчку объявления переменной internalName. Данная переменная в объекте Omonovez инициализируется при объявлении. Мы с вами дали команду на создание нового класса, и поэтому первым делом инициализируются подобным образом объявленные переменные, причем еще до выполнения любых конструкторов. Для того чтобы наблюдать происходящее подробнее, раскройте "плюсик" у объекта this в окне Autos. Обратите внимание: переменной присвоилось значение, указанное нами после знака "равно".

Следующий шаг перенесет нас на заголовок конструктора объекта по умолчанию — тот, что без параметров. Жмем F11 снова и оказываемся в заголовке конструктора с параметром базового объекта MVD_Worker. Уф! И вот только теперь у нас началось непосредственное создание объекта, мы начинаем возвратное движение по иерархии только что пройденных объектов. По мере того как мы совершаем следующие шаги, мы видим, как присваивается значение internalHasDocument во внутренностях конструктора MVD_Worker, затем мы прыгаем обратно, в тело конструктора Omonovez без параметров. У нас в этом конструкторе никаких действий не совершается. После него мы попадаем в конструктор с параметром Name, где переменной internalName данного экземпляра объекта присваивается значение "Федя". На этом создание нового объекта заканчивается, и мы возвращаемся к нашему тестовому коду.
Давайте подведем итог последовательности создания нового объекта. Прежде чем выполнится код, который вы вписали в метод — конструктор объекта, всем переменным присваиваются значения "инициализируемых при описании", затем по очереди вызываются конструкторы всех предков нашего объекта начиная с самого дальнего его предка. И только после того, как все конструкторы отработают, управление будет передано уже на ваш код конструктора текущего объекта. За счет этого механизма к тому моменту, когда вы соберетесь изменять то поведение унаследованного объекта, которое он имеет по умолчанию, сам объект-предок уже полностью инициализирован и готов принять ваши изменения.
В следующей конструкции нашего тестового кода мы пытаемся с помощью метода CheckDocument проверить у нашего омоновца документы. Давайте пройдем и этот код по шагам и посмотрим, что именно при этом происходит.
После нажатия F11 мы с вами сразу оказываемся во внутренностях метода CheckDocument() класса MVD_Worker. Это происходит потому, что наш класс Omonovez не имеет метода с таким именем. Не найдя подходящего метода в нашем классе, Net.Framework пытается отыскать его среди методов иерархии предков нашего объекта. 

Поиск подходящего метода начинается с непосредственного предка текущего объекта и постепенно углубляется вглубь по "дереву родословной", исследуя последовательно всех "пап", "дедушек", "прадедушек" etc. В нашем случае искомый метод находится у объекта MVD_Worker. Именно в нем мы и оказываемся.
Желтая полоса кода, индицирующая текущую выполняемую инструкцию, сейчас находится на фрагменте метода, считывающем значение со свойства HasDocument. 
Само описание свойства MVD_Worker.HasDocument находится на пару строчек выше.
Как вы думаете, где мы окажемся, если сейчас еще раз нажмем кнопку F11? Думаете, в свойстве MVD_Wor-ker.HasDocument? Ничего подобного! По кнопке F11 мы перейдем в код свойства Omonovez.HasDocument! Этот код немедленно выдаст нам true на наш запрос. Этот его ответ вернется обратно в метод CheckDocument объекта MVD_Worker, а тот, в свою очередь, вернет его нашему тестовому коду.
Если вы помните, свойство HasDocument в классе MVD_Worker было объявлено нами как virtual (перехватываемое). Таким образом, несмотря на то, что переменная omon1 была объявлена нами имеющей тип MVD_Worker, несмотря на наличие в объекте MVD_Worker свойства с именем HasDocument, мы с вами, тем не менее, окажемся во внутренностях свойства HasDocument нашего объекта Omonovez! Нажмите F11 и сами в этом убедитесь.
Обратите внимание вот на такой любопытный момент. Метод класса-предка MVD_Worker.CheckDocument только что, на наших глазах, вызвал метод своего собственного класса-потомка Omonovez.HasDocument, считал его значение и обработал его по своему собственному алгоритму. При этом объект MVD_Worker не только понятия не имеет о том, как именно реализован этот метод класса-потомка, но даже вообще не знает, существует ли в природе сам класс-потомок. Ему это просто безразлично. Если бы этот метод не был перехвачен в классе-потомке, отработало бы родное для этого класса свойство MVD_Worker.HasDocument, реализующее общее поведение для всех классов работников MVD.
Для того чтобы закрепить полученные знания и убедиться, что произошедшее не было случайностью, пройдите самостоятельно по шагам следующие две конструкции в коде:
MVD_Worker gai1 = new GIBDD (false);
hasDocument = gai1. CheckDocu-ment();

Несмотря на другой алгоритм проверки документов, метод базового класса CheckDocument() вполне корректно с ним работает, проверяя помимо удостоверения еще и права работника GAI.
На мой взгляд, описанный выше механизм — наиболее интересная с точки зрения практики возможность использования полиморфизма. Применяя ее в своем коде, мы можем в коде базовых классов описывать довольно замысловатые _общие_ алгоритмы по обработке данных. Таким общим механизмом может быть, например, последовательность, описывающая выстрел из табельного оружия. Прежде чем выстрелить, необходимо, скажем, проверить готовность оружия. Описываем в базовом классе виртуальное свойство GunIsReady, всегда возвращающее false. Почему так? А потому, что в самом базовом классе MVD_Worker не описано какое-либо конкретное оружие: откуда нам знать на этом этапе, как именно проверяется его готовность к стрельбе? Класс базовый, поэтому мы описываем только _основы_ стрельбы из оружия, до конкретной реализации которой нам на этом этапе программирования совершенно нет никакого дела. Так вот, описав виртуальное свойство готовности к стрельбе, давайте опишем столь же виртуальный метод, производящий выстрел. Назовем этот метод, скажем, MakeShoot(). В нашем базовом классе его тело пусто, по тем же самым вышеуказанным причинам. Ну вот, у нас все готово для того, чтобы начать реализовывать в базовом классе метод, описывающий стрельбу. Назовем его Shoot. Объявлять этот метод виртуальным или нет, решайте для себя сами. Я бы сделал его виртуальным, так как вполне возможно, что в будущем мне станут короткими описываемые его телом "штанишки" и захочется расширить их в классе-наследнике. Объявив этот метод, давайте запишем в нем базовый алгоритм для выстрела из произвольного оружия. Как он выглядит, вы можете посмотреть в файле примера, приведенного мной в начале статьи (MVD_Worker.Shoot).
Описав общие принципы стрельбы, давайте теперь перейдем к описанию конкретной ее реализации в классах-потомках. Для начала давайте научим стрелять класс омоновца. Для этого нам необходимо перехватить два метода, объявленные в базовом классе как виртуальные. Первый метод — проверка готовности оружия GunIsReady. Я не буду захламлять свой пример описанием конкретной реализации автомата Калашникова и методами, контролирующими его готовность к стрельбе. Если вы захотите поэкспериментировать, вы легко сделает это сами. Наш метод просто будет сразу возвращать true, сигнализируя таким образом о том, что оружие готово к стрельбе. Второй перехватываемый метод — MakeShoot — отвечает за сам процесс выстрела. Я также не стану реализовывать его в своем примере. Займитесь этим сами, в качестве домашнего задания. "Заглушку" метода я, тем не менее, создам, для того чтобы продемонстрировать вам, что он будет вызван в нужное время. Как выглядят оба перехваченных метода, вы можете посмотреть в текстах примера, которые я привел в начале статьи.
Обратите внимание: для нас нет никакой необходимости описывать в классе Omonovez процесс самого выстрела (Shoot). Пока нас удовлетворяет его алгоритм, мы можем пользоваться унаследованным от MVD_Worker механизмом. Это, в принципе, общее свойство любого объектно-ориентированного приложения. Наследуя класс, вам следует описать в своем классе-потомке лишь те механизмы, которые отличаются своей функциональностью от механизмов, описанных в классе-предке. Дублировать код, уже имеющийся в предке, не нужно, да это и считается плохой практикой.
Давайте пройдем по шагам вызов следующего фрагмента в нашем коде, в котором разные работники MVD производят выстрел.
omon1.Shoot();
gai1.Shoot();

Чтобы лишний раз не проходить уже рассмотренные нами методы тестового кода, поставьте точку остановки сразу на вызов функции omon1.Shoot(). После того как вы сделаете один шаг, вы окажетесь в теле метода MVD_Worker.Shoot. Следующий шаг перенесет вас в реализацию проверки готовности оружия Omonovez.GunIsReady. Выданное им true вернется обратно в класс MVD_Worker и позволит его оператору выбора if приступить к процессу самого выстрела MakeShoot. Как вы, наверно, догадались, в качестве MakeShoot будет вызван метод все того же класса Omonovez. Отработка метода завершилась, мы теперь переходим к отслеживанию выстрела работника GAI. Тут все происходит по-другому. Класс GIBDD не содержит своей реализации метода GunIsReady, поэтому для него отрабатывает метод имеющейся в базовом классе. Так как этот метод возвращает false, выстрел не производится.
Заметьте: мы никак не обрабатываем ситуацию отсутствия нужного нам метода в классе-потомке. Нам в этом просто нет никакой надобности, потому что никакой ошибки не может возникнуть в принципе. Если объект не обладает нужной функциональностью, он ей просто не обладает, и все тут. Это не ошибка, это всего лишь отсутствие функциональности, не приводящее к краху программы. Чувствуете разницу по сравнению с обычным, процедурным программированием?
Дабы еще раз обыграть вам достоинства полиморфизма, напишу еще один несложный пример. Давайте создадим еще два простых объекта. Первый называется Uprava и описывает управление, в котором работают наши сотрудники MVD_Worker. Второй объект — это автобус, в котором ездят на задание омоновцы. Дабы не заниматься лишней писаниной, давайте унаследуем оба объекта от уже имеющегося в Net.Framework класса ArrayList. Этот класс описывает динамический массив и имеет массу свойств и методов для работы с этим массивом. Описание наших классов может выглядеть так:

public class Uprava:System. Collections.ArrayList{}
public class AutoBus:System. Collections.ArrayList{}

Так как никакой функциональности к массиву мы не добавляем, то и тело объявленного класса можно оставить пустым. Конструкторы нам создавать также нет никакой необходимости. Фактически мы создали новые классы только для того, чтобы дать классу ArrayList более подходящее нашему применению имя типа. К слову, я настоятельно рекомендую вам всегда поступать подобным образом при создании специализированных экземпляров типов широкого назначения (в нашем случае массива), объявленных в пространстве имен Net.Framework. Во-первых, благодаря этому ваш код легче читать. Во-вторых, вы избегаете ошибок типа "поставить кофеварку на газовую плиту". Возможно, когда-нибудь в будущем вам придет в голову создать специальный класс "гаража", в котором будет стоять наш автобус. Так вот, метод "гаража"
void Add(Autobus avto1) 

добавляет в гараж именно автобусы, а вот метод
void Add(ArrayList avto1) 

поместит в гараж все что угодно, лишь бы оно было потомком ArrayList. Этим потомком может оказаться как магазин от "калашника", так и "управление внутренних дел" и даже сам гараж. В последнем случае вы наверняка получите нечто вроде черной дыры, а это вряд ли то, что вы планировали, создавая свой класс. Дабы избежать этой проблемы, всегда создавайте специализированного потомка базового класса для решения специальных задач.
Разобравшись с нюансами, давайте напишем несложную программу, работающую с этими новыми для нас классами.
Uprava upr1 = new Uprava();
AutoBus avto = new AutoBus();
for (int i=0;i<10;i++){
	upr1.Add(new Omonovez());
	}
			
bool hasDocument=false;
for (int i=0;i<10;i++){
	upr1.Add(new GIBDD(hasDocu-ment));

	hasDocument=!hasDocument;
	}
			
foreach(MVD_Worker wrk in upr1){
	if(wrk.HasDocument) avto. Add(wrk);
	} 
foreach(MVD_Worker wrk in avto){
	wrk.Shoot(); 
	}

Рассмотрим мой код более подробно. Сначала мы создаем по одному экземпляру классов Uprava и Autobus.
Затем в первом цикле мы "принимаем на работу" десять омоновцев.
Во втором цикле мы добавляем к ним 10 гаишников. Причем благодаря изменению переменной hasDocument в теле цикла пять человек из них имеют документы, а пять человек документов не имеют.
В третьем цикле мы перебираем всех работников нашего "первого управления" и сажаем в автобус только тех из них, кто имеет на руках документы.
В четвертом цикле они стреляют в потолок автобуса, кто как умеет. Точнее, омоновцы стреляют, а гаишники ничего не делают, так как им не выдали оружия.
Обратите внимание: мы нигде не выясняем, кто из них кто. Да и нам это совершенно неинтересно, так как вся нужная нам функциональность описана в их базовом классе MVD_Worker. Для решения поставленной задачи ее вполне достаточно.
А вот омоновцы у нас, тем не менее, стреляют в потолок, а работники GIBDD нет, да и работников GIBDD, не имеющих прав, в авто не пускают. Причем, повторю, никаких проверок на тип объекта или наличие прав у работников GIBDD в моем коде нет. Мы работаем с разными типами объектов как с объектами одного типа. Все происходит как бы само собой, волшебным образом. И благодарить за это волшебство мы должны только полиморфизм.

Модификатор NEW
Рассматривая тему полиморфизма, я чуть было не забыл об одном нюансе. В предыдущей статье я упоминал о таком кодовом слове, как "new", и говорил о том, что в случае использования полиморфизма работа new и override существенно отличается. Давайте изменим объявление метода GunIsReady в классе Omonovez c
protected override bool GunIsReady

на
protected new bool GunIsReady

и посмотрим, как это изменение скажется на работе вызова метода omon1.Shoot() в нашем тестовом коде. Переправляем объявление метода как было указано выше, ставим точку остановки на вызов omon1.Shoot() в нашем тестовом коде и запускаем отладку. По нажатию кнопки F11 мы попадаем, как и должно, во внутренности метода MVD_Worker.Shoot. Неожиданности начнутся, когда вы попытаетесь еще раз нажать F11. Вместо того чтобы "прыгнуть" в метод isGunReady класса Omonovez, вы окажетесь в методе isGunReady самого базового класса MVD_Worker. То есть полиморфизм перестанет работать. Вот вам и "два примерно одинаковых способа перехватить метод предка"! Теперь, когда мы с вами, надеюсь, разобрались с полиморфизмом, я вам могу пояснить и то, зачем потребовалось вводить в язык два способа перехвата методов.
Если вы в своем классе переопределяете некий родительский метод с модификатором new, то вы этим как бы разрываете полиморфную связь между своим объектом и его предком.
Для наглядного примера представьте себе кусок резинового шланга для поливки газонов. Мы с вами будем запускать в него с одной стороны шарик от подшипника, а затем с другой стороны будем его ловить. Говоря "new", мы с вами как бы перевязываем его примерно на половине длины узлом. Если мы теперь возьмем шарик и запустим его с одной стороны шланга, он докатится до узла и вернется обратно. Если же мы пустим шарик с другой стороны шланга, он опять докатится до узла в центре и снова вернется к нам обратно. Связь между двумя сторонами шланга окажется разорванной. При этом нарушается только способность шланга пропускать через себя шарики (да и воду) от подшипника. Шлангом он из-за этого быть не перестал и остался таким же резиновым, как был. Примерно так же работает и модификатор new, обрывая полиморфную связь между базовым классом и его наследниками на уровне метода, в котором объявлен, и не затрагивая несвязанных с ним других методов и свойств.
Честно говоря, я затрудняюсь так, с места придумать вам пример использования этого механизма по его прямому назначению. Разве что он пригодится в том случае, если некий базовый класс реализует неподходящую для вас функциональность.

Скажем, мы решили создать некую промежуточную прослойку между классом Omonovez и базовым классом MVD_Worker. В нем мы сконцентрируем функциональность, присущую всем бойцам ОМОНа. Впоследствии вы планируете развить из него иерархию, описывающую различные подразделения бойцов ОМОН. Да вот незадача! В нашем новом базовом классе, назовем его BaseOmonovez, vs предусмотрели совершенно другой, альтернативный способ стрельбы из табельного оружия. Тем не менее, из соображений единообразия мы решили назвать методы так же, как и методы базового для него класса MVD_Worker (isGunReady, MakeShoot и Shoot). В этом случае new окажет нам неоценимую услугу. Объявив все три этих метода с этим кодовым словом, мы сможем создать в нашем новом классе Omonovez новую идеологию стрельбы из оружия, обладающую, тем не менее, теми же самыми именами методов. Если нам при этом захочется воспользоваться алгоритмом, уже имеющимся в базовом классе, мы можем вызвать его с помощью кодового слова base.
Пример у меня получился несколько надуманным (честно говоря, я никогда не пользовался new в этом качестве), а вот вариантов использований побочных свойств механизма new я вам с места предложу сразу два. Оба они довольно востребованы в реальных практических задачах.
Во-первых, еще в прошлой статье я вам показал, как с его помощью можно перехватить метод класса-предка в том случае, если нужный вам метод не объявлен как виртуальный, а доступа к исходному коду класса у вас нет. Такая ситуация часто возникает когда вы пытаетесь унаследовать "чужой" класс, попавший к вам в виде откомпилированной DLL без исходников.
Во-вторых, new довольно полезна, когда вам необходимо изменить видимость какого-либо свойства или метода в базовом классе. Предположим, у нас имеется некий класс под названием Simple, в котором объявлен публичный метод под названием Method1.
class Simple{
public void Method1(){}
}

Мы из каких-либо соображений не хотим видеть этот метод публичным. Для нашего последующего кода было бы намного удобнее, если бы этот метод был приватным. Переправить объявление в самом классе Simple нельзя, так как существует уже несколько приложений, использующих этот класс, и, как назло, все они вызывают этот метод.
Напрашивающееся решение перехватить этот метод и объявить свою реализацию с нужным модификатором доступа приводит к ошибке времени компиляции. Что же нам делать? И тут нам на помощь и приходит new. Перехватить метод с новым модификатором в C# нельзя, а вот объявлять его заново с нужным модификатором с точки зрения языка вполне допустимо.
class Child:Simple{
private new void Method1(){}
}

Таким образом, у нас и волки сыты, и овцы целы. Своим вновь объявленным методом Method1() мы маскируем одноименный метод в базовом классе. Благодаря новому модификатору метод получается у нас private, а это именно то, чего мы и добивались.

Продолжение следует.

Герман Иванов


Компьютерная газета. Статья была опубликована в номере 26 за 2003 год в рубрике программирование :: разное

©1997-2024 Компьютерная газета