Основы программирования на C#

         

Оператор if


Начнем с синтаксиса оператора if:

if(выражение_1) оператор_1 else if(выражение_2) оператор_2 ... else if(выражение_K) оператор_K else оператор_N

Какие особенности синтаксиса следует отметить? Выражения if должны заключаться в круглые скобки и быть булевого типа. Точнее, выражения должны давать значения true или false. Напомню, арифметический тип не имеет явных или неявных преобразований к булевому типу. По правилам синтаксиса языка С++, then-ветвь оператора следует сразу за круглой скобкой без ключевого слова then, типичного для большинства языков программирования. Каждый из операторов может быть блоком - в частности, if-оператором. Поэтому возможна и такая конструкция:

if(выражение1) if(выражение2) if(выражение3) ...

Ветви else и if, позволяющие организовать выбор из многих возможностей, могут отсутствовать. Может быть опущена и заключительная else-ветвь. В этом случае краткая форма оператора if задает альтернативный выбор - делать или не делать - выполнять или не выполнять then-оператор.

Семантика оператора if проста и понятна. Выражения if проверяются в порядке их написания. Как только получено значение true, проверка прекращается и выполняется оператор (это может быть блок), который следует за выражением, получившим значение true. С завершением этого оператора завершается и оператор if. Ветвь else, если она есть, относится к ближайшему открытому if.



Оператор присваивания


Как в языке С++, так и в C# присваивание формально считается операцией. Вместе с тем запись:

X = expr;

следует считать настоящим оператором присваивания, так же, как и одновременное присваивание со списком переменных в левой части:

X1 = X2 = ... = Xk = expr;

В отличие от языка C++ появление присваивания в выражениях C# хотя и допустимо, но практически не встречается. Например, запись:

if(x = expr)...

часто используемая в С++, в языке C# в большинстве случаев будет воспринята как ошибка еще на этапе компиляции.

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



Оператор return


Еще одним оператором, относящимся к группе операторов перехода, является оператор return, позволяющий завершить выполнение процедуры или функции. Его синтаксис:

return [выражение];

Для функций его присутствие и аргумент обязательны, поскольку выражение в операторе return задает значение, возвращаемое функцией.



Оператор switch


Частным, но важным случаем выбора из нескольких вариантов является ситуация, при которой выбор варианта определяется значениями некоторого выражения. Соответствующий оператор C#, унаследованный от C++, но с небольшими изменениями в синтаксисе, называется оператором switch. Вот его синтаксис:

switch(выражение) { case константное_выражение_1: [операторы_1 оператор_перехода_1] ... case константное_выражение_K: [операторы_K оператор_перехода_K] [default: операторы_N оператор_перехода_N] }

Ветвь default может отсутствовать. Заметьте, по синтаксису допустимо, чтобы после двоеточия следовала пустая последовательность операторов, а не последовательность, заканчивающаяся оператором перехода. Константные выражения в case должны иметь тот же тип, что и switch-выражение.

Семантика оператора switch чуть запутана. Вначале вычисляется значение switch-выражения. Затем оно поочередно в порядке следования case сравнивается на совпадение с константными выражениями. Как только достигнуто совпадение, выполняется соответствующая последовательность операторов case-ветви. Поскольку последний оператор этой последовательности является оператором перехода (чаще всего это оператор break), то обычно он завершает выполнение оператора switch. Использование операторов перехода - это плохая идея. Таким оператором может быть оператор goto, передающий управление другой case-ветви, которая, в свою очередь, может передать управление еще куда-нибудь, получая блюдо "спагетти" вместо хорошо структурированной последовательности операторов. Семантика осложняется еще и тем, что case-ветвь может быть пустой последовательностью операторов. Тогда в случае совпадения константного выражения этой ветви со значением switch-выражения будет выполняться первая непустая последовательность очередной case-ветви. Если значение switch-выражения не совпадает ни с одним константным выражением, то выполняется последовательность операторов ветви default, если же таковой ветви нет, то оператор switch эквивалентен пустому оператору.

Полагаю, что оператор switch - это самый неудачный оператор языка C# как с точки зрения синтаксиса, так и семантики. Неудачный синтаксис порождает запутанную семантику, являющуюся источником плохого стиля программирования. Понять, почему авторов постигла неудача, можно, оправдать - нет. Дело в том, что оператор унаследован от С++, где его семантика и синтаксис еще хуже. В языке C# синтаксически каждая case-ветвь должна заканчиваться оператором перехода (забудем на минуту о пустой последовательности), иначе возникнет ошибка периода компиляции. В языке С++ это правило не является синтаксически обязательным, хотя на практике применяется та же конструкция с конечным оператором break. При его отсутствии управление "проваливается" в следующую case-ветвь. Конечно, профессионал может с успехом использовать этот трюк, но в целом ни к чему хорошему это не приводит. Борясь с этим, в C# потребовали обязательного включения оператора перехода, завершающего ветвь. Гораздо лучше было бы, если бы последним оператором мог быть только оператор break, писать его было бы не нужно и семантика стала бы прозрачной - при совпадении значений двух выражений выполняются операторы соответствующей case-ветви, при завершении которой завершается и оператор switch.

<




Еще одна неудача в синтаксической конструкции switch связана с существенным ограничением, накладываемым на case-выражения, которые могут быть только константным выражением. Уж если изменять оператор, то гораздо лучше было бы использовать синтаксис и семантику Visual Basic, где в case-выражениях допускается список, каждое из выражений которого может задавать диапазон значений.

Разбор случаев - это часто встречающаяся ситуация в самых разных задачах. Применяя оператор switch, помните о недостатках его синтаксиса, используйте его в правильном стиле. Заканчивайте каждую case-ветвь оператором break, но не применяйте goto.

Когда разбор случаев предполагает проверку попадания в некоторый диапазон значений, приходится прибегать к оператору if для формирования специальной переменной. Этот прием демонстрируется в следующем примере, где идет работа над данными нашего класса Testing:

/// <summary> /// Определяет период в зависимости от возраста - age /// Использование ветвящегося оператора if /// </summary> public void SetPeriod() { if ((age > 0)&& (age <7))period=1; else if ((age >= 7)&& (age <17))period=2; else if ((age >= 17)&& (age <22))period=3; else if ((age >= 22)&& (age <27))period=4; else if ((age >= 27)&& (age <37))period=5; else period =6; }

Этот пример демонстрирует применение ветвящегося оператора if. С содержательной точки зрения он интересен тем, что в поля класса пришлось ввести специальную переменную period, позволяющую в дальнейшем использовать разбор случаев в зависимости от периода жизни:

/// <summary> /// Определяет статус в зависимости от периода - period /// Использование разбора случаев - оператора Switch /// </summary> public void SetStatus() { switch (period) { case 1: status = "child"; break; case 2: status = "schoolboy"; break; case 3: status = "student"; break; case 4: status = "junior researcher"; break; case 5: status = "senior researcher"; break; case 6: status = "professor"; break; default : status = "не определен"; break; } Console.WriteLine("Имя = {0}, Возраст = {1}, Статус = {2}", name, age, status); }//SetStatus



Этот пример демонстрирует корректный стиль использования оператора switch. В следующем примере показана роль пустых последовательностей операторов case-ветвей для организации списка выражений одного варианта:

/// <summary> /// Разбор случаев с использованием списков выражений /// </summary> /// <param name="operation">операция над аргументами</param> /// <param name="arg1">первый аргумент бинарной операции</param> /// <param name="arg2">второй аргумент бинарной операции</param> /// <param name="result">результат бинарной операции</param> public void ExprResult(string operation,int arg1, int arg2, ref int result) { switch (operation) { case "+": case "Plus": case "Плюс": result = arg1 + arg2; break; case "-": case "Minus": case "Минус": result = arg1 - arg2; break; case "*": case "Mult": case "Умножить": result = arg1 * arg2; break; case "/": case "Divide": case "Div": case "разделить": case "Делить": result = arg1 / arg2; break; default: result = 0; Console.WriteLine("Операция не определена"); break; } Console.WriteLine ("{0} ({1}, {2}) = {3}", operation, arg1, arg2, result); }//ExprResult


Операторы break и continue


В структурном программировании признаются полезными "переходы вперед" (но не назад), позволяющие при выполнении некоторого условия выйти из цикла, из оператора выбора, из блока. Для этой цели можно использовать оператор goto, но лучше применять специально предназначенные для этих целей операторы break и continue.

Оператор break может стоять в теле цикла или завершать case-ветвь в операторе switch. Пример его использования в операторе switch уже демонстрировался. При выполнении оператора break в теле цикла завершается выполнение самого внутреннего цикла. В теле цикла, чаще всего, оператор break помещается в одну из ветвей оператора if, проверяющего условие преждевременного завершения цикла:

public void Jumps() { int i = 1, j=1; for(i =1; i<100; i++) { for(j = 1; j<10;j++) { if (j>=3) break; } Console.WriteLine("Выход из цикла j при j = {0}", j); if (i>=3) break; } Console.WriteLine("Выход из цикла i при i= {0}", i); }//Jumps

Оператор continue используется только в теле цикла. В отличие от оператора break, завершающего внутренний цикл, continue осуществляет переход к следующей итерации этого цикла.



Операторы языка C#


Состав операторов языка C#, их синтаксис и семантика унаследованы от языка С++. Как и положено, потомок частично дополнил состав, переопределил синтаксис и семантику отдельных операторов, постарался улучшить характеристики языка во благо программиста. Посмотрим, насколько это удалось языку C#.



Операторы перехода


Операторов перехода, позволяющих прервать естественный порядок выполнения операторов блока, в языке C# имеется несколько.



Операторы выбора


Как в С++ и других языках программирования, в языке C# для выбора одной из нескольких возможностей используются две конструкции - if и switch. Первую из них обычно называют альтернативным выбором, вторую - разбором случаев.



Описание методов (процедур и функций). Синтаксис


Синтаксически в описании метода различают две части - описание заголовка и описание тела метода:

заголовок_метода тело_метода

Рассмотрим синтаксис заголовка метода:

[атрибуты][модификаторы]{void| тип_результата_функции} имя_метода([список_формальных_аргументов])

Имя метода и список формальных аргументов составляют сигнатуру метода. Заметьте, в сигнатуру не входят имена формальных аргументов - здесь важны типы аргументов. В сигнатуру не входит и тип возвращаемого результата.

Квадратные скобки (метасимволы синтаксической формулы) показывают, что атрибуты и модификаторы могут быть опущены при описании метода. Подробное их рассмотрение будет дано в лекциях, посвященных описанию классов. Сейчас же упомяну только об одном из модификаторов - модификаторе доступа. У него четыре возможных значения, из которых пока рассмотрим только два - public и private. Модификатор public показывает, что метод открыт и доступен для вызова клиентами и потомками класса. Модификатор private говорит, что метод предназначен для внутреннего использования в классе и доступен для вызова только в теле методов самого класса. Заметьте, если модификатор доступа опущен, то по умолчанию предполагается, что он имеет значение private и метод является закрытым для клиентов и потомков класса.

Обязательным при описании заголовка является указание типа результата, имени метода и круглых скобок, наличие которых необходимо и в том случае, если сам список формальных аргументов отсутствует. Формально тип результата метода указывается всегда, но значение void однозначно определяет, что метод реализуется процедурой. Тип результата, отличный от void, указывает на функцию. Вот несколько простейших примеров описания методов:

void A() {...}; int B(){...}; public void C(){...};

Методы A и B являются закрытыми, а метод С - открыт. Методы A и С реализованы процедурами, а метод B - функцией, возвращающей целое значение.



Определенное присваивание


Присваивание в языке C# называется определенным присваиванием (definite assignment). В этом термине отражен тот уже обсуждавшийся факт, что все используемые в выражениях переменные должны быть ранее инициализированы и иметь определенные значения. Единственное, за чем компилятор не следит, так это за инициализацией переменных массива. Для них используется инициализация элементов, задаваемая по умолчанию. Приведу пример:

//определенное присваивание int an =0 ; //переменные должны быть инициализированы for (int i= 0;i<5;i++) {an =i+1;} x+=an; z+=an; y = an; string[] ars = new string[3]; double[] ard = new double[3]; for (int i= 0;i<3;i++) { //массивы могут быть без инициализации ard[i] += i+1; ars[i] += i.ToString()+1; Console.WriteLine("ard[" +i + "]=" +ard[i] + "; ars[" +i + "]=" +ars[i]); }

Заметьте, в этом фрагменте переменная an обязана быть инициализированной, а массивы ard и ars не инициализируются и спокойно участвуют в вычислениях.



Организация интерфейса


