Этюды в тональности C# 1

Этюды в тональности C#

Заканчивая рассказ о TV паттерне программирования, я подумал о том, а не слишком ли быстро я бегу. Как ни старался я писать попроще, однако же подаваемый мной материал получился все-таки довольно сложным для восприятия начинающим программистом. Помимо того, пролистав текст статьи, я обнаружил, что вовсю оперирую программными конструкциями, совершенно не рассмотренными мной в предыдущих статьях из цикла "Неформальное введение в Net Framework". В этой и последующих статьях я постараюсь восполнить образовавшийся пробел и рассказать вам о конструкциях языка, оставленных мной за рамками повествования. На тот случай, если вы не читали предыдущие статьи цикла, публиковавшиеся в КГ, рекомендую посетить мою авторскую веб-страничку на сайте издательства (короткий путь к ней: http://german2004.da.ru ).

Константы
Хороший тон программирования требует от нас не использовать в своем коде никаких загадочных чисел. Написав нечто вроде "Diameter/3.14", вы впоследствии можете очень долго гадать, что именно это таинственное число 3.14 означает, и откуда оно вообще взялось в вашей программе. Не смейтесь: так оно и есть. Это сегодня вы помните, что 8.654 — это диаметр колеса, а 16 — это число зубьев некоей шестеренки — через полгода предназначение этих чисел будет для вас тайной за семью печатями. Если бы вы, следуя хорошему тону программирования, заранее описали для этого числа константу и в последующем коде ссылались на нее, ваша программа бы выглядела намного понятнее. Судите сами:
const float pi =3.14;

res=Diameter / pi;
Помимо улучшения общей внятности кода программы, использование констант также сильно упрощает процесс внесения изменений в алгоритм. Если в некоем модуле вы в пятнадцати разных процедурах использовали одно и то же число 3.14, то при необходимости изменить его, скажем, на 3.142 вам придется править код в пятнадцати разных местах. При этом довольно высока вероятность, что, внося исправления, вы пропустите одно или несколько значений, и ваша программа будет работать неправильно.

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

Перечисления
Предположим, при создавании некоего приложения вам потребовалось описать в нем константы, соответствующие порядковому номеру месяцев года. Следующим шагом вы могли бы объявить эти константы в своем проекте вот таким образом:
const int January=1;
const int February=2;
const int March=3;
const int April=4;
const int May=5;
const int June=6;
const int July=7;
const int August=8;
const int September=9;
const int October=10;
const int November=11;
const int December=12;

В дальнейшем мы могли бы написать следующий блок кода, работающий с этими константами:
function Process(int MonthNum){
switch (MonthNum) {
case January:
...
break;
case February:
...
break;
}
}

Обратите внимание: переменная MonthNum в объявлении функции у нас задается как число типа int. Предназначение числа типа int слишком расплывчато и двусмысленно. Мы можем передать нашему методу Process вместо номера месяца, скажем, число сотрудников вашей организации или даже и вовсе количество яблок, съеденных вами за день. Ничто в программе не остановит нас от подобного неправильного использования функции. Для того, чтобы избегать подобных неоднозначностей, и была придумана такая конструкция языка, как перечисление.
Перечисление (enum) — это просто удобные человеку названия для некоей последовательности цифр. По своей сути они не больше чем череда именованных констант, выделенных в отдельный тип — нечто вроде массива констант. Использование перечислений (enum) снимает описанную мной неоднозначность да и дополнительно еще и уменьшает количество ненужной писанины в вашем коде. Выглядит объявление перечисления примерно так:

public enum Months {January, February, March, April, May, June, July, August, September, October, November, December}
В дальнейшем вы обращаетесь к членам этого перечисления как к свойствам некоего класса. Например, функция Process с использованием перечисления могла бы выглядеть следующим образом:

function Process(Months month){
switch(month) {
case Months.January:
...
break;
case Months.February:
...
break;
}
}

Обратили внимание на тип параметра функции? Вам не удастся теперь, используя перечисления, случайно передать в эту функцию какое-либо произвольное число. В этой версии функция Process соглашается принимать параметры только конкретного типа Months, а именно порядковый номер месяца. Помимо этой защиты от неправильного использования метода, мы получили и еще один небольшой бонус от использования перечислений. Теперь нам не нужно запоминать английские названия всех месяцев. После того как вы набрали Months и поставили точку, Visual Studio самостоятельно выведет список имеющихся месяцев, и вы можете выбрать из него тот, который вам нужен в данный момент времени.
Если вы пытались повторить приведенный мной пример, то наверняка обратили внимание на одну шероховатость в его коде. Дело в том, что по умолчанию члены, входящие в перечисление, нумеруются начиная с нуля. Иногда это обстоятельство никак не мешает, а вот в случае с нумерацией месяцев я бы предпочел видеть месяцы пронумерованными не с нуля, а с единицы.

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

public enum Digits {
One=1,//Значение равно 1
Two,//Значение равно 2
Three,//Значение равно 3
Four,//Значение равно 4
Ten=10,//Значение равно 10
Fifteen=15,//Значение равно 15
Sixteen//Значение равно 16
}

Итак, за счет механизма перечислений вы можете без труда описывать длинные последовательности констант и при этом избегаете необходимости для каждой из них указывать конкретное значение. Перечисления также являются отдельным пользовательским типом данных: вы можете передавать его в качестве параметра методам других объектов, при этом данный метод будет защищен от получения параметров неправильного типа. Перечисления упрощают ввод кода: вам не нужно запоминать наизусть названия всех ваших констант — Visual Studio сама вам их подскажет после точки. Побочным результатом этого является улучшение читаемости вашего кода. Во-первых, человеку легче иметь дело с мнемоническими обозначениями вместо безликих цифр, а во-вторых, вы можете называть члены перечисления достаточно длинными именами — скажем, MyFirstEnumConstant — все равно вам не придется набирать это название вручную.

События (event) и делегаты (delegate)
Даже если вы только-только начинаете программировать на C#, вы уже наверняка хорошо знакомы и с событиями, и с делегатами. Я делаю такое смелое заявление потому, что эти лексические конструкции не требуется изучать специально. На событиях и построено по умолчанию все программирование в NET Framework, да и в остальных современных языках программирования — например, Delphi. Поэтому вы, конечно же, с ними встречались, хотя, возможно, и не знали, что код, который вы пишете, называется такими мудреными словами.
Когда вы бросаете на форму кнопку и пишете обработчик ее нажатия, вы пишете как раз обработчик события (event). В данном конкретном случае — обработчик события OnClick для этой кнопки. После того как вы два раза щелкнули мышкой по нужной кнопке, среда программирования формирует для вас заготовку функции. Эта функция является реализацией объявленного для кнопки делегата — говоря по-русски, прототипа функции по обработке ее нажатия. Функция выглядит именно такой, какой вы ее видите, только потому, что кто-то из ее создателей заблаговременно описал в коде ее класса соответствующий делегат.

Делегаты
Ничего сложного в понятии делегата нет. Как я и сказал выше, делегат — это не более чем описание "макета" какой-либо функции. Вы просто говорите среде, что в каком-то объекте, который вы, возможно, напишете в дальнейшем (а может, и не напишете), будет существовать некий метод. Как он будет называться, вы пока не знаете, но стопроцентно уверены, что он обязательно будет иметь два параметра. Первый параметр, скажем, будет иметь тип объект, а второй будет, скажем, текстовой строкой. Описав этот гипотетический метод, вы можете уже сейчас, на данном этапе программирования, вызывать его из кода своего объекта.

Кнопка из нашего примера знает, что в тот момент, когда кто-то по ней щелкнул мышкой, ей следует вызвать метод, который выглядит заранее известным ей образом. Она знает, что в этот метод следует передать такие-то параметры. Больше ничего о внешнем мире кнопке знать не нужно. Создавший эту кнопку программист может сосредоточиться на написании внешней реакции кнопки на раздражитель — ну, скажем, на прорисовке ее нажатия. За счет описания делегата его вовсе не волнует, как именно его кнопка интегрируется в будущее приложение, о котором он сейчас совершенно ничего не знает.
С другой стороны "веревочки" программист, создающий приложение, также использует кнопку, не вникая в детали ее внутренней реализации. Он знает только о том, что существует некий "контракт" между ним и программистом кнопки. В этом "контракте" они договорились, что метод, срабатывающий на нажатие кнопки, выглядит определенным образом. Ему осталось только его запрограммировать. Уловили идею? Сейчас я вам расскажу, как это делается.

Синтаксически делегат описывается с помощью кодового слова delegate — например, вот так:
public delegate string MyDelegate(object sender);
В этом примере мы описываем прототип метода. Этот метод возвращает значение типа строка (string) и принимает один параметр типа объект (object). Слово MyDelegate в его названии задает не название метода, как вы, вероятнее всего, подумали, а его тип. В дальнейшем, ссылаясь на этот делегат, вы указываете данный тип как признак того, какой именно делегат вы хотите использовать в своем классе.

namespace MyProject {
public delegate string MyDelegate(object sender);
public class MyClass {
public MyDelegate GetName;
publlic string GetNameThis(object Param1){
if(GetName!=null) GetName(Param1);
}
}
}

В этом фрагменте мы сначала объявляем делегат типа MyDelegate. Этот делегат возвращает значение типа string и имеет один параметр типа object. В коде метода GetNameThis вы, предварительно убедившись, что значение переменной делегата не равно null, просто вызываете его, как обычную функцию. Проверка на null необходима из-за того, что сам факт объявления делегата не приводит к формированию реальной функции. Да и откуда бы ей взяться? Ведь программа ничего о ней не знает до тех пор, пока какой-либо фрагмент вашего кода не дополнит эту "шапку" функции конкретным своим методом.

К слову, описание в качестве параметра делегата типа object не случайно. Наверняка, изучая код обработчиков событий, созданных мастером Visual Studio, вы обращали внимание на то, что это его излюбленный прием. Если вы помните, все объекты NetFramework имеют object среди своих предков, поэтому, используя полиморфизм, мы можем подставить на место object любого из его потомков, то есть любой произвольный объект, который вам взбредет в голову.
Для работы с таким переданным полиморфным параметром существуют две специальные команды. Первая из них, называющаяся is, позволяет выяснить, а не принадлежит ли полученный методом объект к некоему заранее известному нам типу. Вторая команда, называющаяся as, приводит полученный объект к тому типу, который нам нужен. Как правило, с ее помощью объекту, переданному через переменную типа object, возвращают его истинное лицо — тот тип, который он имел изначально, пока не превратился при такой системе передачи параметров в безликий object.

...
if(sender is Label){
(sender as Label).Text= "Hi";
}
...

Привести тип объекта можно только в том случае, если "приводимый" объект является потомком того типа, к которому он приводится. Говоря по-русски, мы не можем заставить объект-предок стать объектом-потомком, а вот обратная трансформация легкодопустима. Поэтому, объявляя параметр как тип object, мы получаем возможность подставлять на место этого параметра любой объект.
Программистам на Delphi концепция делегатов хорошо знакома по ссылкам на функцию. Единственное новшество, которое добавила к их функциональности NET FRAMEWORK, так это свой обязательный строгий контроль типов. Вам не удастся, скажем, объявить делегат одного типа, а присвоить ему метод другого типа. Среда вас обругает еще на этапе компиляции. На тот случай, если вы забыли, напомню, что в NET метод считается подходящим под конкретный делегат только в том случае, если его возвращаемое значение, число и тип параметров совпадают с теми, которые объявлены в делегате. Предположим, у нас имеется делегат OnMyCommand и несколько методов. Давайте посмотрим, какие из предложенных ниже методов могут быть присвоены такому делегату.

public delegate void OnMy Command(object sender,MyCmd cmd,object Parameter);
void MyFunc1(object sender){}
int MyFunc2(object sender, MyCmd cmd,object Parameter){}
void MyFunc3(MyCmd cmd, object sender,object Parame-ter){}
void MyFunc4(object MyForm, MyCmd cmdOpen,object dev1){}

Методы MyFunc1, MyFunc2, MyFunc3 на роль делегата OnCommand не годятся. У первого параметров маловато, у второго возвращаемое значение не то, а у третьего параметры не в том порядке идут. А вот метод MyFunc4 вполне ему подойдет, а как называются параметры — не суть важно. Главное — чтобы у них тип совпадал.

События
Разобравшись с понятием делегата, мы теперь можем во всеоружии продолжать изучение понятия события (event). Как вы уже, наверно, догадались, событие — это и есть ссылка на делегат, определенный нами заранее. Предположим, нам потребовалось изнутри кода своего объекта сообщить внешнему миру о том, что у нас что-то произошло. Этим "что-то" может быть, к примеру, факт нажатия на кнопку. Для этого мы просто вызываем из своего метода описанный в нем делегат. Как это сделать, я рассказал вам чуть выше. Разница лишь в том, что делегат следует оформить определенным образом. Для того чтобы оформить в своем объекте событие вместо делегата, вам необходимо объявить в нем переменную, обладающую типом нужного делегата, и воспользоваться специальным синтаксисом, указывающим, что эта переменная является событием. Таким признаком описания события является кодовое слово event.

public event OnMyCommand OnCommand;
Вот и все, теперь наш делегат OnCommand известен другим объектам в нашей программе, и они могут подключить к ней свои собственные обработчики. В С# обработчики событий подключаются с помощью лексической конструкции "+=". Например, вот так:

....
MyClass my = new MyClass();
my.OnCommand+= new OnMy Command(HandleEvent);
...

Еще раз напомню: для того, чтобы ваш обработчик успешно подключился, необходимо, чтобы он совпадал по маске с делегатом, объявленным как тип события. То есть наш метод HandleEvent обязан (заметьте: обязан!) обладать тремя параметрами, причем первый и третий типа object, а второй — типа MyCmd. При этом ваш метод не должен возвращать какое-либо значение (иметь тип возвращаемого значения void). Ну, короче говоря, все как в делегате OnMyCommand.

Как именно будет называться ваш метод — обработчик события, роли не играет: называйте его как хотите. Я рекомендовал бы называть все подобные методы одинаково. Это облегчит вам впоследствии процесс программирования. Вам не придется мучительно вспоминать, как же вас угораздило обозвать в этом конкретном объекте метод, отвечающий за обработку конкретного события.
Подключаемые обработчики цепляются в приложении один за другим. То есть вы можете "повесить" на одно событие сразу несколько обработчиков. Все они будут вызваны последовательно, в порядке своего подключения. После того как "мавр сделал свое дело", и обработчик вам больше не нужен, вы можете отключить его от события, используя команду "-=", как показано в следующем примере:

public void HandleEvent1 (object sender,MyCmd cmd, object Parameter){
...
}
public void HandleEvent2 (object sender,MyCmd cmd, object Parameter){
...
}
public void HandleEvent3 (object sender,MyCmd cmd, object Parameter){
...
}
...
MyClass my = new MyClass();
my.OnCommand+= new OnMy Command(HandleEvent1);
my.OnCommand+= new OnMy Command(HandleEvent2);
my.OnCommand+= new OnMy Command(HandleEvent3);
...
my.OnCommand-= new OnMy Command(HandleEvent1);
my.OnCommand-= new OnMy Command(HandleEvent2);
my.OnCommand-= new OnMy Command(HandleEvent3);

Все три обработчика сначала подключаются к событию OnCommand, при каждом "срабатывании" события вызываются в порядке своего подключения, а затем отключаются от события.

Продолжение следует.
(с) Герман Иванов,
http://german2004.da.ru



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

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