Неформальное введение в объектно-ориентированное программирование на платформе Net.Framework.Герман Иванов. Статья пятая. Механизм наследованияВ предыдущей статье, мы с вами, на примере класса MVD_Worker рассмотрели, как в классах C# выглядят такие конструкции языка как поля (переменные), свойства, методы и конструкторы. Также нами был рассмотрен один из столпов объектно-ориентированного программирования называющийся инкапсуляция. Эту статью я посвящу второму столпу ООП, называется этот столп - наследование. Механизм наследования был введен в ООП для того, чтобы избежать многократного дублирования в тексте вашей программы одних и тех же свойств и методов. Помните, мы в прошлой статье создавали класс Omonovez имеющий, в отличие от обычного класса MVD_Worker, еще и каску с автоматом? Заметьте, помимо этих отличных от обычного работника МВД атрибутов, он, наравне с ними, также обладает удостоверением работника МВД. Так зачем дважды описывать в коде один и тот же вид удостоверения? Давайте просто унаследуем наш новый класс Omonovez от класса MVD_Worker, таким образом, получая всю функциональность (и атрибутику) работника МВД и расширив ее новыми возможностями, присущими только омоновцу. Запускайте Visual Studio и создайте новый проект с помощью пункта меню “File-> New -> Project”. Выбираете тип проекта “Visual C# project”. В правом списке указываем, что мы желаем создать “Windows Application”, внизу подправляем названия папки и нового проекта, на те, которые вам удобны. После того, как Visual Studio пошуршит винчестером и создаст вам заготовку нового проекта, сходите в пункт меню “View” и попросите вывести вам на экран “Toolbox” и “Properties Window”. C появившегося “ToolBox” перетащите на главную форму приложения компонент ListBox. В окне “Properties” найдите свойство “(Name)” и впишите вместо указанного по умолчанию “ListBox1” новое название нашего списка “log”. Также отыщите в этом же окне свойство “Dock”. Когда вы щелкните мышкой по стрелочке смены его значения, вниз выпадет мини-мастер, позволяющий, в наглядной форме, указать, как именно вы хотите видеть расположения своего списка на форме. Поиграйтесь разными значениями, для того чтобы понять, что именно он вам предлагает. Затем укажите, тот из вариантов, который вам больше понравится. Затем с тулбара перетаскиваем на форму кнопку(Button). Укажите ей имя (Name) равным Go, а в поле Text напишите "Старт". Расположите кнопку и список на форме эстетичным на ваш взгляд образом. Уф! Ну вот, мы с вами подготовили шаблон нашего будущего приложения и готовы приступить к изучению понятия наследования. Сначала нам следует описать базовый класс. По прямо таки навязываемому средой Visual Studio стилю программирования, каждый класс следует создавать в своем собственном файле. Впрочем, я напрасно ехидничаю это совершенно правильный подход. При такой системе, вы можете, при желании, скомпилировать свой класс в отдельную библиотеку(DLL). В дальнейшем, создавая очередной проект, вы можете пользоваться только этой, заранее скомпилированной версией, вовсе не подключая к проекту исходные коды класса. Для этого вам нужно лишь сослаться на свою библиотеку в разделе References нужного проекта. Заведите где-либо на жестком диске папку, в которой вы и будете накапливать свои собственные DLL. Через некоторое время вы заметите, как мало нового кода вы пишете в своих приложениях. Хорошо спроектированные классы, скомпилированные в отдельную библиотеку, позволяют существенно упростить вашу будущую работу. С другой стороны, никто не запрещает Вам подключить файл с исходным кодом класса к своему текущему рабочему проекту и компилировать весь проект в один файл без всяких дополнительных DLL. Выбор остается за вами. Если же вы, наперекор Visual Studio, создаете много разных классов в одном файле, то каждый раз, когда вам потребуется лишь один из них, вам придется подключать _все_ классы, которые описаны в этом файле. Поэтому, вслед за Visual Studio, я также постараюсь приучить вас к несложному правилу гласящему “один класс в одном файле”. Итак, приступаем к созданию нового класса. Выбираем в меню пункт “Project -> AddClass”. Перед нами появляется мастер создания нового класса. Укажите в нем, что вы хотите создать “Class” (этот пункт итак уже выбран по умолчанию), переправьте, в поле ввода внизу, название файла на MVD_Worker.cs и нажмите кнопку “Open”. Мастер сгенерит для вас новый файл, а в нем заготовку для нашего будущего класса MVD_Worker. Вы еще не забыли о том, что содержимое этой заготовки можно, при желании, полностью подстроить под себя? Ее шаблон находится в файле "C:\ Program Files \ Microsoft Visual Studio.NET 2003 \ VC# \ VC#Wizards \ CSharpAddClassWiz \ Templates \ 1033 \ NewCSharpFile.cs". Как вы видите, несмотря на большую длину в пути, название файла с шаблоном довольно прозрачно для понимания. Вы его легко найдете, а вместе с ним и шаблоны для других мастеров используемых Visual Studio при самостоятельной генерации кода. Понимание текста заготовки класса, сгенерированного мастером, не должно вызвать у вас проблем. Все что он описал в коде, хорошо вам знакомо по прошлым моим статьям. Дополнительного пояснения требует, на мой взгляд, лишь понятие namespace (пространство имен).Пример наследованияМы рассмотрели все то, что было упущено в моих предыдущих статьях и надеюсь теперь код, сгенерированный мастером нового класса Visual Studio для вас совершенно понятен. Давайте приступим к его модификации. Не трогая директиву namespace, расширяем код класса MVD_Worker. Добавляем в него поля и свойства, описывающие наличие документа у нашего гипотетического работника. Создаем пару конструкторов, умалчиваемый без параметров и еще один, принимающий один параметр, в котором мы указываем, имеется ли у вновь создаваемого работника удостоверение или нет.
Вот и все. Наш базовый класс готов. Для того чтобы красиво отформатировать написанный вами код, выберите его в блок и используйте комбинацию клавиш Ctrl-K, Ctrl-F. Ну вот, теперь у нас все чисто и аккуратно. С помощью Ctrl-S сохраняем файл и приступаем к созданию класса потомка. Опять вызываем мастер нового класса "Project->Add Class". Говорим, что хотим создать файл по имени Omonovez.cs. Мастер создает нам заготовку нового класса, и мы приступаем к ее модификации. Видите строчку public class Omonovez? Давайте приведем ее к следующему виду:
То есть, к уже имеющемуся коду мы добавили двоеточие и имя нашего класса MVD_Worker. С помощью двоеточия и следующего за ним имени класса предка и обозначается наследование этим классом-потомком свойств и методов класса-родителя. Весь остальной код класса пока оставляем без изменения. Что же нам дало это обсуждаемое наследование? Давайте глянем на его работу с помощью простой демонстрации. Выделите в редакторе файл с главной формой вашего приложения (тот самый на который мы вставляли список и кнопку). Дважды щелкните по кнопке. В ответ среда сгенерирует вам прототип для метода обработчика нажатия на эту конкретную кнопку. Приводим метод-обработчик к вот такому виду:
Запускаем программу на исполнение с помощью кнопки F5 и если вы все сделали правильно, перед вами появится окно Windows приложения с нашей формой, списком и кнопкой. Щелкаем в этом окне по кнопке и в списке появятся две строчки текста:
Обратите внимание, несмотря на то, что мы с вами не описывали свойство HasDocument в классе Omonovez оно там, тем не менее, присутствует и исправно работает! Появилось оно в нашем новом классе, как раз за счет механизма наследования. Все методы, поля и свойства "класса-предка", объявленные как публичные (public) доступны "классу-потомку". Вы можете использовать их в своем классе-потомке так, как вам заблагорассудится. Давайте посмотрим их использование на еще одном примере. Давайте изменим поведение нашего класса Omonovez. Пусть экземпляр этого класса всегда носит при себе удостоверение. Откройте на редактирование файл Omonovez.cs и внесите в его конструктор следующую строчку: HasDocument=true; (вы же уже теперь знаете где у объекта находится конструктор, не так ли?). Ну и как попробовали? Что не получается? Правильно, ошибка - "Данное свойство ReadOnly изменять его нельзя!". Так что нам делать в такой ситуации? Первое решение, которое сразу приходит в голову, это просто объявить переменную internalHasDocument, ту самую, которая объявлена в классе MVD_Worker, не с модификатором private, а с модификатором protected и обращаться к ней напрямую из конструктора класса Omonovez. Постойте! Я же вам еще ничего не рассказывал о том, что это вообще за модификатор такой protected и зачем он нужен. Давайте, я воспользуюсь случаем, и исправлю это упущение. Как вы уже знаете, переменная объявленная как private доступна только внутри того класса, где она объявлена. Попытка обратится к ней "извне" приводит к ошибке времени компиляции. Придумано это для того, чтобы реализовать понятие инкапсуляции. Подразумевается, что для модификации такой переменной у вас в классе должно быть объявлено публичное свойство или метод. На первый взгляд, тут все хорошо задумано, но в некоторых ситуациях этот механизм работает крайне неэффективно. Предположим мы описали переменную, описывающую количество патронов в автомате нашего бойца. При добавлении патронов, мы для начала выясняем, а есть ли у омоновца вообще автомат, иначе и заряжать то ему по крупному счету нечего. Затем проверяем, не превышает ли количество заряжаемых патронов вместимость магазина. Также попутно убеждаемся, что автомат не заклинило, так как в этом случае зарядить его невозможно. Заклинивает автомат тогда, когда мы пытаемся затолкать в него больше патронов, чем влезает в его магазин. Заклинивший автомат выдает пятисекундную задержку при попытке выяснить количество имеющихся в нем патронов или при попытке его перезарядить. Для того чтобы починить автомат необходимо вынуть из него все патроны. Фрагмент кода, реализующий подобный алгоритм, может выглядеть примерно так.
Через некоторое количество времени работы с классом, нас достали постоянные заклинивания автомата. Мы создаем класс потомок и в его конструкторе заполняем магазин автомата по одному патрону, сразу проверяя их качество и правильность их установки в магазин. То есть заклинить автомат у нас просто физически не может. Вот тут то мы и наступаем на неудобство “закрытости” переменной internalPatronCount. В тот момент, когда мы добавляем очередной патрон, у нас каждый раз отрабатывают методы get{} и set{} проверяющие в своем теле, а есть ли у омоновца автомат, а не заклинило ли его … ну и так далее по тексту программы. Нам эти проверки на данном участке кода вовсе не нужны. Мы заведомо знаем, что автомат не заклинит, что мы добавляем патроны по одному, но избавится от проверки, тем не менее, никак нельзя. Я взял достаточно простой пример, но представьте себе ситуацию, при которой вы добавляете полмиллиона записей в базу данных и при этом после добавления каждой из них, методом set{} производится, к примеру, заблаговременно предусмотренное программистом класса, услужливое сжатие базы данных. Так что же делать в такой ситуации? Тут на помощь и приходит модификатор доступа protected. В большинстве случаев переменная, объявленная как protected, ведет себя как private переменная. Точно также она недоступна внешнему коду, использующему наш класс, и точно также к ней может свободно обращаться код изнутри того класса, в котором она объявлена. Но в отличие от private переменных, переменная объявленная как protected свободно доступна и всем потомкам класса, в котором она объявлена. Из кода класса-потомка вы можете напрямую изменять ее значение, не пользуясь для этого свойством, а вот все остальные классы в вашем проекте могут с ней работать только через свойство. Итак, с назначением модификатора protected мы вроде разобрались, теперь попробуйте исправить объявление переменной internalHasDocument в классе MVD_Worker следующим образом:
а в конструкторе Omonovez вписать вот что:
Теперь у нас все получилось. После нажатия кнопки на форме нашего приложения ListBox log выводит:
Вот что, еще раз обратите внимание на такой нюанс. Мы создаем два разных объекта, и они по умолчанию обладают совершенно разными свойствами. В первом случае у нас создается экземпляр MVD_Worker, который ничего не знает о существовании своего потомка Omonovez. Из-за этого его незнания изменения алгоритма в этом классе-потомке, никак не влияют на алгоритм работы самого класса-предка. Во втором случае мы создаем производный от MVD_Worker класс Omonovez, который широко пользуется функциональностью своего класса родителя, поэтому изменение алгоритма класса предка сказывается на всех производных от него классах потомках. Когда вы проектируете базовый класс-предок, хорошенько подумайте о том, что именно должно в него входить и по возможности сделайте этот класс как можно более простым. Ну что, подведем итог. Проблему то мы с вами решили, но как-то некрасиво. Получается, что классы потомки свободно обращаются с полями класса предка так, как их левая нога захочет. А как же наш первый столп инкапсуляция? Ведь класс предок запросто может иметь свои собственные сложные алгоритмы действий, срабатывающие во время присвоения свойства hasDocument? Непорядок. Обратите внимание, у нас в классе MVD_Worker имеется конструктор, принимающий в качестве параметра bool значение. Это значение указывает можно ли давать этому работнику в руки документы или нет. Давайте-ка и воспользуемся этим конструктором. Переправляйте переменную internalHasDocument в классе MVD_Worker обратно с protected на private, а конструктор класса Omonovez давайте оформим следующим образом:
Вам наверняка сразу бросилось в глаза новая форма записи конструктора, очень похожая на объявление класса с предком. Похожесть эта не случайна. В данном случае мы указываем, что при вызове нашего конструктора, следует обратиться к конкретному конструктору предка. А именно конструктору, принимающему параметр bool, и передать ему значение true. Наш конструктор как бы "унаследует" указанный конструктор предка. Прежде чем код, в конструкторе нашего нового класса, получит управление, отрабатывает конструктор класса предка, имеющий параметр в виде значения типа bool. О механизме поиска средой Net.Framework такого конструктора я вам рассказывал в предыдущей статье, когда говорил о перегрузке операторов. На самом деле, даже тогда, когда мы с вами вовсе не указываем тип конструктора предка, а просто пишем public Omonovez(){…} Net.Framework разворачивает эту запись в public Omonovez():base(){…}. То есть, вызывает дефолтовый конструктор объекта предка, не имеющий никаких параметров. Как вы помните из предыдущих статей, такой конструктор всегда есть, даже тогда, когда мы с вами его явно не описали. Откуда он берется? Помните, я вам говорил о том, что все типы данных в Net.Framework унаследованы, в конце концов, от одного единственного класса по имени Object? Вот его конструктор без параметров и вызывается в том случае, если у вашего класса (или его предков) такой конструктор не объявлен. С помощью явного указания нужного нам конструктора мы можем изменить порядок действий Net.Framework по умолчанию, и вызвать тот конкретный конструктор, который нам нужен. Попробуйте запустить на исполнения нашу новую версию программы. Ну вот - все получилось! У wrk документов нет, у boez они есть и при этом мы не нарушили инкапсуляцию в объекте MVD_Worker. Сплошная идиллия. Помимо слова base, означающего вызов конструктора предка, вы можете указать также слово this означающее вызов другого конструктора, этого же самого объекта. Зачем это может потребоваться? Давайте расширим наш класс Omonovez и дадим ему на голову каску, а в руки автомат. Приведите код класса к вот такому виду:
Обратите внимание на то, каким образом у меня тут оформлены конструкторы. Во-первых, имеется конструктор без параметров, в свою очередь вызывающий базовый конструктор предка с указанием дать в руки бойцу документы. Второй конструктор предварительно вызывает предыдущий конструктор с помощью ссылки this(), а затем присваивает бойцу имя. Третий конструктор спихивает установку имени предыдущему конструктору, вызывая this(Name), а затем, от себя, выдает ему каску. Четвертый конструктор доверяет присвоение имени и выдачу каски третьему конструктору, через вызов this(Name,Kaska), а сам додает ему автомат. Таким образом, если нам, когда-либо впоследствии, потребуется, скажем, изменить алгоритм одевания на бойца каски, мы будем менять код только в одном единственном месте, а именно в третьем конструкторе. И это наше изменение никак не затронет остальной алгоритм. Происходит так потому, что каждый конструктор выполняет только тот фрагмент кода, в котором поведение объекта _отличается_ от поведения задаваемого другими конструкторами. Вся же остальная работа доверятся тому, кто ее умеет хорошо делать, а именно другим специализированным конструкторам. Если вы самостоятельно рассматривали пространство имен Net.Framework, то наверняка обратили внимание на то, что большинство входящих в него классов имеет по несколько конструкторов, отличающихся числом параметров. Так скажем System.Data.DataColumn можно создать пятью разными способами. Мне кажется, что вместо того чтобы пять раз описывать в коде присваивание одних и тех же переменных, намного проще использовать данную технику. Думаю, разработчики Net.Framework так и поступили. Иначе можно только позавидовать их терпению, ибо некоторые объекты имеют более 20 разных конструкторов! Давайте протестируем наш новый объект Omonovez, исправьте код, выполняющийся при нажатии кнопки Go, следующим образом:
Ну вот, если вы все сделали правильно, у вас получилось подразделение из 4 бойцов обладающих именем, каской и автоматом в самых разных наборах из этих свойств. Говоря о наследовании, мы с вами не рассмотрели еще такую важную возможность ООП как переопределение методов. Суть идеи сводится вот к чему. Когда вы создаете базовый класс, вы задаете в нем какую-либо функциональность. Мы в нашем примере, в качестве такой функциональности задали наличие документа и способ его получения. Для этого мы воспользовались private переменной internalHasDocument и Read-Only bool свойством HasDocument. Прошло время, и нам стало не хватать такой простой функциональности в некоторых из его классов-потомков. Скажем, мы создали класс GIBDD, и тут выяснилось, что помимо обычного удостоверения работника MVD он еще должен носить при себе права на управление автомобилем. Экая невидаль! - воскликнем мы и по накатанной дорожке, создаем private переменную internalHasPrava и свойство HasPrava для его чтения.
Но постойте! Некрасиво как-то получается. Выходит, у всех нормальных работников МВД мы проверяем только свойство HasDocument, а у гаишников приходится проверять еще одно дополнительное свойство? Ну ладно, мы с вами издалека отличим работника ГИБДД от омоновца, а программе-то как их различать? У каждого встречного класса спрашивать - “ты кто - гаишник или омоновец?” К слову метод для этого у всех классов Net.Framework имеется, наследуется он от Object и вызывается как класс.GetType(). Возвращает тип класса. Если вы в предыдущем примере, после boez4.Inventar(log) добавите еще одну строчку кода:
То в конце лога у вас появится строчка - “ваше_namespace.Omonovez”. Сравнив ее со строчкой “ ваше_namespace.GIBDD” мы можем понять, нужно ли дополнительно проверять права у данного работника MVD. Но это совсем уж дремучий и неправильный способ. Намного корректнее было бы воспользоваться для этой цели оператором “is”. Этот оператор сравнивает объект с указанным типом и возвращает true, если объект принадлежит к этому типу или же унаследован от него. Например:
Вот такой код, с точки зрения программирования под Net.Framework, более правилен. Но он, тем не менее, все равно неудобен. И что, нам так каждый раз придется развлекаться, прежде чем проверить у работника MVD документы? К счастью есть другой способ. И даже не один, а как принято в Net.Framework как минимум два. Первый способ требует небольшой модификации исходного класса MVD_Worker. Отыскиваем в нем строчку объявления свойства HasDocument и приводим ее к вот такому виду:
Новым в этом объявлении является только модификатор virtual. Предназначен он для того, чтобы указать, что данный метод может быть, теоретически, перехвачен в классе потомке. Я не зря написал “теоретически”, если вы его не перехватываете, он работает как обычно. Поэтому, если вы создаете новый класс и вы считаете, что некоторые из его свойств или методов могут быть в дальнейшем перехвачены, смело ставьте virtual. Многие гуру ООП не согласятся со мной в этом мнении, дело в том, что виртуальные методы замедляют вашу программу. Но, на мой взгляд, этот вопрос носит скорее теоретический характер и в большинстве реальных программ столь незначительной потерей быстродействия можно пренебречь. Итак, как объявить метод, который можно перехватить, мы знаем, теперь давайте посмотрим, как нам этим методом воспользоваться. А довольно легко! Объявляем в нашем классе-потомке вот такое свойство:
Для того чтобы перехватить виртуальный метод предка, следует в классе потомке объявить метод с тем же именем и тем же набором параметров как у перехватываемого нами метода и указать модификатор override. Вот и все. Изнутри этого своего метода вы можете обратиться к исходному методу в классе-предке с помощью кодового слова base. Я рекомендую вам, перехватив метод предка, всегда вызывать его исходный метод из своего кода, бог его знает, какую функциональность он внутри себя реализует. Пускай он сделает свои дела, а мы добавим потом свои. Даже в нашем несложном примере чтения свойства HasDocument вполне могло оказаться, что родительский класс MVD_Worker, к примеру, подсчитывает, сколько раз каждый работник показывал свое удостоверение. В зависимости от числа показов он начисляет им зарплату. Если мы не вызовем свойство предка, он не сможете реализовать этот алгоритм. Зачем же нам обижать работников этого славного ведомства! Всегда, перехватив метод предка, на всякий случай вызовите оригинальный метод из своего кода. В нашем случае я воспользовался значением, полученным от предка, для того, чтобы выяснить есть ли у нашего работника удостоверение МВД, а затем дополнительно проверил, а есть ли у него права. Таким образом, наш класс GIBDD совершенно корректно обрабатывает, стандартный для всех работников MVD, метод HasDocument и нам теперь нет никакой необходимости как-то отдельно обрабатывать работников ГАИ при проверке документов. Давайте проверим, сработает ли наш код или нет. Приведите обработчик нажатия кнопки Go на нашей форме к вот такому виду:
После запуска программа выведет две строчки, False и True. Это означает, что метод HasDocument правильно реагирует на наличие прав у работника ГАИ. Вторым способом перехватить метод предка является использование ключевого слова new. Используется оно в основном тогда, когда нужный вам метод предка не объявлен как virtual, а исходный код класса-предка вам недоступен. Например, попал к вам в виде откомпилированной кем-то DLL. Для того чтобы перехватить такой метод впишите вместо слова override слово new и все. Теперь вы сможете по-своему обрабатывать и такие, заранее не объявленные как virtual, методы. Вы спросите, а зачем потребовалось вводить два разных способа перехвата методов, когда хватило бы и одного new? Дело в том, что эти два способа работают немного по-разному. Эту разницу мы с вами обсудим в следующей статье, когда вплотную подберемся к третьему, самому интересному, столпу ООП называемому полиморфизм.А пока, в качестве домашнего задания, попробуйте откомпилировать вот такой обработчик кнопки Go и попытаться понять, что именно происходит в этом моем примере. Немалую помощь в понимании происходящего вы сможете получить от Visual Studio, если пройдете этот код в ее отладчике “по шагам”.
Обратите внимание и на то, что в случае с new и в случае с override результаты получаются совершенно разные. | |
Ссылки:
|