Практически все проекты, построенные в наших лекциях, были консольными приложениями. В реальной жизни консольные проекты - это большая редкость. Причина, по которой из 12 возможных типов проектов мы выбирали наименее используемый, понятна. Нашей целью являлось изучение свойств языка, классов библиотеки FCL, для этих целей консольный проект вполне подходит, позволяя избегать введения не относящихся к сути дела деталей. Теперь цель достигнута - основные средства языка C# рассмотрены, учебный курс завершается. Остались важные темы, требующие более подробного рассмотрения, такие, как, например, работа с атрибутами, создание собственных атрибутов, класс Reflection, работа с файлами и базами данных; но все это предмет будущего курса. Тем не менее, нельзя окончить этот курс, не посвятив две последние лекции Windows-приложениям. Мне бы хотелось, чтобы активные слушатели (читатели) все консольные проекты переделали в Windows-проекты, построив подходящий для них интерфейс.

Первое знакомство с Windows-проектами состоялось в лекции 2, я настоятельно рекомендую перечитать ее, прежде чем продолжить чтение данной лекции. Вкратце напомню, как создается и выполняется Windows-проект. По умолчанию он содержит класс Form1 - наследника класса Form. Этот класс содержит точку входа в проект - процедуру Main, вызывающую статический метод Run класса Application, который создает объект класса Form1 и открывает форму - видимый образ объекта - для интерактивной работы пользователя. Открываемая форма содержит пользовательский интерфейс - окошки, кнопки, списки, другие элементы управления, меню . Все эти элементы способны реагировать на события, возникающие при выполнении пользователем каких-либо действий - нажатии кнопок, ввода текста, выбора пунктов меню.



Организация интерфейса


Создадим теперь интерфейс, позволяющий конечному пользователю работать с объектами наших классов. Как всегда, интерфейс создавался вручную в режиме проектирования. На форме я создал меню с большим числом команд и инструментальную панель с 18 кнопками, команды которых повторяли основную команду меню. Описывать процесс создания интерфейса не буду - он подробно рассмотрен в предыдущей главе. Поскольку вся работа по созданию интерфейса транслируется в программный код формы, то просто приведу этот достаточно длинный текст почти без всяких купюр:

using System; using System.Drawing; using System.Collections; using System.ComponentModel; using System.Windows.Forms; using System.Data; using Shapes;

