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

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


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

В качестве предисловия сообщу вам новость, если вы ее до сих пор не знаете. "Майкрософт" официально объявила о выходе релиза Visual Studio 2003 Everett и пакета Net.Framework 1.1. Волна официальных презентаций этого нового продукта, совмещенных с представлением операционной системы Windows Server 2003, катится по крупным городам территории экс-СССР. В Санкт-Петербурге это будет происходить 25 мая этого года. Я на этом "Представлении" ((с) Московское представительство "Майкрософт") обязательно побываю и расскажу вам, что же именно там происходило.
К тому времени, когда эта моя статья выйдет в печать, вы сможете приобрести себе экземпляр этой новой среды разработки "Майкрософт". Сам пакет Net.Framework 1.1 можно свободно скачать с его домашней страницы (http://msdn.microsoft.com/downloads/list/netdevframework.asp? frame=true) или получить через механизм Windows Update.

Поэтому я перестаю мучить вас консольными приложениями и командными строками. В дальнейшем я собираюсь пользоваться в своих примерах только Visual Studio 2003 Everett. Надеюсь, вы последуете моему примеру и также перейдете на этот новый продукт "Майкрософт" или уже пользуетесь предыдущей версией среды, Visual Studio Net. Те же из читателей, кто до сих пор пользуется компиляторами командной строки, наверняка из предыдущих моих статей получили достаточно сведений для того, чтобы самостоятельно сообразить, что и где следует набирать для того, чтобы откомпилировать мои примеры.
В предыдущей статье мы с вами на примере класса 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 (пространство имен):

namespace xxxxx {
...
}

где xxxxxx — название вашего проекта. Этот блок кода ограничивает весь ваш вновь созданный с помощью мастера класс.
Пространство имен используется в Net.Framework для того, чтобы объединять в единое целое блоки кода. Объединяются они в рамках какого-либо одного вашего проекта или по признаку выполнения сходной функциональности в иерархии Net.Framework.
За счет использования пространства имен у программиста не болит голова о том, а не имеется ли случаем где-либо в недрах Net.Framework объект, который уже называется точно так же как тот, новый, который он собрался создать. По своей сути пространство имен — это нечто вроде обычной папки Windows, в которую вложены файлы (в которых хранятся различные классы) и другие папки (вложенные пространства имен). Давайте продолжим эту аналогию и посмотрим, насколько она удачна.
У вас на винчестере запросто могут быть две совершенно разных папки, в которых находятся одинаково называющиеся файлы, а вот создать два файла с одинаковым именем в одной папке нельзя. Так же дело обстоит и в Net.Framework. В разных пространствах имен могут находиться классы, имеющие одинаковые имена, а в одном пространстве имен такие классы создавать нельзя.
Папки Windows помогают упорядочивать файлы на вашем диске. Если мы видим на диске C: папку под названием Мои документы, то вполне логично можем предположить, что в ней хранятся документы, созданные пользователем. Так и в Net.Framework: если мы видим, скажем, пространство имен System.Text, то вполне верно предполагаем, что там находятся классы, предназначенные для обработки текстов.
Когда вы хотите в Windows указать путь к файлу сквозь последовательность вложенных папок, вы пользуетесь символом "\". К примеру, путь к файлу sysoc.inf, находящемуся в папке System32, в свою очередь, находящейся в папке Windows, расположенной в корне диска C:, записывается как c:\windows\system32\sysoc.inf. В Net.Frame-work символом-разделителем при описании пути сквозь пространство имен служит символ точки: ".". Так, если вы хотите сослаться на класс RegularExpressions, находящийся в пространстве имен Text, вложенном, в свою очередь, в пространство имен System, следует указать следующий путь: System.Text.RegularExpressions.

Подобная форма структурирования кода имеет массу достоинств. Вместе с тем, она приводит к тому, что вам придется, ссылаясь на объект, описывать весь путь к нему, а это довольно много лишней писанины. Впрочем, на самом деле все не так страшно, как кажется на первый взгляд.
Во-первых, в Visual Studio встроена нежно любимая многими, и мной в том числе, технология IntelliSense. Как только вы напишете в редакторе Visual Studio слово System и поставите после него точку, активизируется выпадающий список, в котором вам будет предложено выбрать вложенные в System классы или пространства имен. После того как вы выберете, скажем, Text, поставьте опять точку, и вам будет предложен для выбора список вложенных уже в Text пространств имен и классов.
Этот "список" довольно интеллектуален. В зависимости от того места в тексте программы, где вы его вызвали, он показывает только те пространства имен, классы и свойства, которые допустимо использовать в данном месте кода. Поэтому не удивляйтесь, если не сможете найти в нем что-либо, что заведомо должно в нем присутствовать. Просто сместитесь на правильный фрагмент в своем коде. Не стоит, к примеру, пытаться описывать using внутри какого-либо класса — место этой директивы в самом начале файла с кодом.

Если бы единственным способом указать путь сквозь пространство имен была технология IntelliSense, жизнь программиста на C# все равно была бы довольно печальной, несмотря на все достоинства этой технологии. К счастью, IntelliSense возможности Net.Framework не ограничиваются. Если вы в своем модуле часто используете объекты из какого-либо пространства имен, вы можете указать его в начале модуля с помощью кодового слова using. После этого вы можете вызывать объект из этого пространства, просто набрав его имя без указания пути. Так, для примера, если вам часто приходится пользоваться в коде объектом RegularExpressions, то вместо того, чтобы каждый раз писать System.Text.RegularExpressions, один раз укажите в начале файла using System.Text — в дальнейшем вы можете просто писать RegularExpression, не указывая полного пути. Вернувшись к нашей аналогии с папками Windows, заметим, что подобная техника очень сильно напоминает работу переменной Path в среде Windows/MSDOS.
Директив using может быть указано столько, сколько вам нужно для комфортной работы с объектами Net.Framework или созданными вами самим пространствами имен. Наверняка у вас возникнет вопрос: а что произойдет, если я укажу несколько директив using, и в выбранных мной с ее помощью пространствах имен окажутся одинаково названные объекты? Хороший вопрос. Сразу замечу, что выбор пространства имен директивой using не приводит к немедленному копированию всех содержащихся в нем классов в ваш код. Это не более чем способ кратко оформлять текст вашей программы. Неоднозначность может возникнуть лишь в момент объявления конкретного типа объекта в вашей программе. Net.Framework предоставляет вам три способа выкрутиться из такой ситуации. Впрочем, это я придумал три способа — возможно, их намного больше.

Первый способ:
Ссылаясь на дублирующиеся объекты, указывайте полное имя с указанием пути. Игнорируя указание директивы using, просто пишите объявления этих неуживчивых классов так, как будто никакой директивы using не было в помине. В этой ситуации Net.Framework не остается ничего другого, как отказаться от своей интеллектуальности и создать именно тот класс, который вы просите.

Второй способ:
Используйте неполный путь в директиве using. Возьмем простой пример. Скажем, у нас есть два пространства имен, MyProject.MyClasses1 и MyProject.MyClasses2. В каждом из них присутствует класс, называющийся MyClass. Вместо того чтобы указывать две директивы, using MyProject.MyClasses1 и using MyProject.MyClasses2, напишите одну директиву using MyProject, а объект вызывайте либо как MyClasses1.MyClass, либо как MyClasses2.MyClass.

Третий способ (мой любимый):
Используйте специальную форму директивы using, при которой пространству имен присваивается удобное вам "прозвище" (Alias). Выглядит такая форма записи следующим образом:

using streg = System.Text.Regular Expressions;

где streg — это alias, который я выбрал для пространства имен System.Text.RegularExpressions. Обратите внимание: это я выбрал streg — вы можете выбрать любую другую удобную вам аббревиатуру. Название конкретного алиаса может быть любым. В дальнейшем вы можете обращаться к пространству имен System.Text.RegularExpressions с помощью краткой формы записи streg.

Например, таким образом:

streg.Regex reg = new streg.Regex();

Во всех приведенных мной случаях вы избежите ситуации с возможной неоднозначностью имен в вашей программе.
Способ с алиасами удобен еще и тем, что при такой форме записи, набрав алиас (streg) и поставив точку, вы получите подсказку от IntelliSense обо всех входящих в это пространство имен классах.
Так как очень трудно запомнить названия всех классов в Net.Framework, я сам пользуюсь алиасами даже тогда, когда в моем коде нет конфликтов имен классов. Возможность быстро подставить в свой код правильное имя класса, о котором вы знаете лишь то, что он объявлен где-то в недрах System.Data, и его название оканчивается, вроде бы, на xxxColumn, существенно ускоряет написание кода. Помимо этого, вам не нужно теперь заучивать имена всех классов в Net.Framework. С помощью технологии IntelliSense вы всегда сможете быстро отыскать тот класс, который вам потребовался.
Помните мою вторую статью из этого цикла? Прощай, проблема "семь-десять объектов, которыми человеческий мозг может оперировать одновременно"! Когда я только приступал к изучению Net.Framework, передо мной встала задача написать несложную консольную программу, изменяющую нужным мне образом некий ключ в реестре. Я сел, в задумчивости глядя на совершенно незнакомый мне тогда язык C#, и написал: using Microsoft. Среда услужливо мне подсказала: вам, мол, дальше куда? Предлагаю на выбор: CSharp, VisualBasic или Win32. Win32, разумеется! — ответил я. Выпавший вслед за моим выбором список оказался на этот раз более длинным, но слово Registry мне бросилось в глаза сразу. Дальше — дело техники, и нужную программу я написал минут за пятнадцать. Совсем неплохо для человека, только-только начинающего знакомиться с новым языком программирования!
Ну ладно, вернемся от моих воспоминаний к нашему насущному примеру с MVD_Worker. Мы рассмотрели все то, что было упущено в моих предыдущих статьях, и, надеюсь, теперь код, сгенерированный мастером нового класса Visual Studio, вам совершенно понятен. Давайте приступим к его модификации. Не трогая директиву namespace, расширяем код класса MVD_Worker. Добавляем в него поля и свойства, описывающие наличие документа у нашего гипотетического работника. Создаем пару конструкторов, умалчиваемый без параметров и еще один, принимающий один параметр, в котором мы указываем, имеется у вновь создаваемого работника удостоверение или нет.

public class MVD_Worker
{
private bool internalHasDocument;
public bool HasDocument
{
get{return internalHasDocument;}
}

public MVD_Worker()
{
internalHasDocument=false;
}
public MVD_Worker(bool WorkerHasDocument)
{
internalHasDocument=WorkerHasDocument;
}
}

Вот и все. Наш базовый класс готов. Для того чтобы красиво отформатировать написанный вами код, выберите его в блок и используйте комбинацию клавиш Ctrl-K, Ctrl-F. Ну вот, теперь у нас все чисто и аккуратно. С помощью Ctrl-S сохраняем файл и приступаем к созданию класса-потомка.
Опять вызываем мастер нового класса Project
-> Add Class. Говорим, что хотим создать файл по имени Omonovez.cs. Мастер создает нам заготовку нового класса, и мы приступаем к ее модификации. Видите строчку public class Omonovez? Давайте приведем ее к следующему виду:
public class Omonovez:MVD_Worker
То есть к уже имеющемуся коду мы добавили двоеточие и имя нашего класса MVD_Worker. С помощью двоеточия и следующего за ним имени класса-предка и обозначается наследование этим классом-потомком свойств и методов класса-родителя. Весь остальной код класса пока оставляем без изменения.
Что же нам дало это обсуждаемое наследование? Давайте взглянем на его работу с помощью простой демонстрации. Выделите в редакторе файл с главной формой вашего приложения (тот самый, в который мы вставляли список и кнопку). Дважды щелкните по кнопке. В ответ среда сгенерирует вам прототип для метода — обработчика нажатия на эту конкретную кнопку. Приводим метод-обработчик вот к такому виду:

private void Go_Click(object sender, System.EventArgs e)
{
MVD_Worker wrk = new MVD_Worker();
log.Items.Add("wrk.Has Document = "+wrk.HasDocument);
Omonovez boez = new Omonovez();
log.Items.Add("boez.Has Document = "+boez.HasDocument);
}

Запускаем программу на исполнение с помощью кнопки F5, и, если вы все сделали правильно, перед вами появится окно Windows-приложения с нашей формой, списком и кнопкой. Щелкаем в этом окне по кнопке, и в списке появятся две строчки текста:

wrk.Has Document = False
boez.Has Document = False

Обратите внимание: несмотря на то, что мы с вами не описывали свойство HasDocument в классе Omonovez, оно там, тем не менее, присутствует и исправно работает!
Появилось оно в нашем новом классе как раз за счет механизма наследования. Все методы, поля и свойства класса-предка, объявленные как публичные (public), доступны классу-потомку. Вы можете использовать их в своем классе-потомке так, как вам заблагорассудится. Давайте посмотрим их использование на еще одном примере. Давайте изменим поведение нашего класса Omonovez. Пусть экземпляр этого класса всегда носит при себе удостоверение. Откройте на редактирование файл Omonovez.cs и внесите в его конструктор следующую строчку: HasDocument=true; (вы же уже теперь знаете, где у объекта находится конструктор, не так ли?).
Ну и как попробовали? Что, не получается? Правильно, ошибка: "Данное свойство Read/Only изменять его нельзя!". Так что нам делать в такой ситуации?
Первое решение, которое сразу приходит в голову, — просто объявить переменную internalHasDocument, ту самую, которая объявлена в классе MVD_Worker, не с модификатором private, а с модификатором protected, и обращаться к ней напрямую из конструктора класса Omonovez.
Постойте! Я же вам еще ничего не рассказывал о том, что это вообще за модификатор такой, protected, и зачем он нужен. Давайте я воспользуюсь случаем и исправлю это упущение.
Как вы уже знаете, переменная, объявленная как private, доступна только внутри того класса, где она объявлена. Попытка обратиться к ней извне приводит к ошибке времени компиляции. Придумано это для того, чтобы реализовать понятие инкапсуляции.
Подразумевается, что для модификации такой переменной у вас в классе должно быть объявлено публичное свойство или метод.
На первый взгляд, тут все хорошо задумано, но в некоторых ситуациях этот механизм работает крайне неэффективно. Предположим, мы описали переменную, описывающую количество патронов в автомате нашего бойца. При добавлении патронов мы для начала выясняем, а есть ли у омоновца вообще автомат, иначе и заряжать-то ему, по крупному счету, нечего. Затем проверяем, не превышает ли количество заряжаемых патронов вместимость магазина. Также попутно убеждаемся, что автомат не заклинило, так как в этом случае зарядить его невозможно. Заклинивает автомат тогда, когда мы пытаемся затолкать в него больше патронов, чем влезает в его магазин. Заклинивший автомат выдает пятисекундную задержку при попытке выяснить количество имеющихся в нем патронов или при попытке его перезарядить. Для того чтобы починить автомат, необходимо вынуть из него все патроны.
Фрагмент кода, реализующий подобный алгоритм, может выглядеть примерно так:

private int internalPatronCount=0;
private bool internalJammed=false;
const int maxPatron=30;

public int PatronCount{
get {
if (internalHasKalashnik){
if(internalJammed){
System.Threading.Thread.Sleep(5000);
}
return internalPatronCount;
}
else {
return 0;
}
}
set {
if (internalHasKalashnik){
if(internalJammed){
if(value==0){
internalJammed=false;
internalPatronCount=0;
}
else{
System.Threading.Thread.Sleep(5000);
}
} else {
if (value<=maxPatron) {
internalPatronCount=value;
}else {
internalPatronCount=maxPatron;
internalJammed=true;
}
}
}
}
}

Через некоторое количество времени работы с классом нас достали постоянные заклинивания автомата. Мы создаем класс-потомок и в его конструкторе заполняем магазин автомата по одному патрону, сразу проверяя их качество и правильность их установки в магазин. То есть заклинить автомат у нас просто физически не может. Вот тут-то мы и наталкиваемся на неудобство "закрытости" переменной internalPatronCount. В тот момент, когда мы добавляем очередной патрон, у нас каждый раз отрабатывают методы get{} и set{}, проверяющие в своем теле, а есть ли у омоновца автомат, а не заклинило ли его... ну, и так далее по тексту программы. Нам эти проверки на данном участке кода вовсе не нужны. Мы заведомо знаем, что автомат не заклинит, что мы добавляем патроны по одному, но избавиться от проверки, тем не менее, никак нельзя. Я взял достаточно простой пример, но представьте себе ситуацию, при которой вы добавляете полмиллиона записей в базу данных, и при этом после добавления каждой из них методом set{} производится, к примеру, заблаговременно предусмотренное программистом класса услужливое сжатие базы данных.
Так что же делать в такой ситуации? Тут на помощь и приходит модификатор доступа protec-ted. В большинстве случаев переменная, объявленная как protected, ведет себя как private-переменная. Точно так же она недоступна внешнему коду, использующему наш класс, и точно так же к ней может свободно обращаться код изнутри того класса, в котором она объявлена. Но, в отличие от private-переменных, переменная, объявленная как protected, свободно доступна и всем потомкам класса, в котором она объявлена. Из кода класса-потомка вы можете напрямую изменять ее значение, не пользуясь для этого свойством, а вот все остальные классы в вашем проекте могут с ней работать только через свойство.
Итак, с назначением модификатора protected мы вроде разобрались, теперь попробуйте исправить объявление переменной internalHasDocu-ment в классе MVD_Worker следующим образом:

protected bool internalHasDocument;

а в конструкторе Omonovez вписать вот что:

internalHasDocument=true;

Теперь у нас все получилось. После нажатия кнопки на форме нашего приложения ListBox log выводит:

wrk.Has Document = False
boez.Has Document = True

И вот еще что: еще раз обратите внимание на следующий нюанс.
Мы создаем два разных объекта, и они по умолчанию обладают совершенно разными свойствами. В первом случае у нас создается экземпляр MVD_Worker, который ничего не знает о существовании своего потомка Omonovez. Из-за этого его незнания изменения алгоритма в этом классе-потомке никак не влияют на алгоритм работы самого класса-предка.
Во втором случае мы создаем производный от MVD_Worker класс Omonovez, который широко пользуется функциональностью своего класса-родителя, поэтому изменение алгоритма класса-предка сказывается на всех производных от него классах-потомках.
Когда вы проектируете базовый класс-предок, хорошенько подумайте о том, что именно должно в него входить и по возможности сделайте этот класс как можно более простым.
Ну что, подведем итог. Проблему-то мы с вами решили, но как-то некрасиво. Получается, что классы-потомки свободно обращаются с полями класса-предка так, как их левая нога захочет. А как же наш первый столп — инкапсуляция? Ведь класс-предок запросто может иметь свои собственные сложные алгоритмы действий, срабатывающие во время присвоения свойства hasDocument. Непорядок.
Обратите внимание: у нас в классе MVD_Worker имеется конструктор, принимающий в качестве параметра bool-значение. Это значение указывает, можно давать этому работнику в руки документы или нет. Давайте-ка и воспользуемся этим конструктором. Переправляйте переменную internalHasDocument в классе MVD_Worker обратно с protected на private, а конструктор класса Omonovez давайте оформим следующим образом:

public Omonovez():base(true)
{
}

Вам наверняка сразу бросилась в глаза новая форма записи конструктора, очень похожая на объявление класса с предком. Похожесть эта не случайна. В данном случае мы указываем, что при вызове нашего конструктора следует обратиться к конкретному конструктору предка. А именно конструктору, принимающему параметр bool, и передать ему значение true.
Наш конструктор как бы унаследует указанный конструктор предка. Прежде чем код в конструкторе нашего нового класса получит управление, отрабатывает конструктор класса-предка, имеющий параметр в виде значения типа bool. О механизме поиска средой Net.Framework такого конструктора я вам рассказывал в предыдущей статье, когда говорил о перегрузке операторов.
На самом деле даже тогда, когда мы с вами вовсе не указываем тип конструктора предка, а просто пишем public Omonovez(){...}, Net.Framework разворачивает эту запись в public Omonovez():base(){...}. То есть вызывает дефолтовый конструктор объекта предка, не имеющий никаких параметров. Как вы помните из предыдущих статей, такой конструктор всегда есть, даже тогда, когда мы с вами его явно не описали. Откуда он берется? Помните, я вам говорил о том, что все типы данных в Net.Framework унаследованы, в конце концов, от одного-единственного класса по имени Object? Вот его конструктор без параметров и вызывается в том случае, если у вашего класса (или его предков) такой конструктор не объявлен.
С помощью явного указания нужного нам конструктора мы можем изменить порядок действий Net.Framework по умолчанию и вызвать тот конкретный конструктор, который нам нужен.
Попробуйте запустить на исполнение нашу новую версию программы. Ну вот — все получилось! У wrk документов нет, у boez они есть, и при этом мы не нарушили инкапсуляцию в объекте MVD_Worker. Сплошная идиллия.
Помимо слова base, означающего вызов конструктора предка, вы можете указать также слово this, означающее вызов другого конструктора этого же самого объекта. Зачем это может потребоваться? Давайте расширим наш класс Omonovez и дадим ему на голову каску, а в руки автомат. Приведите код класса вот к такому виду:

public class Omonovez:MVD_Worker
{
private string internalName = "Unnamed";
private bool internalHasKalashnik = false;
private bool internalHasKaska = false;
/// опустим для краткости описание свойств.
/// опишите их самостоятельно в качестве домашнего задания.
public void Inventar(System.Windows.Forms.ListBox log)
{
log.Items.Add("");
log.Items.Add("Боец-"+this.Name);
log.Items.Add(" Документы -"+ HasDocument);
log.Items.Add(" Каска -"+ internalHasKaska);
log.Items.Add(" Калашник -"+ internalHasKalashnik);
}
public Omonovez():base(true){}
public Omonovez(string Name):this(){internalName=Name;}
public Omonovez(string Name,bool UseKaska):this(Name) {
internalHasKaska = UseKaska;
}
public Omonovez(string Name,bool UseKaska,bool UseKalashnik) :this(Name,UseKaska){
internalHasKalashnik=UseKalashnik;
}
}

Обратите внимание на то, каким образом у меня тут оформлены конструкторы.
Во-первых, имеется конструктор без параметров, в свою очередь, вызывающий базовый конструктор предка с указанием дать в руки бойцу документы.
Второй конструктор предварительно вызывает предыдущий конструктор с помощью ссылки this(), а затем присваивает бойцу имя.
Третий конструктор спихивает установку имени предыдущему конструктору, вызывая this(Name), а затем, от себя, выдает ему каску.
Четвертый конструктор доверяет присвоение имени и выдачу каски третьему конструктору через вызов this(Name,Kaska), а сам додает ему автомат.
Таким образом, если нам когда-либо впоследствии потребуется, скажем, изменить алгоритм одевания на бойца каски, мы будем менять код только в одном-единственном месте, а именно в третьем конструкторе. И это наше изменение никак не затронет остальной алгоритм. Происходит так потому, что каждый конструктор выполняет только тот фрагмент кода, в котором поведение объекта _отличается_ от поведения, задаваемого другими конструкторами. Вся же остальная работа доверяется тому, кто ее умеет хорошо делать, а именно другим специализированным конструкторам.
Если вы самостоятельно рассматривали пространство имен Net.Framework, то наверняка обратили внимание на то, что большинство входящих в него классов имеет по несколько конструкторов, отличающихся числом параметров. Так, скажем, System.Data.DataColumn можно создать пятью разными способами. Мне кажется, что, вместо того чтобы пять раз описывать в коде присваивание одних и тех же переменных, намного проще использовать данную технику. Думаю, разработчики Net.Framework так и поступили. Иначе можно только позавидовать их терпению, ибо некоторые объекты имеют более 20 разных конструкторов!
Давайте протестируем наш новый объект Omonovez. Исправьте код, выполняющийся при нажатии кнопки Go, следующим образом:

private void Go_Click(object sender, System.EventArgs e) {
Omonovez boez1 = new Omonovez();
boez1.Inventar(log);
Omonovez boez2 = new Omonovez("Вася");
boez2.Inventar(log);
Omonovez boez3 = new Omonovez("Петя",true);
boez3.Inventar(log);
Omonovez boez4 = new Omonovez("Федя",true,true);
boez4.Inventar(log);
}

Ну вот, если вы все сделали правильно, у вас получилось подразделение из четырех бойцов, обладающих именем, каской и автоматом в самых разных наборах из этих свойств.
Говоря о наследовании, мы с вами не рассмотрели еще такую важную возможность ООП, как переопределение методов. Суть идеи сводится вот к чему. Когда вы создаете базовый класс, вы задаете в нем какую-либо функциональность. Мы в нашем примере в качестве такой функциональности задали наличие документа и способ его получения. Для этого мы воспользовались private-переменной internalHasDocument и Read-Only bool-свойством HasDocument. Прошло время, и нам стало не хватать такой простой функциональности в некоторых из его классов-потомков. Скажем, мы создали класс GIBDD, и тут выяснилось, что, помимо обычного удостоверения работника MVD, он еще должен носить при себе права на управление автомобилем. Эка невидаль! — воскликнем мы и по накатанной дорожке создадим private-переменную internalHasPrava и свойство HasPrava для ее чтения.

public class GIBDD:MVD_Worker
{
private bool internalHasPrava;
public bool HasPrava
{
get{return internalHasPrava;}
}

public GIBDD(bool hasPrava):base(true)
{
this.internalHasPrava=hasPrava;
}
}

Но постойте! Некрасиво как-то получается. Выходит, у всех нормальных работников МВД мы проверяем только свойство HasDocument, а у гаишников приходится проверять еще одно дополнительное свойство? Ну ладно, мы с вами издалека отличим работника ГИБДД от омоновца, а программе-то как их различать? У каждого встречного класса спрашивать: "ты кто, гаишник или омоновец?"
К слову, метод для этого у всех классов Net.Framework имеется, наследуется он от Object и вызывается как класс.GetType(). Возвращает тип класса. Если вы в предыдущем примере после boez4.Inventar(log) добавите еще одну строчку кода:

log.Items.Add(boez1.GetType().ToString());

то в конце лога у вас появится строчка: ваше_namespace.Omonovez. Сравнив ее со строчкой ваше_namespace.GIBDD, мы можем понять, нужно ли дополнительно проверять права у данного работника MVD. Но это совсем уж дремучий и неправильный способ. Намного корректнее было бы воспользоваться для этой цели оператором is. Он сравнивает объект с указанным типом и возвращает true, если объект принадлежит к этому типу или же унаследован от него. Например:

Omonovez boez1 = new Omonovez();
GIBDD gai1 = new GIBDD();
...
if(boez1 is MVD_Worker){} // вернет true, боец является работником МВД
if(boez1 is Omonovez){} // также вернет true, он омоновец
if(boez1 is GIBDD){} // вернет false, нет, он не работник GIBDD
...
if(gai1 is Omonovez){} // вернет false, гаишник не омоновец
if(gai1 is MVD_Worker){} // вернет true, да, он работник МВД
if(gai1 is GIBDD){} // вернет true, правильно! Он гаишник.

Вот такой код с точки зрения программирования под Net.Framework более правилен. Но он, тем не менее, все равно неудобен. И что, нам так каждый раз и придется развлекаться, прежде чем проверить у работника MVD документы? К счастью, есть другой способ. И даже не один, а, как принято в Net.Framework, как минимум два.
Первый способ требует небольшой модификации исходного класса MVD_Worker. Отыскиваем в нем строчку объявления свойства HasDocument и приводим ее вот к такому виду:

public virtual bool HasDocument
{
get{return internalHasDocument;}
}

Новым в этом объявлении является только модификатор virtual. Предназначен он для того, чтобы указать, что данный метод может быть, теоретически, перехвачен в классе-потомке. Я не зря написал "теоретически": если вы его не перехватываете, он работает как обычно. Поэтому, если вы создаете новый класс и считаете, что некоторые из его свойств или методов могут быть в дальнейшем перехвачены, смело ставьте virtual. Многие гуру ООП не согласятся со мной в этом мнении: дело в том, что виртуальные методы замедляют вашу программу. Но, на мой взгляд, этот вопрос носит скорее теоретический характер, и в большинстве реальных программ столь незначительной потерей быстродействия можно пренебречь.
Итак, как объявить метод, который можно перехватить, мы знаем, теперь давайте посмотрим, как нам этим методом воспользоваться. А довольно легко! Объявляем в нашем классе-потомке вот такое свойство:

public override bool HasDocument{
get{
if(base.HasDocument){
return internalHasPrava;
}else{return false;}
}
}

Для того чтобы перехватить виртуальный метод предка, следует в классе-потомке объявить метод с тем же именем и тем же набором параметров, как у перехватываемого нами метода, и указать модификатор override. Вот и все. Изнутри этого своего метода вы можете обратиться к исходному методу в классе-предке с помощью кодового слова base. Я рекомендую вам, перехватив метод предка, всегда вызывать его исходный метод из своего кода: бог его знает, какую функциональность он внутри себя реализует. Пускай он сделает свои дела, а мы добавим потом свои. Даже в нашем несложном примере чтения свойства HasDocument вполне могло оказаться, что родительский класс MVD_Worker, к примеру, подсчитывает, сколько раз каждый работник показывал свое удостоверение. В зависимости от числа показов он начисляет им зарплату. Если мы не вызовем свойство предка, он не сможет реализовать этот алгоритм. Зачем же нам обижать работников этого славного ведомства! Всегда, перехватив метод предка, на всякий случай вызовите оригинальный метод из своего кода.
В нашем случае я воспользовался значением, полученным от предка, для того, чтобы выяснить, есть ли у нашего работника удостоверение МВД, а затем дополнительно проверил, а есть ли у него права. Таким образом, наш класс GIBDD совершенно корректно обрабатывает стандартный для всех работников MVD метод HasDocument, и нам теперь нет никакой необходимости как-то отдельно обрабатывать работников ГАИ при проверке документов. Давайте проверим, сработает ли наш код. Приведите обработчик нажатия кнопки Go на нашей форме вот к такому виду:

GIBDD gai1 = new GIBDD(false);
log.Items.Add(gai1.HasDocument);
GIBDD gai2 = new GIBDD(true);
log.Items.Add(gai2.HasDocument);

После запуска программа выведет две строчки: False и True. Это означает, что метод HasDocument правильно реагирует на наличие прав у работника ГАИ.
Вторым способом перехватить метод предка является использование ключевого слова new. Используется оно, в основном, тогда, когда нужный вам метод предка не объявлен как virtual, а исходный код класса-предка вам недоступен — например, попал к вам в виде откомпилированной кем-то DLL. Для того чтобы перехватить такой метод, впишите вместо слова override слово new, и все. Теперь вы сможете по-своему обрабатывать и такие заранее не объявленные, как virtual-методы. Вы спросите: а зачем потребовалось вводить два разных способа перехвата методов, когда хватило бы и одного new? Дело в том, что эти два способа работают немного по-разному. Эту разницу мы с вами обсудим в следующей статье, когда вплотную подберемся к третьему, самому интересному, столпу ООП, называемому полиморфизм.
А пока, в качестве домашнего задания, попробуйте откомпилировать вот такой обработчик кнопки Go и попытаться понять, что именно происходит в этом моем примере. Немалую помощь в понимании происходящего вы сможете получить от Visual Studio, если пройдете этот код в ее отладчике "по шагам".

MVD_Worker omon1=new Omonovez("Федя",true,true);
log.Items.Add("omon1.hasDocument?"+omon1.HasDocument);
MVD_Worker gai1 = new GIBDD(false);
log.Items.Add("gai1.hasDocument?"+gai1.HasDocument);
MVD_Worker gai2 = new GIBDD(true);
log.Items.Add("gai2.hasDocument?"+gai2.HasDocument);
Обратите внимание и на то, что в случае с new и в случае с override результаты получаются совершенно разные.

Продолжение следует.
Герман Иванов



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

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