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

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

Статья шестая. Полиморфизм.

  • Статья пятая. Механизм наследования
  • Отладка приложений в Visual Studio Net.

    В предыдущих статьях цикла, мы с вами, на примере класса MVD_Worker рассмотрели, как в классах C# выглядят такие конструкции языка как поля (переменные), свойства, методы и конструкторы. Также нами был рассмотрены два из трех столпов объектно-ориентированного программирования называющихся инкапсуляция и наследование. Мы поговорили и о переопределении методов класса предка в классе потомке. В качестве домашнего задания, я попросил вас поиграться несколько необычным примером кода.

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

    У нас на очереди третий столп объектно-ориентированного программирования, называющийся полиморфизм. Итак, если вы набирали код домашнего задания, то наверняка обратили внимание на странно выглядящее объявление типа объектов.


    MVD_Worker omon1 =new Omonovez("Федя",true,true);

    MVD_Worker gai1 =new GIBDD(false);


    Заметьте, мы объявляем переменную будущего объекта как имеющую тип MVD_Worker. Создаем - же мы экземпляр класса с помощью конструкторов классов Omonovez и GIBDD! То есть создаем экземпляр совершенно другого класса. На первый взгляд это выглядит как ошибка. Тем не менее, Visual Studio ничего не имеет против такой записи и успешно компилирует наш код. “Ошибка Майкрософт?” подумаете вы, “Нет” отвечу я, просто неизученная еще нами возможность.

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

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

    Итак, приступим. Все классы, как обычно, созданы в отдельных файлах. Имя файла совпадает с именем класса и имеет расширение “.cs”. Как создавать классы и располагать их в отдельных файлах я подробно рассказал в предыдущей статье и снова повторять не буду.

    Основным базовым классом нашего проекта является класс MVD_Worker, описываемый таким кодом:


    public class MVD_Worker

    {

    private bool internalHasDocument;

    public virtual bool HasDocument

    {

    get{return internalHasDocument;}

    }

    protected virtual bool GunIsReady

    {

    get{return false;}

    }

    protected virtual void MakeShoot()

    {

    }

    public virtual void Shoot()

    {

    if(GunIsReady){

    MakeShoot();

    }

    }

    public bool CheckDocument()

    {

    return HasDocument;

    }

    public MVD_Worker()

    {

    internalHasDocument=false;

    }

    public MVD_Worker( bool WorkerHasDocument)

    {

    internalHasDocument=WorkerHasDocument;

    }

    }


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

    Также у нас есть класс, потомок MVD_Worker, называющийся Omonovez. Выглядит он следующим образом:


    public class Omonovez:MVD_Worker

    {

    private string internalName = "Unnamed";

    public override bool HasDocument

    {

    get{return true;}

    }

    protected override bool GunIsReady

    {

    get{return true;}

    }

    protected override void MakeShoot()

    {

    // код выстрела.

    }

    public string Name

    {

    get{return internalName;}

    }

    public Omonovez():base(true)

    {

    }

    public Omonovez(string Name):this()

    {

    internalName=Name;

    }

    }


    От ранее использовавшегося нами класса, его отличает усеченный набор свойств, я оставил ему лишь имя. В качестве компенсации классу добавлено переопределение базового свойства HasDocument. Наш Omonovez обрабатывает его самостоятельно, сразу возвращая true. Так как свойства усечены, класс имеет всего два конструктора. Один базовый без параметров, и один принимающий в качестве своего параметра строчку с именем бойца. Для сокращения кода я использовал присваивание имени бойца по умолчанию сразу в момент объявления переменной.


    private string internalName = "Unnamed";


    Подобная техника вполне допустима и не считается неправильной в C#. Она имеет свои плюсы и минусы. Основной плюс очевиден - краткость записи. Основной минус заключается в том, что указанные таким образом присваивания неочевидно производятся в конструкторе объекта. На мой взгляд, неочевидностей в коде следует, по возможности, избегать. Такое присваивание возможно и не во всех случаях, например оно не сработает, если вы объявляете, таким образом, статическую(static) переменную. На мой взгляд, эти два способа инициализации переменных демонстрируют извечную борьбу между наглядностью кода и ленью программиста. Я сам также ленив и поэтому время от времени объявляю свойства и в такой, краткой форме.

    Следующий класс, участвующий в нашем проекте, это GIBDD, прямой потомок MVD_Worker. Основное его отличие от базового класса заключается в том, что он проверяет у работника ГИБДД не только наличие удостоверения, но еще и наличие прав на вождение автомобиля. Для того чтобы добиться такой функциональности, мы переопределили свойство hasDocument и все необходимые проверки делаем внутри него. Класс имеет всего один конструктор с параметром, указывающим есть ли права у данного гаишника. Базовый конструктор я не стал описывать намеренно. При его отсутствии нам не удаться создать экземпляр объекта, не присвоив ему явно наличие или отсутствие прав.

    Код класса выглядит вот так


    public class GIBDD:MVD_Worker

    {

    private bool internalHasPrava=false;

    public bool HasPrava

    {

    get{return internalHasPrava;}

    }

    public override bool HasDocument

    {

    get

    {

    if(base.HasDocument)

    {

    return internalHasPrava;

    }

    else{return false;}

    }

    }

    public GIBDD(bool hasPrava):base(true)

    {

    internalHasPrava=hasPrava;

    }

    }


    Помимо указанных классов в нашем проекте имеется и запускающий основной файл приложения. Оформите ли вы его как консольное приложение, набросаете на форму компонентов и сделаете приложение WinForms, это ваше личное дело. Единственное, что от вас требуется, это описать в своем коде процедуру, в которую вы будете вставлять впоследствии код, который я вас попрошу. Если вы создаете WinForms приложение, бросьте на форму кнопку и дважды по ней щелкните мышкой. Сформируется обработчик нажатия на эту кнопку. Он нам вполне подойдет. Если у вас консольное приложение, оформите удобным для вас образом процедуру и вызывайте ее из метода Main. Короче говоря, как вам удобно, так и сделайте... Готовы? Ок. Теперь давайте занесем в эту вашу процедуру код, работу которого мы с вами будем исследовать. Выглядит он так:


    bool hasDocument;

    MVD_Worker work1=new MVD_Worker();

    hasDocument = work1.CheckDocument();

    MVD_Worker omon1=new Omonovez("Федя");

    hasDocument = omon1. CheckDocument();

    MVD_Worker gai1 = new GIBDD(false);

    hasDocument = gai1. CheckDocument();

    omon1.Shoot();


    Обратите внимание, в коде отсутствуют какие-либо средства для вывода состояния объекта на экран. Мы с вами теперь не маленькие, уже изучили общие принципы отладки в Visual Studio и теперь можем без проблем посмотреть значение любых свойств с помощью ее встроенного отладчика. Захламлять же вывод нашей программы на экран, да еще специально писать для этого лишний код, нам больше нет никакой необходимости. Установите точку остановки на первую строчку нашего тестового кода (MVD_Worker work1=new MVD_Worker();) и запускайте приложение на исполнение с помощью команды "Debug->Start (F5)". Приложение стартует, вы жмете на кнопку, обработчиком нажатия которой является наш код, выполнение программы приостанавливается и перед вами снова появляется редактор с нашим тестовым кодом. Ваша точка остановки перекрасилась в желтый цвет. На ее красном кружочке слева, появилась желтая стрелочка, индицирующая инструкцию в коде, которую отладчик собрался выполнить. На всякий случай уточню не "выполнил", а "собрался выполнить". У вас все так? Если нет, вы что-то сделали неправильно. Перечитайте в этой статье главу озаглавленную "Отладка приложений в Visual Studio" еще разок.

    Давайте оглядимся вокруг. Помимо окна редактора, перед вами должны находится еще несколько окон. Нас интересуют описанные мной ранее "Autos", "Local" и "Watch". Если они по какой-то причине не появились, достаньте их самостоятельно. Для этого идете в пункт меню "Debug->Window" и щелкаете по соответствующим пунктам.

    Я создавал приложение WinForms и поэтому у меня в окне Autos имеется два пункта. this - ссылка на объект {WindowsApplication1.Form1}(это наша форма, в методе которой мы сейчас и находимся) и work1 помеченный как <undefined value>. Напротив this имеется плюсик, попробуйте по нему щелкнуть. "Оба-на!"- воскликнут любители поэкспериментировать. Раскроется дерево, содержащее свойства всех вложенных в нашу форму объектов, там есть и кнопка, по которой вы щелкали и список лога, если вы поленились его убрать с формы предыдущих примеров. Как я и рассказывал выше, вы можете посмотреть или изменить любое из этих свойств. К примеру, надпись на кнопке.

    Второй объект (work1) это наша переменная типа MVD_Worker в которую мы собираемся создать новый объект. Помечена она как <undefined value> потому, что этого еще не произошло. Сейчас переменная еще не содержит никаких объектов, поэтому ее значение на данный момент неопределенно. Давайте-ка, это исправим и приступим к пошаговому исполнению нашей программы. Нас интересует подробное изучение происходящего в нашем коде, поэтому мы воспользуемся командой "Step Into (F11)". Нажимаем F11 и оказываемся в теле конструктора объекта MVD_Worker без параметров. Нажимаем F11 повторно и курсор перемещается на инструкцию присваивающую переменной intHasDocument значение false. Обратите внимание, на значение этой переменной Нажимаем F11 еще раз, и курсор переместится на закрывающую скобку. В окне "Autos" мы можем проконтролировать значение переменной intHasDocument. Как и следовало ожидать, оно равняется false. Еще раз нажимаем F11 и нас возвращают обратно на строчку нашего тестового примера. Привычно нажимаем F11 еще раз, и желтый курсор переместится на следующую строчку кода, в которой мы пробуем присвоить значение переменной hasDocument с помощью метода


    work1.CheckDocument().


    Остановимся на минутку и изучим содержимое окна Autos. Так как мы с вами перешли на новую строчку, в которой упоминается переменная hasDocument, она появились в этом окне. Также объект work1 перестал быть <undefined value>, а стал {WindowsApplication1.MVD_Worker}. Напротив него появился плюсик, раскрыв его, мы можем посмотреть на то, чему сейчас равняются его свойства intHasDocument и hasDocument. Все указанные поля сейчас равны false. Ну что же, вполне ожидаемое поведение. Для тренировки, попробуйте самостоятельно пройти с помощью F11, следующую строчку кода. Ту самую, с присваиванием переменной hasDocument значения возвращаемого методом work1.CheckDocument(). Никаких неожиданностей вам встретится не должно. Вы, сначала, зайдете в тело метода CheckDocument(), потом из него прыгнете в метод get{}, возвращающий значение свойства, а затем вернетесь обратно, сначала в метод CheckDocument(), а затем и в наш тестовый код на строчку создания экземпляра объекта omon1, принеся "в клюве" значение свойства intHasDocument. Вот вы и получили наглядное подтверждение ваших знаний того, как работают свойства и методы классов.

    Теперь давайте вместе прогуляемся по процессу создания объекта 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_Worker.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. CheckDocument();


    Несмотря на другой алгоритм проверки документов, метод базового класса 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(hasDocument));

    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, вы предусмотрели совершенно другой, альтернативный способ стрельбы из табельного оружия. Тем не менее, из соображений единообразия, мы решили назвать методы также как и методы базового для него класса 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, а это именно то, чего мы и добивались.

  •  

    Ссылки:


    При перепечатке сохранение раздела "Ссылки" обязательно!!!