namespace Final { /// <summary> /// Эта форма обеспечивает интерфейс для создания, /// рисования, показа, перемещения, сохранения в списке /// и выполнения других операций над объектами семейства /// геометрических фигур. Форма имеет меню и /// инструментальные панели. /// </summary> public class Form1 : System.Windows.Forms.Form { //fields Graphics graphic; Brush brush, clearBrush; Pen pen, clearPen; Color color; Figure current; TwoWayList listFigure; private System.Windows.Forms.MainMenu mainMenu1; private System.Windows.Forms.ImageList imageList1; private System.Windows.Forms.ToolBar toolBar1; private System.Windows.Forms.MenuItem menuItem1; // аналогичные определения для других элементов меню private System.Windows.Forms.MenuItem menuItem35; private System.Windows.Forms.ToolBarButton toolBarButton1; // аналогичные определения для других командных кнопок private System.Windows.Forms.ToolBarButton toolBarButton18; private System.ComponentModel.IContainer components; public Form1() { InitializeComponent(); InitFields(); } void InitFields() { graphic = CreateGraphics(); color = SystemColors.ControlText; brush = new SolidBrush(color); clearBrush = new SolidBrush(SystemColors.Control); pen = new Pen(color); clearPen = new Pen(SystemColors.Control); listFigure = new TwoWayList(); current = new Person(20, 50, 50); } /// <summary> /// Clean up any resources being used. /// </summary> protected override void Dispose( bool disposing ) { if( disposing ) { if (components != null) { components.Dispose(); } } base.Dispose( disposing ); } #region Windows Form Designer generated code /// <summary> /// Required method for Designer support - do not modify /// the contents of this method with the code editor. /// </summary> private void InitializeComponent() { // Код, инициализирующий компоненты и построенный // дизайнером, опущен } #endregion /// <summary> /// Точка входа в приложение - процедура Main, /// запускающая форму /// </summary> [STAThread] static void Main() { Application.Run(new Form1()); } private void menuItem7_Click(object sender, System.EventArgs e) { createEllipse(); } void createEllipse() { //clear old figure if (current != null) current.Show(graphic, clearPen, clearBrush); //create ellipse current = new Ellipse(50, 30, 180,180); } private void menuItem8_Click(object sender, System.EventArgs e) { createCircle(); } void createCircle() { //clear old figure if (current != null) current.Show(graphic, clearPen, clearBrush); //create circle current = new Circle(30, 180,180); } private void menuItem9_Click(object sender, System.EventArgs e) { createLittleCircle(); } void createLittleCircle() { //clear old figure if (current != null) current.Show(graphic, clearPen, clearBrush); //create littlecircle current = new LittleCircle(180,180); } private void menuItem10_Click(object sender, System.EventArgs e) { createRectangle(); } void createRectangle() { //clear old figure if (current != null) current.Show(graphic, clearPen, clearBrush); //create rectangle current = new Rect(50, 30, 180,180); } private void menuItem11_Click(object sender, System.EventArgs e) { createSquare(); } void createSquare() { //clear old figure if (current != null) current.Show(graphic, clearPen, clearBrush); //create square current = new Square(30, 180,180); } private void menuItem12_Click(object sender, System.EventArgs e) { createPerson(); } void createPerson() { //clear old figure if (current != null) current.Show(graphic, clearPen, clearBrush); //create person current = new Person(20, 180,180); } private void menuItem13_Click(object sender, System.EventArgs e) { showCurrent(); } void showCurrent() { //Show current current.Show(graphic, pen, brush); } private void menuItem14_Click(object sender, System.EventArgs e) { clearCurrent(); } void clearCurrent() { //Clear current current.Show(graphic, clearPen, clearBrush); } private void menuItem17_Click(object sender, System.EventArgs e) { incScale(); } void incScale() { //Increase scale current.Show(graphic, clearPen, clearBrush); current.Scale(1.5); current.Show(graphic, pen, brush); } private void menuItem18_Click(object sender, System.EventArgs e) { decScale(); } void decScale() { //Decrease scale current.Show(graphic, clearPen, clearBrush); current.Scale(2.0/3); current.Show(graphic, pen, brush); } private void menuItem19_Click(object sender, System.EventArgs e) { moveLeft(); } void moveLeft() { //Move left current.Show(graphic, clearPen, clearBrush); current.Move(-20,0); current.Show(graphic, pen, brush); } private void menuItem20_Click(object sender, System.EventArgs e) { moveRight(); } void moveRight() { //Move right current.Show(graphic, clearPen, clearBrush); current.Move(20,0); current.Show(graphic, pen, brush); } private void menuItem21_Click(object sender, System.EventArgs e) { moveTop(); } void moveTop() { //Move top current.Show(graphic, clearPen, clearBrush); current.Move(0,-20); current.Show(graphic, pen, brush); } private void menuItem22_Click(object sender, System.EventArgs e) { moveDown(); } void moveDown() { //Move down current.Show(graphic, clearPen, clearBrush); current.Move(0, 20); current.Show(graphic, pen, brush); } private void menuItem23_Click(object sender, System.EventArgs e) { //choose color ColorDialog dialog = new ColorDialog(); if (dialog.ShowDialog() ==DialogResult.OK) color =dialog.Color; pen = new Pen(color); brush = new SolidBrush(color); } private void menuItem24_Click(object sender, System.EventArgs e) { //Red color color =Color.Red; pen = new Pen(color); brush = new SolidBrush(color); } private void menuItem25_Click(object sender, System.EventArgs e) { //Green color color =Color.Green; pen = new Pen(color); brush = new SolidBrush(color); } private void menuItem26_Click(object sender, System.EventArgs e) { //Blue color color =Color.Blue; pen = new Pen(color); brush = new SolidBrush(color); } private void menuItem27_Click(object sender, System.EventArgs e) { //Black color color =Color.Black; pen = new Pen(color); brush = new SolidBrush(color); } private void menuItem28_Click(object sender, System.EventArgs e) { //Gold color color =Color.Gold; pen = new Pen(color); brush = new SolidBrush(color); } private void menuItem29_Click(object sender, System.EventArgs e) { //put_left: добавление фигуры в список listFigure.put_left(current); } private void menuItem30_Click(object sender, System.EventArgs e) { //put_right: добавление фигуры в список listFigure.put_right(current); } private void menuItem31_Click(object sender, System.EventArgs e) { //remove: удаление фигуры из списка if(!listFigure.empty()) listFigure.remove(); } private void menuItem32_Click(object sender, System.EventArgs e) { goPrev(); } void goPrev() { //go_prev: передвинуть курсор влево if(!(listFigure.Index == 1)) { listFigure.go_prev(); current = listFigure.item(); } } private void menuItem33_Click(object sender, System.EventArgs e) { goNext(); } void goNext() { //go_next: передвинуть курсор вправо if( !(listFigure.Index == listFigure.Count)) { listFigure.go_next(); current = listFigure.item(); } } private void menuItem34_Click(object sender, System.EventArgs e) { //go_first listFigure.start(); if(!listFigure.empty()) current = listFigure.item(); } private void menuItem35_Click(object sender, System.EventArgs e) { //go_last listFigure.finish(); if(!listFigure.empty()) current = listFigure.item(); } private void menuItem15_Click(object sender, System.EventArgs e) { showList(); } void showList() { //Show List listFigure.start(); while(listFigure.Index <= listFigure.Count) { current = listFigure.item(); current.Show(graphic,pen,brush); listFigure.go_next(); } listFigure.finish(); } private void menuItem16_Click(object sender, System.EventArgs e) { clearList(); } void clearList() { //Clear List listFigure.start(); while(!listFigure.empty()) { current = listFigure.item(); current.Show(graphic,clearPen,clearBrush); listFigure.remove(); } } private void Form1_MouseMove(object sender, System.Windows.Forms.MouseEventArgs e) { if((current != null) && current.dragged_figure) { current.Show(graphic,clearPen,clearBrush); Point pt = new Point(e.X, e.Y); current.center_figure = pt; current.Show(graphic,pen,brush); } } private void Form1_MouseUp(object sender, System.Windows.Forms.MouseEventArgs e) { current.dragged_figure = false; } private void Form1_MouseDown(object sender, System.Windows.Forms.MouseEventArgs e) { Point mousePoint = new Point (e.X, e.Y); Rectangle figureRect = current.Region_Capture(); if ((current != null) && (figureRect.Contains(mousePoint))) current.dragged_figure = true; } protected override void OnPaint(System.Windows.Forms.PaintEventArgs e) { //show current figure current.Show(graphic, pen, brush); } private void toolBar1_ButtonClick(object sender, System.Windows.Forms.ToolBarButtonClickEventArgs e) { int buttonNumber = toolBar1.Buttons.IndexOf(e.Button); switch (buttonNumber) { case 0: createEllipse(); break; case 1: createCircle(); break; case 2: createLittleCircle(); break; case 3: createRectangle(); break; case 4: createSquare(); break; case 5: createPerson(); break; case 6: showCurrent(); break; case 7: clearCurrent(); break; case 8: showList(); break; case 9: clearList(); break; case 10: incScale(); break; case 11: decScale(); break; case 12: moveLeft(); break; case 13: moveRight(); break; case 14: moveTop(); break; case 15: moveDown(); break; case 16: goNext(); break; case 17: goPrev(); break; } } } }




Команд меню и кнопок в нашем интерфейсе много, поэтому много и обработчиков событий, что приводит к разбуханию кода. Но каждый из обработчиков событий довольно прост. Ограничусь кратким описанием главного меню:

команды пункта главного меню Create позволяют создавать геометрические фигуры разных классов;команды пункта главного меню Show позволяют показать или стереть текущую фигуру или все фигуры, сохраняемые в списке;две команды пункта Scale позволяют изменить масштаб фигуры (увеличить ее или уменьшить);команды пункта Move позволяют перемещать текущую фигуру в четырех направлениях;команды пункта Color позволяют либо задать цвет фигур в диалоговом окне, либо выбрать один из предопределенных цветов;группа команд пункта List позволяет помещать текущую фигуру в список, перемещаться по списку и удалять из списка ту или иную фигуру;командные кнопки инструментальной панели соответствуют наиболее важным командам меню;реализована возможность перетаскивания фигур по экрану мышью.

В заключение взгляните, как выглядит форма в процессе работы с объектами:


Рис. 25.1.  Финальный проект. Форма в процессе работы


Организация меню в формах


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

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

Кроме структуры, заданной главным меню, в форме и в элементах управления разрешается организовывать контекстные меню, появляющиеся (всплывающие) при нажатии правой кнопки мыши.



Основные методы


У класса StringBuilder методов значительно меньше, чем у класса String. Это и понятно - класс создавался с целью дать возможность изменять значение строки. По этой причине у класса есть основные методы, позволяющие выполнять такие операции над строкой как вставка, удаление и замена подстрок, но нет методов, подобных поиску вхождения, которые можно выполнять над обычными строками. Технология работы обычно такова: конструируется строка класса StringBuilder; выполняются операции, требующие изменение значения; полученная строка преобразуется в строку класса String; над этой строкой выполняются операции, не требующие изменения значения строки. Давайте чуть более подробно рассмотрим основные методы класса StringBuilder:

public StringBuilder Append (<объект>). К строке, вызвавшей метод, присоединяется строка, полученная из объекта, который передан методу в качестве параметра. Метод перегружен и может принимать на входе объекты всех простых типов, начиная от char и bool до string и long. Поскольку объекты всех этих типов имеют метод ToString, всегда есть возможность преобразовать объект в строку, которая и присоединяется к исходной строке. В качестве результата возвращается ссылка на объект, вызвавший метод. Поскольку возвращаемую ссылку ничему присваивать не нужно, то правильнее считать, что метод изменяет значение строки;public StringBuilder Insert (int location,<объект>). Метод вставляет строку, полученную из объекта, в позицию, указанную параметром location. Метод Append является частным случаем метода Insert;public StringBuilder Remove (int start, int len). Метод удаляет подстроку длины len, начинающуюся с позиции start;public StringBuilder Replace (string str1,string str2). Все вхождения подстроки str1 заменяются на строку str2;public StringBuilder AppendFormat (<строка форматов>, <объекты>). Метод является комбинацией метода Format класса String и метода Append. Строка форматов, переданная методу, содержит только спецификации форматов. В соответствии с этими спецификациями находятся и форматируются объекты. Полученные в результате форматирования строки присоединяются в конец исходной строки.




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

//Методы Insert, Append, AppendFormat StringBuilder strbuild = new StringBuilder(); string str = "это это не "; strbuild.Append(str); strbuild.Append(true); strbuild.Insert(4,false); strbuild.Insert(0,"2*2=5 - "); Console.WriteLine(strbuild); string txt = "А это пшеница, которая в темном чулане хранится," +" в доме, который построил Джек!"; StringBuilder txtbuild = new StringBuilder(); int num =1; foreach(string sub in txt.Split(',')) { txtbuild.AppendFormat(" {0}: {1} ", num++,sub); } str = txtbuild.ToString(); Console.WriteLine(str);

В этом фрагменте кода конструируются две строки. Первая из них создается из строк и булевых значений true и false. Для конструирования используются методы Insert и Append. Вторая строка конструируется в цикле с применением метода AppendFormat. Результатом этого конструирования является строка, в которой простые предложения исходного текста пронумерованы.

Обратите внимание, что сконструированная вторая строка передается в обычную строку класса String. Никаких проблем преобразования строк одного класса в другой класс не возникает, поскольку все объекты, в том числе, объекты класса StringBuilder, обладают по определению методом ToString.

Обратите внимание, как выглядят результаты работы.


Рис. 14.4.  Операции и методы класса StringBuilder


Открытость


Среда разработки теперь является открытой языковой средой. Это означает, что наряду с языками программирования, включенными в среду фирмой Microsoft - Visual C++ .Net (с управляемыми расширениями), Visual C# .Net, J# .Net, Visual Basic .Net, - в среду могут добавляться любые языки программирования, компиляторы которых создаются другими фирмами-производителями. Таких расширений среды Visual Studio сделано уже достаточно много, практически они существуют для всех известных языков - Fortran и Cobol, RPG и Component Pascal, Oberon и SmallTalk. Я у себя на компьютере включил в среду компилятор одного из лучших объектных языков - языка Eiffel.

Открытость среды не означает полной свободы. Все разработчики компиляторов при включении нового языка в среду разработки должны следовать определенным ограничениям. Главное ограничение, которое можно считать и главным достоинством, состоит в том, что все языки, включаемые в среду разработки Visual Studio .Net, должны использовать единый каркас - Framework .Net. Благодаря этому достигаются многие желательные свойства: легкость использования компонентов, разработанных на различных языках; возможность разработки нескольких частей одного приложения на разных языках; возможность бесшовной отладки такого приложения; возможность написать класс на одном языке, а его потомков - на других языках. Единый каркас приводит к сближению языков программирования, позволяя вместе с тем сохранять их индивидуальность и имеющиеся у них достоинства. Преодоление языкового барьера - одна из важнейших задач современного мира. Благодаря единому каркасу, Visual Studio .Net в определенной мере решает эту задачу в мире программистов.



Отладка


Что должно делать для создания корректного и устойчивого программного продукта? Как минимум, необходимо:

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



Отладка и инструментальная среда Visual Studio .Net


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



Отладочная печать и условная компиляция


Одним из основных средств отладки является отладочная печать, позволяющая получить данные о ходе и состоянии процесса вычислений. Обычно разрабатываются специальные отладочные методы, вызываемые в критических точках программы - на входе и выходе программных модулей, на входе и выходе циклов и так далее. Искусство отладки в том и состоит, чтобы получить нужную информацию о прячущихся ошибках, проявляющихся, возможно, только в редких ситуациях.

Хотелось бы иметь легкий механизм управления отладочными методами, позволяющий включать при необходимости те или иные инструменты. Для этого можно воспользоваться механизмом условной компиляции, встроенным в язык C#. Этот механизм состоит из двух частей. К проекту, точнее, к конфигурации проекта можно добавить специальные константы условной компиляции. Вызов отладочного метода может быть сделан условным. Если соответствующая константа компиляции определена, то происходит компиляция вызова метода и он будет вызываться при выполнении проекта. Если же константа не определена (выключена), то вызов метода даже не будет компилироваться и никаких динамических проверок - вызывать метод или нет - делаться не будет.

Как задавать константы компиляции? Напомню, что проекты в Visual Studio существуют в нескольких конфигурациях. В ходе работы с проектом можно легко переключаться с одной конфигурации на другую, после чего она становится активной, можно изменять настройки конфигурации, можно создать собственные конфигурации проекта. По умолчанию проект создается в двух конфигурациях - Debug и Release, первая из которых предназначена для отладки, вторая - для окончательных вычислений. Первая не предполагает оптимизации и в ней определены две константы условной компиляции - DEBUG и TRACE, во второй - определена только константа TRACE. Отладочная версия может содержать вызовы, зависящие от константы DEBUG, которые будут отсутствовать в финальной версии. Используя страницу свойств, к конфигурации проекта можно добавлять новые константы компиляции.

В лекции 2 рассказывалось, как добраться до страницы свойств проекта. Взгляните еще раз на рис. 2.3 этой лекции, где показана страница свойств, и обратите внимание на первую строчку, содержащую список констант условной компиляции активной конфигурации (в данном случае - Debug). К этому списку можно добавлять собственные константы.

<

Можно также задавать константы условной компиляции в начале модуля проекта вперемешку с предложениями using. Предложение define позволяет определить новую константу:

#define COMPLEX

Как используются константы условной компиляции? В языке С++, где имеется подобный механизм, определен специальный препроцессорный IF-оператор, анализирующий, задана константа или нет. В языке C# используется вместо этого гораздо более мощный механизм. Как известно, методы C# обладают набором атрибутов, придающих методу разные свойства. Среди встроенных атрибутов языка есть атрибут Conditional, аргументом которого является строка, задающая имя константы:

[Conditional ("COMPLEX")] public void ComplexMethod () {...}

Если константа условной компиляции COMPLEX определена для активной конфигурации проекта, то произойдет компиляция вызова метода ComplexMethod, когда он встретится в тексте программы. Если же такая константа отсутствует в конфигурации, то вызов метода игнорируется.

На методы, для которых возможно задание атрибута Conditional, накладывается ряд ограничений. Метод не должен быть:

функцией, возвращающей значение;методом интерфейса;методом с модификатором override. Возможно его задание для virtual-метода. В этом случае атрибут наследуется методами потомков.

Атрибут Conditional, обычно с аргументом DEBUG, сопровождает модули, написанные для целей отладки. Но использование этого атрибута не ограничивается интересами отладки. Зачастую проект может использоваться в нескольких вариантах, например, в облегченном и более сложном. Методы, вызываемые в сложных ситуациях, например, ComplexMethod, имеющий атрибут условной компиляции, будут вызываться только в той конфигурации, где определена константа COMPLEX.

Приведу пример работы с отладочными методами. Рассмотрим класс, в котором определены три метода, используемые при отладке:

public class DebugPrint { [Conditional("DEBUG")] static public void PrintEntry(string name) { Console.WriteLine("Начал работать метод " + name); } [Conditional("DEBUG")] static public void PrintExit(string name) { Console.WriteLine("Закончил работать метод " + name); } [Conditional("DEBUG")] static public void PrintObject(object obj, string name) { Console.WriteLine("Объект {0}: {1}", name, obj.ToString()); } }



В классе Testing определено поле класса:

int state = 1;

и группа методов:

public void TestDebugPrint() { DebugPrint.PrintEntry("Testing.TestDebugPrint"); PubMethod(); DebugPrint.PrintObject(state, "Testing.state"); DebugPrint.PrintExit("Testing.TestDebugPrint"); } void InMethod1() { DebugPrint.PrintEntry("InMethod1"); // body DebugPrint.PrintExit("InMethod1"); } void InMethod2() { DebugPrint.PrintEntry("InMethod2"); // body DebugPrint.PrintExit("InMethod2"); } public void PubMethod() { DebugPrint.PrintEntry("PubMethod"); InMethod1(); state++; InMethod2(); DebugPrint.PrintExit("PubMethod"); }

Этот пример демонстрирует трассировку хода вычислений, для чего в начало и конец каждого метода вставлены вызовы отладочных методов, снабжающие нас информацией о ходе вычислений. Такая трассировка иногда бывает крайне полезной на этапе отладки, но, естественно, она не должна присутствовать в финальной версии проекта. Взгляните на результаты, полученные при вызове метода TestDebugPrint в конфигурации Debug.


Рис. 23.1.  Трассировка вычислений в процессе отладки

При переходе к конфигурации Release отладочная информация появляться не будет.


Отношение вложенности


Рассмотрим два класса A и B, связанных отношением вложенности. Оба класса применяются для демонстрации идей и потому устроены просто, не неся особой смысловой нагрузки. Пусть класс-поставщик A уже построен. У класса два поля, конструктор, один статический и один динамический метод. Вот его текст:

public class ClassA { public ClassA(string f1, int f2) { fieldA1 = f1; fieldA2 = f2; } public string fieldA1; public int fieldA2; public void MethodA() { Console.WriteLine( "Это класс A"); Console.WriteLine ("поле1 = {0}, поле2 = {1}", fieldA1, fieldA2); } public static void StatMethodA() { string s1 = "Статический метод класса А"; string s2 = "Помните: 2*2 = 4"; Console.WriteLine(s1 + " ***** " + s2); } }

Построим теперь класс B - клиента класса A. Класс будет устроен похожим образом, но в дополнение будет иметь одним из своих полей объект inner класса A:

public class ClassB { public ClassB(string f1A, int f2A, string f1B, int f2B) { inner = new ClassA(f1A, f2A); fieldB1 = f1B; fieldB2 = f2B; } ClassA inner; public string fieldB1; public int fieldB2; public void MethodB1() { inner.MethodA(); Console.WriteLine( "Это класс B"); Console.WriteLine ("поле1 = {0}, поле2 = {1}", fieldB1, fieldB2); } }

Обратите внимание: конструктор клиента (класса B) отвечает за инициализацию полей класса, поэтому он должен создать объект поставщика (класса A), вызывая, как правило, конструктор поставщика. Если для создания объектов поставщика требуются аргументы, то они должны передаваться конструктору клиента, как это сделано в нашем примере.

После того как конструктор создал поле - объект поставщика - методы класса могут использовать этот объект, вызывая доступные клиенту методы и поля класса поставщика. Метод класса B - MethodB1 начинает свою работу с вызова: inner.MethodA, используя сервис, поставляемый методом класса A.



Отношения "является" и "имеет"


При проектировании классов часто возникает вопрос, какое же отношение между классами нужно построить. Рассмотрим совсем простой пример двух классов - Square и Rectangle, описывающих квадраты и прямоугольники. Наверное, понятно, что эти классы следует связать скорее отношением наследования, чем вложенности; менее понятным остается вопрос, а какой из этих двух классов следует сделать родительским. Еще один пример двух классов - Car и Person, описывающих автомобиль и персону. Какими отношениями с этими классами должен быть связан класс Person_of_Car, описывающий владельца машины? Может ли он быть наследником обоих классов? Найти правильные ответы на эти вопросы проектирования классов помогает понимание того, что отношение "клиент-поставщик" задает отношение "имеет" ("has"), а отношение наследования задает отношение "является" ("is a"). В случае классов Square и Rectangle понятно, что каждый объект квадрат "является" прямоугольником, поэтому между этими классами имеет место отношение наследования, и родительским классом является класс Rectangle, а класс Square является его потомком.

В случае автомобилей, персон и владельцев авто также понятно, что владелец "имеет" автомобиль и "является" персоной. Поэтому класс Person_of_Car является клиентом класса Car и наследником класса Person.



Отношения между классами


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

Определение 1. Классы А и В находятся в отношении "клиент-поставщик", если одним из полей класса В является объект класса А. Класс А называется поставщиком класса В, класс В называется клиентом класса А.

Определение 2. Классы А и В находятся в отношении "родитель - наследник", если при объявлении класса В класс А указан в качестве родительского класса. Класс А называется родителем класса В, класс В называется наследником класса А.

Оба отношения - наследования и вложенности - являются транзитивными. Если В - клиент А и С - клиент В, то отсюда следует, что С - клиент А. Если В - наследник А и С - наследник В, то отсюда следует, что С - наследник А.

Определения 1 и 2 задают прямых или непосредственных клиентов и поставщиков, прямых родителей и наследников. Вследствие транзитивности необходимо ввести понятие уровня. Прямые клиенты и поставщики, прямые родители и наследники относятся к соответствующему уровню 1 (клиенты уровня 1, поставщики уровня 1 и так далее). Затем следует рекурсивное определение: прямой клиент клиента уровня k относится к уровню k+1.

Для отношения наследования используется терминология, заимствованная из естественного языка. Прямые классы-наследники часто называются сыновними или дочерними классами. Непрямые родители называются предками, а их непрямые наследники - потомками.

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

Классы библиотеки FCL связаны как отношением вложенности, так и отношением наследования. Длинные цепочки наследования достаточно характерны для классов этой библиотеки.



Отношения между клиентами и поставщиками


Что могут делать клиенты и что могут делать поставщики? Класс-поставщик создает свойства (поля) и сервисы (методы), предоставляемые своим клиентам. Клиенты создают объекты поставщика. Вызывая доступные им методы и поля объектов, они управляют работой созданных объектов поставщика. Клиенты не могут ни повлиять на поведение методов поставщика, ни изменить состав предоставляемых им полей и методов, они не могут вызывать закрытые поставщиком поля и методы класса.

Класс-поставщик интересен клиентам своей открытой частью, составляющей интерфейс класса. Но большая часть класса может быть закрыта для клиентов - им незачем вникать в детали представления и в детали реализации. Сокрытие информации вовсе не означает, что разработчики класса не должны быть знакомы с тем, как все реализовано, хотя иногда и такая цель преследуется. В общем случае сокрытие означает, что классы-клиенты строят свою реализацию, основываясь только на интерфейсной части класса-поставщика. Поставщик закрывает поля и часть методов класса от клиентов, задавая для них атрибут доступа private или protected. Он может некоторые классы считать привилегированными, предоставляя им методы и поля, недоступные другим классам. В этом случае поля и методы, предназначенные для таких vip-персон, снабжаются атрибутом доступа internal, а классы с привилегиями должны принадлежать одной сборке.

В заключение построим тест, проверяющий работу с объектами классов A и B:

public void TestClientSupplier() { ClassB objB = new ClassB("AA",22, "BB",33); objB.MethodB1(); objB.MethodB2(); objB.MethodB3(); }

Результаты работы этого теста показаны на рис. 18.1.


Рис. 18.1.  Клиенты и поставщики



Параллельная работа обработчиков исключений


Обработчику исключения - catch-блоку, захватившему исключение, - передается текущее исключение. Анализируя свойства этого объекта, обработчик может понять причину, приведшую к возникновению исключительной ситуации, попытаться ее исправить и в случае успеха продолжить вычисления. Заметьте, в принятой C# схеме без возобновления обработчик исключения не возвращает управление try-блоку, а сам пытается решить проблемы. После завершения catch-блока выполняются операторы текущего метода, следующие за конструкцией try-catch-finally.

Зачастую обработчик исключения не может исправить ситуацию или может выполнить это лишь частично, предоставив решение оставшейся части проблем вызвавшему методу - предшественнику в цепочке вызовов. Механизм, реализующий такую возможность - это тот же механизм исключений. Как правило, в конце своей работы обработчик исключения выбрасывает исключение, выполняя оператор throw. При этом у него есть две возможности: повторно выбросить текущее исключение или выбросить новое исключение, содержащее дополнительную информацию.

Некоторые детали будут пояснены позже при рассмотрении примеров.

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



Паутина Безье


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

Прежде чем рассмотреть программный код, давайте посмотрим, как выглядят нарисованные программой кривые Безье, исходящие из одной точки.


Рис. 24.14.  Паутина Безье

Перейдем к рассмотрению кода. Первым делом добавим в поля формы нужные нам объекты:

//fields Point center; Point[] points = new Point[10]; Pen pen; Graphics graph; int count;

Точка center будет задавать общую начальную точку для всех рисуемых кривых Безье, массив points будет задавать остальные точки, используемые при построении кривых Безье. О роли объектов pen и graph, необходимых при рисовании, уже говорилось. Объект count играет техническую роль, о которой скажу чуть позже, прямого отношения к рисованию он не имеет.

В конструкторе формы вызывается метод MyInit, инициализирующий введенные объекты:

void MyInit() { int cx = ClientSize.Width; int cy = ClientSize.Height; points[0] = new Point(0,0); points[1] = new Point(cx/2,0); points[2] = new Point(cx,0); points[3] = new Point(0,cy/2); points[4] = new Point(cx,cy/2); points[5] = new Point(0,cy); points[6] = new Point(cx/2,cy); points[7] = new Point(cx,cy); points[8] = new Point(0,0); points[9] = new Point(cx/2,0); graph = this.CreateGraphics(); center = new Point(cx/2,cy/2); count =1; }

Рисование кривых Безье выполняется в методе DrawWeb, устроенном очень просто. В цикле рисуется 8 кривых, используя точку center и массив points:

void DrawWeb() { for (int i = 0; i<8; i++) graph.DrawBezier(pen,center,points[i],points[i+2], points[i+1]); }




Метод DrawBezier, вызываемый объектом graph класса Graphics, принадлежит группе рассмотренных нами методов Draw. Первым аргументом у всех этих методов является объект класса Pen, а остальные зависят от типа рисуемой фигуры. Для кривой Безье, как уже говорилось, необходимо задать четыре точки.

Главный вопрос, требующий решения: где же вызывать сам метод DrawWeb, где инициализировать рисование в форме? Будем вызывать этот метод в двух местах - в двух обработчиках событий. Поскольку нам хочется реализовать стратегию, по которой точка center будет следовать за курсором мыши, то естественно, чтобы рисование инициировалось обработчиком события MouseMove нашей формы BezierWeb. (Напомню, для подключения события формы или элемента управления достаточно в режиме проектирования выбрать нужный элемент, в окне свойств этого элемента щелкнуть по значку с изображением молнии и из списка возможных событий данного элемента выбрать нужное, что приведет к созданию заготовки обработчика событий.)

Вот текст обработчика этого события:

private void BezierWeb_MouseMove(object sender, System.Windows.Forms.MouseEventArgs e) { pen = SystemPens.Control; DrawWeb(); center.X = e.X; center.Y = e.Y; //pen = new Pen(Color.Aquamarine); pen = SystemPens.ControlText; DrawWeb(); }

Метод DrawWeb вызывается дважды - первый раз с пером цвета фона, другой - с цветом, принятым системой для отображения текста. Обратите внимание, для создания нужного пера в данном случае не вызывается конструктор класса, а используется класс предопределенных системных перьев. Оператор, создающий объект pen с помощью конструктора, закомментирован. Он может использоваться, если нужно рисовать кривые определенным цветом.

Перед рисованием кривых цветом переднего плана общая для всех кривых точка center получает координаты курсора мыши, передаваемые аргументом обработчика события.


Печать рациональных чисел


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

public void PrintRational(string name) { Console.WriteLine(" {0} = {1}/{2}",name,m,n); }

Метод печатает имя и значение рационального числа в форме m/n.



Перечисление RegexOptions


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



Перечисления


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

[атрибуты][модификаторы]enum имя_перечисления[:базовый класс] {список_возможных_значений}

Описание атрибутов отложим на последующие лекции. Модификаторами могут быть четыре известных модификатора доступа и модификатор new. Ключевое слов enum говорит, что определяется частный случай класса - перечисление. Список возможных значений задает те значения, которые могут получать объекты этого класса. Возможные значения должны быть идентификаторами; но допускаются в их написании и буквы русского алфавита. Можно указать также базовый для перечисления класс.

Дело в том, что значения, заданные списком, проецируются на плотное подмножество базового класса. Реально значения объектов перечисления в памяти задаются значениями базового класса, так же, как значения класса bool реально представлены в памяти нулем и единицей, а не константами true и false, удобными для их использования программистами в тексте программ. По умолчанию, базовым классом является класс int, а подмножество проекции начинается с нуля. Но при желании можно изменить интервал представления и сам базовый класс. Естественно, на базовый класс накладывается ограничение. Он должен быть одним из встроенных классов, задающих счетное множество (int, byte, long, другие счетные типы). Единственное исключение из этого правила - нельзя выбирать класс char в качестве базового класса. Как правило, принятый по умолчанию выбор базового класса и его подмножества вполне приемлем в большинстве ситуаций.

Приведу примеры объявлений классов-перечислений:

public enum Profession{teacher, engineer, businessman}; public enum MyColors {red, blue, yellow, black, white}; public enum TwoColors {black, white}; public enum Rainbow {красный, оранжевый, желтый, зеленый, голубой, синий, фиолетовый}; public enum Sex: byte {man=1, woman}; public enum Days:long {Sun,Mon,Tue,Wed,Thu, Fri, Sat};




Вот несколько моментов, на которые следует обратить внимание при объявлении перечислений:

как и другие классы, перечисления могут быть объявлены непосредственно в пространстве имен проекта или могут быть вложены в описание класса. Последний вариант часто применяется, когда перечисление используется в одном классе и имеет атрибут доступа private;константы разных перечислений могут совпадать, как в перечислениях MyColors и TwoColors. Имя константы всегда уточняется именем перечисления;константы могут задаваться словами русского языка, как в перечислении Rainbow;разрешается задавать базовый класс перечисления. Для перечисления Days базовым классом задан класс long;разрешается задавать не только базовый класс, но и указывать начальный элемент подмножества, на которое проецируется множество значений перечисления. Для перечисления Sex в качестве базового класса выбран класс byte, а подмножество значений начинается с 1, так что хранимым значением константы man является 1, а woman - 2.

Рассмотрим теперь пример работы с объектами - экземплярами различных перечислений:

public void TestEnum() { //MyColors color1 = new MyColors(MyColors.blue); MyColors color1= MyColors.white; TwoColors color2; color2 = TwoColors.white; //if(color1 != color2) color2 = color1; if(color1.ToString() != color2.ToString()) Console.WriteLine ("Цвета разные: {0}, {1}", color1, color2); else Console.WriteLine("Цвета одинаковые: {0}, {1}",color1, color2); Rainbow color3; color3 = (Rainbow)3; if (color3 != Rainbow.красный)color3 =Rainbow.красный; int num = (int)color3; Sex who = Sex.man; Days first_work_day = (Days)(long)1; Console.WriteLine ("color1={0}, color2={1}, color3={2}",color1, color2, color3); Console.WriteLine ("who={0}, first_work_day={1}", who,first_work_day); }

Данный пример иллюстрирует следующие особенности работы с объектами перечислений:

объекты перечислений нельзя создавать в объектном стиле с использованием операции new, поскольку перечисления не имеют конструкторов;объекты можно объявлять с явной инициализацией, как color1, или с отложенной инициализацией, как color2. При объявлении без явной инициализации объект получает значение первой константы перечисления, так что color2 в момент объявления получает значение black;объекту можно присвоить значение, которое задается константой перечисления, уточненной именем перечисления, как для color1 и color2. Можно также задать значение базового типа, приведенное к типу перечисления, как для color3;нельзя сравнивать объекты разных перечислений, например color1 и color2, но можно сравнивать строки, возвращаемые методом ToString, например color1.ToSting() и color2.ToString();существуют явные взаимно обратные преобразования констант базового типа и констант перечисления;Метод ToString, наследованный от класса Object, возвращает строку, задающую константу перечисления.


Передача информации между формами


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

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

Представим себе, что несколько форм должны работать с объектом класса Books. Пусть в главной форме такой объект объявлен:

public Books myBooks;

В конструкторе главной формы такой объект создается:

myBooks = new Books(max_books);

где max_books - заданная константа. Пусть еще в главной форме объявлена форма - объект класса NewBook:

public NewBook form2;

При создании объекта form2 его конструктору передается ссылка на главную форму:

form2 = new NewBook(this);

Класс NewBook содержит поля:

private Form1 mainform; private Books books;

а его конструктор следующий код:

mainform = form; books = mainform.myBooks;

Теперь объекту form2 доступны ранее созданные объекты, задающие книги и главную форму, так что в обработчике события Closed, возникающего при закрытии формы, можно задать код:

private void NewBook_Closed(object sender, System.EventArgs e) { mainform.Show(); }

открывающий главную форму.



Перегрузка операций


Под перегрузкой операции понимается существование нескольких реализаций одной и той же операции. Большинство операций языка C# перегружены - одна и та же операция может применяться к операндам различных типов. Поэтому перед выполнением операции идет поиск реализации, подходящей для данных типов операндов. Замечу, что операции, как правило, выполняются над операндами одного типа. Если же операнды разных типов, то предварительно происходит неявное преобразование типа операнда. Оба операнда могут быть одного типа, но преобразование типов может все равно происходить - по той причине, что для заданных типов нет соответствующей перегруженной операции. Такая ситуация достаточно часто возникает на практике, поскольку, например, операция сложения не определена для младших подтипов арифметического типа. Приведу начальный фрагмент процедуры Express, предназначенной для анализа выражений:

/// <summary> /// Анализ выражений /// </summary> public void Express() { //перегрузка операций byte b1 =1, b2 =2, b3; short sh1; int in1; //b3 = b1 + b2; //ошибка: результат типа int b3 = (byte)(b1+b2); //sh1 = b1 + b2; //ошибка: результат типа int sh1 = (short)(b1+b2); in1 = b1+ b2 + sh1; Console.WriteLine("b3= " + b3 + " sh1= "+ sh1 +" in1= " + in1); }//Express

Разберем этот фрагмент. Начнем с первого закомментированного оператора присваивания b3 = b1+b2;. Выражение здесь простейшее, включает одну бинарную операцию сложения. Оба операнда имеют тип byte, казалось бы, и результат должен быть типа byte и без помех присвоен переменной b3. Однако это не так. Для данных типа byte нет перегруженной реализации сложения. Ближайшей операцией является сложение целых типа int. Поэтому оба операнда преобразуются к типу int, выполняется операция сложения, результат имеет тип int и не может быть неявно преобразован в тип byte, - возникает ошибка еще на этапе компиляции. Корректная запись показана в следующем операторе. Аналогичная ситуация возникает, когда в левой части оператора стоит переменная типа short, - и здесь необходимо явное приведение к типу. Этого приведения не требуется, когда в левой части стоит переменная типа int.

Давайте разберем, как в данном примере организован вывод в методе WriteLine. До сих пор при вызове метода задавалось несколько параметров и использовалась форма вывода данных с подстановкой значений параметров в строку, заданную первым параметром. Здесь же есть только один параметр - это строка, заданная сложным выражением. Операция, многократно применяемая в этом выражении, это сложение " + ". Операнды сложения имеют разный тип: левый операнд имеет тип string, правый - арифметический (byte, short, int). В этом случае арифметический тип преобразуется к типу string и выполняется сложение строк (конкатенация). Напомню, при преобразовании арифметического типа к типу string вызывается метод ToString(), определенный для всех встроенных типов. Результатом этого выражения является строка, она и будет результатом вывода метода WriteLine.

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



Переопределение значений аргументов события


Обработчику события, как правило, передаются входные и выходные аргументы, характеризующие событие. Они необходимы, чтобы обработчик мог нужным образом обработать событие. Но работа с аргументами требует аккуратного с ними обращения. Могут возникать проблемы, связанные с тем, что обработчик может переопределить значения аргументов в процессе своей работы.

Приведенный выше пример "Работа со списками" демонстрирует не самый лучший способ определения аргументов, провоцирующий классы Receiver на некорректное обращение с аргументами. Напомню, в классе ChangedEventArgs, определяющем аргументы события, оба свойства item и permit являются закрытыми. Но определены процедуры - свойства Item и Permit, реализующие полный доступ к свойствам, поскольку определены обе процедуры get и set. Это несколько облегчило задачу, поскольку позволило изменять значение входного аргумента item перед зажиганием события для передачи его обработчику. Но входной аргумент оказался не защищенным, и обработчик события может не только использовать это значение для анализа, но и изменить его в качестве побочного эффекта своей работы. В этом случае другой обработчик будет работать уже с некорректным значением. Что еще хуже - это измененное значение может использовать и класс, в процессе своей дальнейшей работы. Поэтому входные аргументы события должны быть закрытыми для обработчиков событий. Это нетрудно сделать, и я приведу необходимые уточнения.

В классе ChangedEventArgs следует изменить процедуру-свойство Item, удалив процедуру Set, разрешающую изменение свойства. В качестве компенсации в класс следует добавить конструктор с аргументом, что позволит в классе, создающем событие, создавать объект класса ChangedEventArgs с нужным значением свойства item. Приведу соответствующий код: public object Item { get {return(item);} //set { item = value;} } public ChangedEventArgs(object item) { this.item = item; }В методы класса ListWithChangedEvent, зажигающие события, нужно ввести изменения. Теперь перед каждым вызовом нужно создавать новый объект, задающий аргументы. Вот измененный код: public override int Add(object value) { int i=0; ChangedEventArgs evargs = new ChangedEventArgs(value); //evargs.Item = value; OnChanged(evargs); if (evargs.Permit) i = base.Add(value); else Console.WriteLine("Добавление элемента запрещено." + "Значение = {0}", value); return i; } public override void Clear() { ChangedEventArgs evargs = new ChangedEventArgs(0); //evargs.Item=0; OnChanged(evargs); base.Clear(); } public override object this[int index] { set { ChangedEventArgs evargs = new ChangedEventArgs(value); //evargs.Item = value; OnChanged(evargs); if (evargs.Permit) base[index] = value; else Console.WriteLine("Замена элемента запрещена." + " Значение = {0}", value); } get {return(base[index]);} }




Таким образом, обработчикам можно запретить изменение входных аргументов события. Но есть еще выходные аргументы события, значения которых определяются в обработчике; в нашем примере это аргумент Permit.

И здесь возникает коллизия интересов - каждый обработчик по своему может формировать значения выходных аргументов, не обращая внимания на результаты работы предыдущих обработчиков. Преимуществом в таких ситуациях обладает последний работающий обработчик события.

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

Итак, если событие имеет аргументы, то все входные аргументы должны быть закрыты для обработчиков события. Если обработчиков несколько, то лучше или не использовать выходных аргументов, или аккуратно запрограммировать логику обработчиков, которая учитывает решения, полученные коллегами - ранее отработавшими обработчиками события.


Персоны и профессии


Рассмотрим еще один пример работы с перечислениями, приближенный к реальности. Добавим в класс Person, рассмотренный в предыдущей лекции 16, поле, определяющее профессию персоны. Вполне разумно иметь перечисление, например, Profession, задающее список возможных профессий. Сделаем это поле, как обычно, закрытым, а доступ к нему обеспечим соответствующим свойством:

Profession prof; public Profession Prof { get {return (prof);} set {prof = value;} }

Добавим еще в класс Person метод Analysis, анализирующий профессию, организуя традиционный разбор случаев и принимая решение на каждой ветви, в данном примере - выводя соответствующий текст:

public void Analysis() { switch (prof) { case Profession.businessman: Console.WriteLine ("профессия: бизнесмен"); break; case Profession.teacher: Console.WriteLine ("профессия: учитель"); break; case Profession.engineer: Console.WriteLine ("профессия: инженер"); break; default: Console.WriteLine ("профессия: неизвестна"); break; } }

Приведу простой тестирующий пример работы с объектом Person и его профессией:

public void TestProfession() { Person pers1 = new Person ("Петров"); pers1.Prof = Profession.teacher; pers1.Analysis(); }

Результаты работы с объектами перечислений, полученные при вызове тестов TestEnum и TestProfession, показаны на рис. 17.3.


Рис. 17.3.  Результаты работы с перечислениями



Первый закон (закон для разработчика)


Корректность системы - недостижима. Каждая последняя найденная ошибка является предпоследней.

Этот закон отражает сложность нетривиальных систем. Разработчик всегда должен быть готов к тому, что в работающей системе имеются ситуации, в которых система работает не в точном соответствии со своей спецификацией, так что от него может требоваться очередное изменение либо системы, либо ее спецификации.



Почему у методов мало аргументов?


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

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



Поля


Первая важнейшая роль переменных, - они задают свойства структур, интерфейсов, классов. В языке C# такие переменные называются полями (fields). Структуры, интерфейсы, классы, поля - рассмотрению этих понятий будет посвящена большая часть этой книги, а сейчас сообщу лишь некоторые минимальные сведения, связанные с рассматриваемой темой. Поля объявляются при описании класса (и его частных случаев - интерфейса, структуры). Когда конструктор класса создает очередной объект - экземпляр класса, то он в динамической памяти создает набор полей, определяемых классом, и записывает в них значения, характеризующие свойства данного конкретного экземпляра. Так что каждый объект в памяти можно рассматривать как набор соответствующих полей класса со своими значениями. Время существования и область видимости полей определяется объектом, которому они принадлежат. Объекты в динамической памяти, с которыми не связана ни одна ссылочная переменная, становятся недоступными. Реально они оканчивают свое существование, когда сборщик мусора (garbage collector) выполнит чистку "кучи". Для значимых типов, к которым принадлежат экземпляры структур, жизнь оканчивается при завершении блока, в котором они объявлены.

Есть одно важное исключение. Некоторые поля могут жить дольше. Если при объявлении класса поле объявлено с модификатором static, то такое поле является частью класса и единственным на все его экземпляры. Поэтому static-поля живут так же долго, как и сам класс. Более подробно эти вопросы будут обсуждаться при рассмотрении классов, структур, интерфейсов.



Поля класса


Поля класса синтаксически являются обычными переменными (объектами) языка. Их описание удовлетворяет обычным правилам объявления переменных, о чем подробно говорилось в лекции 5. Содержательно поля задают представление той самой абстракции данных, которую реализует класс. Поля характеризуют свойства объектов класса. Напомню, что, когда создается новый объект класса (в динамической памяти или в стеке), то этот объект представляет собой набор полей класса. Два объекта одного класса имеют один и тот же набор полей, но разнятся значениями, хранимыми в этих полях. Все объекты класса Person могут иметь поле, характеризующее рост персоны, но один объект может быть высокого роста, другой - низкого, а третий - среднего роста.



Поля класса или функции без аргументов?


Поля хранят информацию о состоянии объектов класса. Состояние объекта динамически изменяется в ходе вычислений - обновляются значения полей. Часто возникающая дилемма при проектировании класса: что лучше - создать ли поле, хранящее информацию, или создать функцию без аргументов, вычисляющую значение этого поля всякий раз, когда это значение понадобится. Решение дилеммы - это вечный для программистов выбор между памятью и временем. Если предпочесть поле, то это приводит к дополнительным расходам памяти. Они могут быть значительными, когда создается большое число объектов - ведь свое поле должен иметь каждый объект. Если предпочесть функцию, то это потребует временных затрат на вычисление значения, и затраты могут быть значительными в сравнении с выбором текущего значения поля.

Если бы синтаксис описания метода допускал отсутствие скобок у функции (метода), в случае, когда список аргументов отсутствует, то клиент класса мог бы и не знать, обращается ли он к полю или к методу. Такой синтаксис принят, например, в языке Eiffel. Преимущество этого подхода в том, что изменение реализации никак не сказывается на клиентах класса. В языке C# это не так. Когда мы хотим получить длину строки, то пишем s.Length, точно зная, что Length - это поле, а не метод класса String. Если бы по каким-либо причинам разработчики класса String решили изменить реализацию и заменить поле Length соответствующей функцией, то ее вызов имел бы вид s.Length().



Построение программных систем методом "раскрутки". Функции обратного вызова


Метод "раскрутки" является одним из основных методов функционально-ориентированного построения сложных программных систем. Суть его состоит в том, что программная система создается слоями. Вначале пишется ядро системы - нулевой слой, реализующий базовый набор функций. Затем пишется первый слой с новыми функциями, которые интенсивно вызывают в процессе своей работы функции ядра. Теперь система обладает большим набором функций. Каждый новый слой расширяет функциональность системы. Процесс продолжается, пока не будет достигнута заданная функциональность. На рис.20.3, изображающем схему построения системы методом раскрутки, стрелками показано, как функции внешних слоев вызывают функции внутренних слоев.


Рис. 20.3.  Построение системы методом "раскрутки"

Успех языка С и операционной системы Unix во многом объясняется тем, что в свое время они были созданы методом раскрутки. Это позволило написать на 95% на языке С транслятор с языка С и операционную систему. Благодаря этому, обеспечивался легкий перенос транслятора и операционной системы на компьютеры с разной системой команд. Замечу, что в те времена мир компьютеров отличался куда большим разнообразием, чем в нынешнее время. Для переноса системы на новый тип компьютера достаточно было написать ядро системы в соответствии с машинным кодом данного компьютера, далее работала раскрутка.

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

Пусть F - функция высшего порядка с параметром G функционального типа. Тогда функцию G, задающую параметр (а иногда и саму функцию F), называют функцией обратного вызова (callback функцией). Термин вполне понятен. Если в некотором внешнем слое функция Q вызывает функцию внутреннего слоя F, то предварительно во внешнем слое следует позаботиться о создании функции G, которая и будет передана F. Таким образом, функция Q внешнего слоя вызывает функцию F внутреннего слоя, которая, в свою очередь (обратный вызов) вызывает функцию G внешнего слоя. Чтобы эта техника работала, должен быть задан контракт. Функция высших порядков, написанная во внутреннем слое, задает следующий контракт: "всякая функция, которая собирается меня вызвать, должна передать мне функцию обратного вызова, принадлежащую определенному мной функциональному классу, следовательно, иметь известную мне сигнатуру".

Наш пример с вычислением интеграла хорошо демонстрирует функции обратного вызова и технику "раскрутки". Можно считать, что класс HighOrderIntegral - это внутренний слой нашей системы. В нем задан делегат, определяющий контракт, и функция EvalIntegral, требующая задания функции обратного вызова в качестве ее параметра. Функция EvalIntegral вызывается из внешнего слоя, где и определяются callback функции из класса Functions.

Многие из функций операционной системы Windows, входящие в состав Win API 32, требуют при своем вызове задания callback-функций. Примером может служить работа с объектом операционной системы Timer. Конструктор этого объекта является функцией высшего порядка, и ей в момент создания объекта необходимо в качестве параметра передать callback-функцию, вызываемую для обработки событий, которые поступают от таймера.

Пример работы с таймером приводить сейчас не буду, ограничусь лишь сообщением синтаксиса объявления конструктора объекта Timer:

public Timer(TimerCallback callback,object state, int dueTime, int period);

Первым параметром конструктора является функция обратного вызова callback, которая принадлежит функциональному классу TimerCallback, заданному делегатом:

public delegate void TimerCallback(object state);



Преобразование к классу интерфейса


Создать объект класса интерфейса обычным путем с использованием конструктора и операции new нельзя. Тем не менее, можно объявить объект интерфейсного класса и связать его с настоящим объектом путем приведения (кастинга) объекта наследника к классу интерфейса. Это преобразование задается явно. Имея объект, можно вызывать методы интерфейса - даже если они закрыты в классе, для интерфейсных объектов они являются открытыми. Приведу соответствующий пример, в котором идет работа как с объектами классов Clain, ClainP, так и с объектами интерфейсного класса IProps:

public void TestClainIProps() { Console.WriteLine("Объект класса Clain вызывает открытые методы!"); Clain clain = new Clain(); clain.Prop1(" свойство 1 объекта"); clain.Prop2("Владимир", 44); Console.WriteLine("Объект класса IProps вызывает открытые методы!"); IProps ip = (IProps)clain; ip.Prop1("интерфейс: свойство"); ip.Prop2 ("интерфейс: свойство",77); Console.WriteLine("Объект класса ClainP вызывает открытые методы!"); ClainP clainp = new ClainP(); clainp.MyProp1(" свойство 1 объекта"); clainp.MyProp2("Владимир", 44); Console.WriteLine("Объект класса IProps вызывает закрытые методы!"); IProps ipp = (IProps)clainp; ipp.Prop1("интерфейс: свойство"); ipp.Prop2 ("интерфейс: свойство",77); }

Этот пример демонстрирует работу с классом, где все наследуемые методы интерфейса открыты, и с классом, закрывающим наследуемые методы интерфейса. Показано, как обертывание и кастинг позволяют добраться до закрытых методов класса. Результаты выполнения этой тестирующей процедуры приведены на рис. 19.1.


Рис. 19.1.  Наследование интерфейса. Две стратегии



Преобразование к типу object


Рассмотрим частный случай присваивания x = e; когда x имеет тип object. В этом случае гарантируется полная согласованность по присваиванию - выражение e может иметь любой тип. В результате присваивания значением переменной x становится ссылка на объект, заданный выражением e. Заметьте, текущим типом x становится тип объекта, заданного выражением e. Уже здесь проявляется одно из важных различий между классом и типом. Переменная, лучше сказать сущность x, согласно объявлению принадлежит классу Object, но ее тип - тип того объекта, с которым она связана в текущий момент, - может динамически изменяться.



Преобразования и класс Convert


Класс Convert, определенный в пространстве имен System, играет важную роль, обеспечивая необходимые преобразования между различными типами. Напомню, что внутри арифметического типа можно использовать более простой, скобочный способ приведения к нужному типу. Но таким способом нельзя привести, например, переменную типа string к типу int, оператор присваивания: ux = (int)s1; приведет к ошибке периода компиляции. Здесь необходим вызов метода ToInt32 класса Convert, как это сделано в последнем примере предыдущего раздела.

Методы класса Convert поддерживают общий способ выполнения преобразований между типами. Класс Convert содержит 15 статических методов вида To <Type> (ToBoolean(),...ToUInt64()), где Type может принимать значения от Boolean до UInt64 для всех встроенных типов, перечисленных в таблице 3.1. Единственным исключением является тип object, - метода ToObject нет по понятным причинам, поскольку для всех типов существует неявное преобразование к типу object. Среди других методов отмечу общий статический метод ChangeType, позволяющий преобразование объекта к некоторому заданному типу.

Существует возможность преобразования к системному типу DateTime, который хотя и не является встроенным типом языка C#, но допустим в программах, как и любой другой системный тип. Приведу простейший пример работы с этим типом:

// System type: DateTime System.DateTime dat = Convert.ToDateTime("15.03.2003"); Console.WriteLine("Date = {0}", dat);

Результатом вывода будет строка:

Date = 15.03.2003 0:00:00

Все методы To <Type> класса Convert перегружены и каждый из них имеет, как правило, более десятка реализаций с аргументами разного типа. Так что фактически эти методы задают все возможные преобразования между всеми встроенными типами языка C#.

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



Преобразования ссылочных типов


Поскольку операции над ссылочными типами не определены (исключением являются строки, но операции над ними, в том числе и присваивание, выполняются как над значимыми типами), то необходимость в них возникает только при присваиваниях и вызовах методов. Семантика таких преобразований рассмотрена в предыдущей лекции 3, где подробно обсуждалась семантика присваивания и совпадающая с ней семантика замены формальных аргументов фактическими. Там же много говорилось о преобразованиях между ссылочными и значимыми типами, выполняемых при этом операциях упаковки значений в объекты и обратной их распаковки.

Коротко повторю основные положения, связанные с преобразованиями ссылочных типов. При присваиваниях (замене аргументов) тип источника должен быть согласован с типом цели, то есть объект, связанный с источником, должен принадлежать классу, являющемуся потомком класса цели. В случае согласования типов, ссылочная переменная цели связывается с объектом источника и ее тип динамически изменяется, становясь типом источника. Это преобразование выполняется автоматически и неявно, не требуя от программиста никаких дополнительных указаний. Если же тип цели является потомком типа источника, то неявное преобразование отсутствует, даже если объект, связанный с источником, принадлежит типу цели. Явное преобразование, заданное программистом, позволяет справиться с этим случаем. Ответственность за корректность явных преобразований лежит на программисте, так что может возникнуть ошибка на этапе выполнения, если связываемый объект реально не является объектом класса цели. За примерами следует обратиться к лекции 3, еще раз обратив внимание на присваивания объектов классов Parent и Child.



Преобразования строкового типа


Важным классом преобразований являются преобразования в строковый тип и наоборот. Преобразования в строковый тип всегда определены, поскольку, напомню, все типы являются потомками базового класса Object, а, следовательно, обладают методом ToString(). Для встроенных типов определена подходящая реализация этого метода. В частности, для всех подтипов арифметического типа метод ToString() возвращает в подходящей форме строку, задающую соответствующее значение арифметического типа. Заметьте, метод ToString можно вызывать явно, но, если явный вызов не указан, то он будет вызываться неявно, всякий раз, когда по контексту требуется преобразование к строковому типу. Вот соответствующий пример:

/// <summary> /// Демонстрация преобразования в строку данных различного типа. /// </summary> public void ToStringTest() { s ="Владимир Петров "; s1 =" Возраст: "; ux = 27; s = s + s1 + ux.ToString(); s1 =" Зарплата: "; dy = 2700.50; s = s + s1 + dy; WhoIsWho("s",s); }


Рис. 4.3.  Вывод на печать результатов теста ToStringTest

Здесь для переменной ux метод был вызван явно, а для переменной dy он вызывается автоматически. Результат работы этой процедуры показан на рис. 4.3.

Преобразования из строкового типа в другие типы, например, в арифметический, должны выполняться явно. Но явных преобразований между арифметикой и строками не существуют. Необходимы другие механизмы, и они в C# имеются. Для этой цели можно использовать соответствующие методы класса Convert библиотеки FCL, встроенного в пространство имен System. Приведу соответствующий пример:

/// <summary> /// Демонстрация преобразования строки в данные различного типа. /// </summary> public void FromStringTest() { s ="Введите возраст "; Console.WriteLine(s); s1 = Console.ReadLine(); ux = Convert.ToUInt32(s1); WhoIsWho("Возраст: ",ux); s ="Введите зарплату "; Console.WriteLine(s); s1 = Console.ReadLine(); dy = Convert.ToDouble(s1); WhoIsWho("Зарплата: ",dy); }

Этот пример демонстрирует ввод с консоли данных разных типов. Данные, читаемые с консоли методом ReadLine или Read, всегда представляют собой строку, которую затем необходимо преобразовать в нужный тип. Тут-то и вызываются соответствующие методы класса Convert. Естественно, для успеха преобразования строка должна содержать значение в формате, допускающем подобное преобразование. Заметьте, например, что при записи значения числа для выделения дробной части должна использоваться запятая, а не точка; в противном случае возникнет ошибка периода выполнения.

На рис. 4.4 показаны результаты вывода и ввода данных с консоли при работе этой процедуры.


Рис. 4.4.  Вывод на печать результатов теста FromStringTest



Преобразования типов в выражениях


Продолжая тему преобразований типов, рассмотрим привычные для программистов преобразования между значимыми типами и, прежде всего, преобразования внутри арифметического типа.

В C# такие преобразования делятся на неявные и явные. К неявным относятся те преобразования, результат выполнения которых всегда успешен и не приводит к потере точности данных. Неявные преобразования выполняются автоматически. Для арифметических данных это означает, что в неявных преобразованиях диапазон типа назначения содержит в себе диапазон исходного типа. Например, преобразование из типа byte в тип int относится к неявным, поскольку диапазон типа byte является подмножеством диапазона int. Это преобразование всегда успешно и не может приводить к потере точности. Заметьте, преобразования из целочисленных типов к типам с плавающей точкой относятся к неявным. Хотя здесь и может происходить некоторое искажение значения, но точность представления значения сохраняется, например, при преобразовании из long в double порядок значения остается неизменным.

К явным относятся разрешенные преобразования, успех выполнения которых не гарантируется или может приводить к потере точности. Такие потенциально опасные преобразования должны быть явно заданы программистом. Преобразование из типа int в тип byte относится к явным, поскольку оно небезопасно и может приводить к потере значащих цифр. Заметьте, не для всех типов существуют явные преобразования. В этом случае требуются другие механизмы преобразования типов, которые будут рассмотрены позже.



Преобразования внутри арифметического типа


Арифметический тип, как показано в таблице 3.1, распадается на 11 подтипов. На рис. 4.1 показана схема преобразований внутри арифметического типа.


Рис. 4.1.  Иерархия преобразований внутри арифметического типа

Диаграмма, приведенная на рисунке, позволяет ответить на ряд важных вопросов, связанных с существованием преобразований между типами. Если на диаграмме задан путь (стрелками) от типа А к типу В, то это означает существование неявного преобразования из типа А в тип В. Все остальные преобразования между подтипами арифметического типа существуют, но являются явными. Заметьте, что циклов на диаграмме нет, все стрелки односторонние, так что преобразование, обратное к неявному, всегда должно быть задано явным образом.

Путь, указанный на диаграмме, может быть достаточно длинным, но это вовсе не означает, что выполняется вся последовательность преобразований на данном пути. Наличие пути говорит лишь о существовании неявного преобразования, а само преобразование выполняется только один раз, - из типа источника А в тип назначения В.

Иногда возникает ситуация, при которой для одного типа источника может одновременно существовать несколько типов назначений и необходимо осуществить выбор цели - типа назначения. Такие проблемы выбора возникают, например, при работе с перегруженными методами в классах.

Понятие перегрузки методов и операций подробно будет рассмотрено в последующих лекциях (см. лекцию 8).

Диаграмма, приведенная на рис. 4.1, и в этом случае помогает понять, как делается выбор. Пусть существует две или более реализации перегруженного метода, отличающиеся типом формального аргумента. Тогда при вызове этого метода с аргументом типа T может возникнуть проблема, какую реализацию выбрать, поскольку для нескольких реализаций может быть допустимым преобразование аргумента типа T в тип, заданный формальным аргументом данной реализации метода. Правило выбора реализации при вызове метода таково: выбирается та реализация, для которой путь преобразований, заданный на диаграмме, короче. Если есть точное соответствие параметров по типу (путь длины 0), то, естественно, именно эта реализация и будет выбрана.

Давайте рассмотрим еще один тестовый пример. В класс Testing включена группа перегруженных методов OLoad с одним и двумя аргументами. Вот эти методы:

/// <summary> /// Группа перегруженных методов OLoad /// с одним или двумя аргументами арифметического типа. /// Если фактический аргумент один, то будет вызван один из /// методов, наиболее близко подходящий по типу аргумента. /// При вызове метода с двумя аргументами возможен /// конфликт выбора подходящего метода, приводящий /// к ошибке периода компиляции. /// </summary> void OLoad(float par) { Console.WriteLine("float value {0}", par); } /// <summary> /// Перегруженный метод OLoad с одним параметром типа long /// </summary> /// <param name="par"></param> void OLoad(long par) { Console.WriteLine("long value {0}", par); } /// <summary> /// Перегруженный метод OLoad с одним параметром типа ulong /// </summary> /// <param name="par"></param> void OLoad(ulong par) { Console.WriteLine("ulong value {0}", par); } /// <summary> /// Перегруженный метод OLoad с одним параметром типа double /// </summary> /// <param name="par"></param> void OLoad(double par) { Console.WriteLine("double value {0}", par); } /// <summary> /// Перегруженный метод OLoad с двумя параметрами типа long и long /// </summary> /// <param name="par1"></param> /// <param name="par2"></param> void OLoad(long par1, long par2) { Console.WriteLine("long par1 {0}, long par2 {1}", par1, par2); } /// <summary> /// Перегруженный метод OLoad с двумя параметрами типа /// double и double /// </summary> /// <param name="par1"></param> /// <param name="par2"></param> void OLoad(double par1, double par2) { Console.WriteLine("double par1 {0}, double par2 {1}",par1, par2); } /// <summary> /// Перегруженный метод OLoad с двумя параметрами типа /// int и float /// </summary> /// <param name="par1"></param> /// <param name="par2"></param> void OLoad(int par1, float par2) { Console.WriteLine("int par1 {0}, float par2 {1}",par1, par2); }



Все эти методы устроены достаточно


Все эти методы устроены достаточно просто. Они сообщают информацию о типе и значении переданных аргументов. Вот тестирующая процедура, вызывающая метод OLoad с разным числом и типами аргументов:

/// <summary> /// Вызов перегруженного метода OLoad. В зависимости от /// типа и числа аргументов вызывается один из методов группы. /// </summary> public void OLoadTest() { OLoad(x); OLoad(ux); OLoad(y); OLoad(dy); // OLoad(x,ux); // conflict: (int, float) и (long,long) OLoad(x,(float)ux); OLoad(y,dy); OLoad(x,dy); }

Заметьте, один из вызовов закомментирован, так как он приводит к конфликту на этапе трансляции. Для устранения конфликта при вызове метода пришлось задать явное преобразование аргумента, что показано в строке, следующей за строкой-комментарием.


Рис. 4.2.  Вывод на печать результатов теста OLoadTest

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

Приведу все-таки некоторые комментарии. При первом вызове метода тип источника - int, а тип аргумента у четырех возможных реализаций соответственно float, long, ulong, double. Явного соответствия нет, поэтому нужно искать самый короткий путь на схеме. Так как не существует неявного преобразования из типа int в тип ulong (на диаграмме нет пути), то остаются возможными три реализации. Но путь из int в long короче, чем остальные пути, поэтому будет выбрана long-реализация метода.

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

Рассмотрим еще ситуацию, приводящую к конфликту. Первый аргумент в соответствии с правилами требует вызова одной реализации, а второй аргумент будет настаивать на вызове другой реализации. Возникнет коллизия, не разрешимая правилами C# и приводящая к ошибке периода компиляции. Коллизию требуется устранить, например, как это сделано в примере. Обратите внимание - обе реализации допустимы, и существуй даже только одна из них, ошибки бы не возникало.


Атрибуты"


Как уже говорилось, регулярные выражения особенно хороши при разборе сложных текстов. Примерами таковых могут быть различные справочники, различные текстовые базы данных, весьма популярные теперь XML-документы, разбором которых приходится заниматься. В качестве заключительного примера рассмотрим структурированный документ, строки которого содержат некоторые атрибуты, например, телефон, адрес и e-mail. Структуру документа можно задавать по-разному; будем предполагать, что каждый атрибут задается парой "имя: Значение" Наша задача состоит в том, чтобы выделить из строки соответствующие атрибуты. В таких ситуациях регулярное выражение удобно задавать в виде групп, где каждая группа соответствует одному атрибуту. Приведу начальный фрагмент кода очередной тестирующей процедуры, в котором описываются строки текста и образцы поиска:

public void TestAttributes() { string s1 = "tel: (831-2) 94-20-55 "; string s2 = "Адрес: 117926, Москва, 5-й Донской проезд, стр.10,кв.7"; string s3 = "e-mail: Valentin.Berestov@tverorg.ru "; string s4 = s1+ s2 + s3; string s5 = s2 + s1 + s3; string pat1 = @"tel:\s(?<tel>\((\d|-)*\)\s(\d|-)+)\s"; string pat2= @"Адрес:\s(?<addr>[0-9А-Яа-я \-\,\.]+)\s"; string pat3 =@"e-mail:\s(?<em>[a-zA-Z.@]+)\s"; string compat = pat1+pat2+pat3; string tel="", addr = "", em = "";

Строки s4 и s5 представляют строку разбираемого документа. Их две, для того чтобы можно было проводить эксперименты, когда атрибуты в документе представлены в произвольном порядке. Каждая из строк pat1, pat2, pat3 задает одну именованную группу в регулярном выражении, имена групп - tel, Адрес, e-mail - даются в соответствии со смыслом атрибутов. Сами шаблоны подробно описывать не буду - сделаю лишь одно замечание. Например, шаблон телефона исходит из того, что номеру предшествует код, заключенный в круглые скобки. Поскольку сами скобки играют особую роль, то для задания скобки как символа используется пара - "\(". Это же касается и многих других символов, используемых в шаблонах, - точки, дефиса и т.п. Строка compat представляет составное регулярное выражение, содержащее все три группы. Строки tel, addr и em нам понадобятся для размещения в них результатов разбора. Применим вначале к строкам s4 и s5 каждый из шаблонов pat1, pat2, pat3 в отдельности и выделим соответствующий атрибут из строки. Вот код, выполняющий эти операции:

Regex reg1 = new Regex(pat1); Match match1= reg1.Match(s4); Console.WriteLine("Value =" + match1.Value); tel= match1.Groups["tel"].Value; Console.WriteLine(tel); Regex reg2 = new Regex(pat2); Match match2= reg2.Match(s5); Console.WriteLine("Value =" + match2.Value); addr= match2.Groups["addr"].Value; Console.WriteLine(addr); Regex reg3 = new Regex(pat3); Match match3= reg3.Match(s5); Console.WriteLine("Value =" + match3.Value); em= match3.Groups["em"].Value; Console.WriteLine(em);




Все выполняется нужным образом - создаются именованные группы, к ним можно получить доступ и извлечь найденный значения атрибутов. А теперь попробуем решить ту же задачу одним махом, используя составной шаблон compat:
Regex comreg = new Regex(compat); Match commatch= comreg.Match(s4); tel= commatch.Groups["tel"].Value; Console.WriteLine(tel); addr= commatch.Groups["addr"].Value; Console.WriteLine(addr); em= commatch.Groups["em"].Value; Console.WriteLine(em); }// TestAttributes
И эта задача успешно решается. Взгляните на результаты разбора текста.

Рис. 15.7.  Регулярные выражения. Пример "Атрибуты"
На этом и завершим рассмотрение регулярных выражений а также лекции, посвященные работе с текстами в C#.

чет и нечет"


Не всякий класс языков можно описать с помощью регулярных выражений. И даже тогда, когда такая возможность есть, могут потребоваться определенные усилия для корректной записи соответствующего регулярного выражения. Рассмотрим, например, язык L1 в алфавите T={0,1}, которому принадлежат пустое слово и слова, содержащие четное число нулей и четное число единиц. В качестве другого примера рассмотрим язык L2, отличающийся от первого тем, что в нем число единиц нечетно. Оба языка можно задать регулярными выражениями, но корректная запись непроста и требует определенного навыка. Давайте запишем регулярные выражения, определяющие эти языки, и покажем, что C# справляется с проблемой их распознавания. Вот регулярное выражение, описывающее первый язык:

(00|11)*((01|10)(00|11)*(01|10)(00|11)*)*

Дадим содержательное описание этого языка. Слова языка представляют возможно пустую последовательность из пар одинаковых символов. Далее может идти последовательность, начинающаяся и заканчивающаяся парами различающихся символов, между которыми может стоять произвольное число пар одинаковых символов. Такая группа может повторяться многократно. Регулярное выражение короче и точнее передает описываемую структуру слов языка L1.

Язык L2 описать теперь совсем просто. Его слова представляют собой единицу, окаймленную словами языка L1.

Прежде чем перейти к примеру распознавания слов языков L1 и L2, приведу процедуру FindMatches, позволяющую найти все вхождения образца в заданный текст:

void FindMatches(string str, string strpat) { Regex pat = new Regex(strpat); MatchCollection matchcol =pat.Matches(str); Console.WriteLine("Строка ={0}\tОбразец={1}",str,strpat); Console.WriteLine("Число совпадений ={0}",matchcol.Count); foreach(Match match in matchcol) Console.WriteLine("Index = {0} Value = {1}, Length ={2}", match.Index,match.Value, match.Length); }//FindMatches

Входные аргументы у процедуры те же, что и у функции FindMatch, ищущей первое вхождение. Я не стал задавать выходных аргументов процедуры, ограничившись тем, что все результаты непосредственно выводятся на печать в самой процедуре. Выполнение процедуры, так же, как и в FindMatch, начинается с создания объекта pat класса Regex, конструктору которого передается регулярное выражение. Замечу, что класс Regex, так же, как и класс String, относится к неизменяемым (immutable) классам, поэтому для каждого нового образца нужно создавать новый объект pat.

В отличие от FindMatch, объект pat вызывает метод Matches, который определяет все вхождения подстрок, удовлетворяющих образцу, в заданный текст. Результатом выполнения метода Matches является автоматически создаваемый объект класса MatchCollection, хранящий коллекцию объектов уже известного нам класса Match, каждый из которых задает очередное вхождение. В процедуре используются свойства коллекции и ее элементов для получения в цикле по элементам коллекции нужных свойств - индекса очередного вхождения подстроки в строку, ее длины и значения.

Вот процедура, в которой многократно вызывается FindMatches для различных строк и образцов поиска:

public void TestMultiPat() { //поиск по образцу всех вхождений string str,strpat,found; Console.WriteLine("Распознавание языков: чет и нечет"); //четное число нулей и единиц strpat ="((00|11)*((01|10)(00|11)*(01|10)(00|11)*)*)"; str = "0110111101101"; FindMatches(str, strpat); //четное число нулей и нечетное единиц string strodd = strpat + "1" + strpat; FindMatches(str, strodd); }//TestMultiPat




Коротко прокомментирую работу этой процедуры. Первые два примера связаны с распознаванием языков L1 и L2 (чет и нечет) - языков с четным числом единиц и нулей в первом случае и нечетным числом единиц во втором. Регулярные выражения, описывающие эти языки, подробно рассматривались. В полном соответствии с теорией, константы задают эти выражения. На вход для распознавания подается строка из нулей и единиц. Для языка L1 метод находит три соответствия. Первое из них задает максимально длинную подстроку, содержащую четное число нулей и единиц, и две пустые подстроки, по определению принадлежащие языку L1. Для языка L2 находится одно соответствие - это сама входная строка. Взгляните на результаты распознавания.

Рис. 15.2.  Регулярные выражения. Пример "чет и нечет"

Дом Джека"


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

public void TestParsing() { string str,strpat; //разбор предложения - создание массива слов str = "А это пшеница, которая в темном чулане хранится," +" в доме, который построил Джек!"; strpat =" +|, "; Regex pat = new Regex(strpat); string[] words; words = pat.Split(str); int i=1; foreach(string word in words) Console.WriteLine("{0}: {1}",i++,word); }//TestParsing

Регулярное выражение, заданное строкой strpat, определяет множество разделителей. Заметьте, в качестве разделителя задан пробел, повторенный сколь угодно много раз, либо пара символов - запятая и пробел. Разделители задаются регулярными выражениями. Метод Split применяется к объекту pat класса Regex. В качестве аргумента методу передается текст, подлежащий расщеплению. Вот как выглядит массив слов после применения метода Split.


Рис. 15.6.  Регулярные выражения. Пример "Дом Джека"



две версии класса Account


Проиллюстрируем рассмотренные выше вопросы на примере проектирования классов Account и Account1, описывающих такую абстракцию данных, как банковский счет. Определим на этих данных две основные операции - занесение денег на счет и снятие денег. В первом варианте - классе Account - будем активно использовать поля класса. Помимо двух основных полей credit и debit, хранящих приход и расход счета, введем поле balance, которое задает текущее состояние счета, и два поля, связанных с последней выполняемой операцией. Поле sum будет хранить сумму денег текущей операции, а поле result - результат выполнения операции. Полей у класса много, и как следствие, у методов класса аргументов будет немного. Вот описание нашего класса:

/// <summary> /// Класс Account определяет банковский счет. Простейший /// вариант с возможностью трех операций: положить деньги /// на счет, снять со счета, узнать баланс.Вариант с полями /// </summary> public class Account { //закрытые поля класса int debit=0, credit=0, balance =0; int sum =0, result=0; /// <summary> /// Зачисление на счет с проверкой /// </summary> /// <param name="sum">зачисляемая сумма</param> public void putMoney(int sum) { this.sum = sum; if (sum >0) { credit += sum; balance = credit - debit; result =1; } else result = -1; Mes(); }//putMoney /// <summary> /// Снятие со счета с проверкой /// </summary> /// <param name="sum"> снимаемая сумма</param> public void getMoney(int sum) { this.sum = sum; if(sum <= balance) { debit += sum; balance = credit - debit; result =2; } else result = -2; Mes(); }//getMoney /// <summary> /// Уведомление о выполнении операции /// </summary> void Mes() { switch (result) { case 1: Console.WriteLine("Операция зачисления денег прошла успешно!"); Console.WriteLine("Cумма={0}, Ваш текущий баланс={1}",sum, balance); break; case 2: Console.WriteLine("Операция снятия денег прошла успешно!"); Console.WriteLine("Cумма={0}, Ваш текущий баланс={1}", sum,balance); break; case -1: Console.WriteLine("Операция зачисления денег не выполнена!"); Console.WriteLine("Сумма должна быть больше нуля!"); Console.WriteLine("Cумма={0}, Ваш текущий баланс={1}", sum,balance); break; case -2: Console.WriteLine("Операция снятия денег не выполнена!"); Console.WriteLine("Сумма должна быть не больше баланса!"); Console.WriteLine("Cумма={0}, Ваш текущий баланс={1}", sum,balance); break; default: Console.WriteLine("Неизвестная операция!"); break; } } }//Account




Как можно видеть, только у методов getMoney и putMoney имеется один входной аргумент. Это тот аргумент, который нужен по сути дела, поскольку только клиент может решить, какую сумму он хочет снять или положить на счет. Других аргументов у методов класса нет - вся информация передается через поля класса. Уменьшение числа аргументов приводит к повышению эффективности работы с методами, так как исчезают затраты на передачу фактических аргументов. Но за все надо платить. В данном случае, усложняются сами операции работы со вкладом, поскольку нужно в момент выполнения операции обновлять значения многих полей класса. Закрытый метод Mes вызывается после выполнения каждой операции, сообщая о том, как прошла операция, и информируя клиента о текущем состоянии его баланса.
А теперь спроектируем аналогичный класс Account1, отличающийся только тем, что у него будет меньше полей. Вместо поля balance в классе появится соответствующая функция с этим же именем, вместо полей sum и result появятся аргументы у методов, обеспечивающие необходимую передачу информации. Вот как выглядит этот класс:
/// <summary> /// Класс Account1 определяет банковский счет. /// Вариант с аргументами и функциями /// </summary> public class Account1 { //закрытые поля класса int debit=0, credit=0; /// <summary> /// Зачисление на счет с проверкой /// </summary> /// <param name="sum">зачисляемая сумма</param> public void putMoney(int sum) { int res =1; if (sum >0)credit += sum; else res = -1; Mes(res,sum); }//putMoney /// <summary> /// Снятие со счета с проверкой /// </summary> /// <param name="sum"> снимаемая сумма</param> public void getMoney(int sum) { int res=2; if(sum <= balance())debit += sum; else res = -2; balance(); Mes(res, sum); }//getMoney /// <summary> /// вычисление баланса /// </summary> /// <returns>текущий баланс</returns> int balance() { return(credit - debit); } /// <summary> /// Уведомление о выполнении операции /// </summary> void Mes(int result, int sum) { switch (result) { case 1: Console.WriteLine("Операция зачисления денег прошла успешно!"); Console.WriteLine("Cумма={0}, Ваш текущий баланс={1}", sum,balance()); break; case 2: Console.WriteLine("Операция снятия денег прошла успешно!"); Console.WriteLine("Cумма={0}, Ваш текущий баланс={1}", sum,balance()); break; case -1: Console.WriteLine("Операция зачисления денег не выполнена!"); Console.WriteLine("Сумма должна быть больше нуля!"); Console.WriteLine("Cумма={0}, Ваш текущий баланс={1}", sum,balance()); break; case -2: Console.WriteLine("Операция снятия денег не выполнена!"); Console.WriteLine("Сумма должна быть не больше баланса!"); Console.WriteLine("Cумма={0}, Ваш текущий баланс={1}", sum,balance()); break; default: Console.WriteLine("Неизвестная операция!"); break; } } }//Account1


Сравнивая этот класс с классом Account, можно видеть, что число полей сократилось с пяти до двух, упростились основные методы getMoney и putMoney. Но, в качестве платы, у класса появился дополнительный метод balance(), многократно вызываемый, и у метода Mes теперь появились два аргумента. Какой класс лучше? Однозначно сказать нельзя, все зависит от контекста, от приоритетов, заданных при создании конкретной системы.
Приведу процедуру класса Testing, тестирующую работу с классами Account и Account1:
public void TestAccounts() { Account myAccount = new Account(); myAccount.putMoney(6000); myAccount.getMoney(2500); myAccount.putMoney(1000); myAccount.getMoney(4000); myAccount.getMoney(1000); //Аналогичная работа с классом Account1 Console.WriteLine("Новый класс и новый счет!"); Account1 myAccount1 = new Account1(); myAccount1.putMoney(6000); myAccount1.getMoney(2500); myAccount1.putMoney(1000); myAccount1.getMoney(4000); myAccount1.getMoney(1000); }
На рис. 9.1 показаны результаты работы этой процедуры.

Рис. 9.1.  Тестирование классов Account и Account1

кок и кук"


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

Console.WriteLine("кок и кук"); strpat="(т|к).(т|к)"; str="кок тот кук тут как кот"; FindMatches(str, strpat);

Вот результаты работы этого фрагмента кода.


Рис. 15.4.  Регулярные выражения. Пример "кок и кук"



Комбинирование делегатов"


Рассмотрим следующую ситуацию. Пусть есть городские службы: милиция, скорая помощь, пожарные. Каждая из служб по-своему реагируют на события, происходящие в городе. Построим примитивную модель жизни города, в которой случаются события и сообщения о них посылаются службам. В последующей лекции эта модель будет развита. Сейчас она носит формальный характер, демонстрируя, главным образом, работу с делегатами, заодно поясняя ситуации, в которых разумно комбинирование делегатов.

Начнем с построения класса с именем Combination, где, следуя уже описанной технологии, введем делегатов как закрытые свойства, доступ к которым идет через процедуру-свойство get. Три делегата одного класса будут описывать действия трех городских служб. Класс будет описываться ранее введенным делегатом MesToPers, размещенным в пространстве имен проекта. Вот программный код, в котором описаны функции, задающие действия служб:

class Combination { private static void policeman(string mes) { //анализ сообщения if(mes =="Пожар!") Console.WriteLine(mes + " Милиция ищет виновных!"); else Console.WriteLine(mes +" Милиция здесь!"); } private static void ambulanceman(string mes) { if(mes =="Пожар!") Console.WriteLine(mes + " Скорая спасает пострадавших!"); else Console.WriteLine(mes + " Скорая помощь здесь!"); } private static void fireman(string mes) { if(mes =="Пожар!") Console.WriteLine(mes + " Пожарные тушат пожар!"); else Console.WriteLine( mes + " Пожарные здесь!"); } }

Как видите, все три функции имеют не только одинаковую сигнатуру, но и устроены одинаково. Они анализируют приходящее к ним сообщение, переданное через параметр mes, а затем, в зависимости от результата, выполняют ту или иную работу, которая в данном случае сводится к выдаче соответствующего сообщения. Сами функции закрыты, и мы сейчас организуем к ним доступ:

public static MesToPers Policeman { get {return (new MesToPers(policeman));} } public static MesToPers Fireman { get {return (new MesToPers(fireman));} } public static MesToPers Ambulanceman { get {return (new MesToPers(ambulanceman));} }




Три статических открытых свойства - Policeman, Fireman, Ambulanceman - динамически создают экземпляры класса MesToPers, связанные с соответствующими закрытыми функциями класса.
Службы у нас есть, покажем, как с ними можно работать. С этой целью добавим в класс Testing, где проводятся различные эксперименты, следующую процедуру:
public void TestSomeServices() { MesToPers Comb; Comb = (MesToPers)Delegate.Combine(Combination.Ambulanceman, Combination.Policeman); Comb = (MesToPers)Delegate.Combine(Comb,Combination.Fireman); Comb("Пожар!");
Вначале объявляется без инициализации функциональная переменная Comb, которой в следующем операторе присваивается ссылка на экземпляр делегата, созданного методом Combine, чей список вызова содержит ссылки на экземпляры делегатов Ambulanceman и Policeman. Затем к списку вызовов экземпляра Comb присоединяется новый кандидат Fireman. При вызове делегата Comb ему передается сообщение "Пожар!". В результате вызова Comb поочередно запускаются все три экземпляра входящие в список, каждому из которых передается сообщение.
Давайте теперь начнем поочередно отключать делегатов, вызывая затем Comb с новыми сообщениями:
Comb = (MesToPers)Delegate.Remove(Comb,Combination.Policeman); //Такое возможно: попытка отключить не существующий элемент Comb = (MesToPers)Delegate.Remove(Comb,Combination.Policeman); Comb("Через 30 минут!"); Comb = (MesToPers)Delegate.Remove(Comb,Combination.Ambulanceman); Comb("Через час!"); Comb = (MesToPers)Delegate.Remove(Comb,Combination.Fireman); //Comb("Через два часа!"); // Comb не определен
В этом фрагменте поочередно отключаются разные службы - милиция, скорая помощь, пожарные, и каждый раз вызывается Comb. После последнего отключения, когда список вызовов становится пустым, вызов Comb приводит к ошибке, потому оператор вызова закомментирован.
Покажем теперь, что ту же работу можно выполнить, используя не методы, а операции:
//операции + и - Comb = Combination.Ambulanceman; Console.WriteLine( Comb.Method.Name); Comb+= Combination.Fireman; Comb+= Combination.Policeman; Comb("День города!"); Comb -= Combination.Ambulanceman; Comb -= Combination.Fireman; Comb("На следующий день!"); }//TestSomeServices
Обратите внимание, здесь демонстрируется вызов свойства Method, возвращающее объект, свойство Name которого выводится на печать. Результаты, порожденные работой этой процедуры, изображены на рис. 20.6.

Рис. 20.6.  Службы города

обратные ссылки"


В этом примере рассматривается ранее упоминавшаяся, но не описанная возможность задания в регулярном выражении обратных ссылок. Можно ли описать с помощью регулярных выражений язык, в котором встречаются две подряд идущие одинаковые подстроки? Ответ на это вопрос отрицательный, поскольку грамматика такого языка должна быть контекстно-зависимой, и нужна память, чтобы хранить уже распознанные части строки. Аппарат регулярных выражений, предоставляемый классами пространства RegularExpression, тем не менее, позволяет решить эту задачу. Причина в том, что расширение стандартных регулярных выражений в Net Framework является не только синтаксическим. Содержательные расширения связаны с введением понятия группы, которой отводится память и дается имя. Это и дает возможность ссылаться на уже созданные группы, что и делает грамматику языка контекстно-зависимой. Ссылка на ранее полученную группу называется обратной ссылкой. Признаком обратной ссылки является пара символов "\k", после которой идет имя группы. Приведу пример:

Console.WriteLine("Ссылка назад - второе вхождение слова"); strpat = @"\s(?<word>\w+)\s\k'word'"; str = "I know know that, You know that!"; FindMatches(str, strpat);

Рассмотрим более подробно регулярное выражение, заданное строкой strpat. В группе, заданной скобочным выражением, после знака вопроса идет имя группы "word", взятое в угловые скобки. После имени группы идет шаблон, описывающий данную группу, в нашем примере шаблон задается произвольным идентификатором "\w+". В дальнейшем описании шаблона задается ссылка на группу с именем "word". Здесь имя группы заключено в одинарные кавычки. Поиск успешно справился с поставленной задачей, подтверждением чему являются результаты работы этого фрагмента кода.


Рис. 15.5.  Регулярные выражения. Пример "обратные ссылки"