Компромисс  находят  в  том,  что  разрешают  реализовать  в  одном

классе сразу несколько интерфейсов. Таким образом, в одном классе

объединяются разные группы методов — как если бы при множе-

ственном наследовании. При этом описание методов выполняется

непосредственно в классе, что позволяет контролировать взаимную

корректность кода.

Хотя методы в интерфейсе только объявляются (то есть, по сути, являют-

ся абстрактными), ключевое слово abstract здесь не указывается. Более

того, по умолчанию все они считаются открытыми. Что касается свойств и

Интерфейсы           229

индексаторов, то, как отмечалось, у них не описываются аксессоры. Если

соответствующий член интерфейса доступен для присваивания значения, в его теле (в фигурных скобках) указывается ключевое слово get (намек на

аксессор для присваивания значения). Если свойство/индексатор доступ-

ны для считывания значения, указывается ключевое слово set.

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

цесс создания класса на основе интерфейса называется реализацией ин-

терфейса. Интерфейс, который реализуется в классе, указывается при

описании класса через двоеточие после имени класса — так же, как при

наследовании классов. Один класс может реализовывать сразу несколь-

ко интерфейсов. В этом случае интерфейсы указываются через запятую.

В классе, который реализует интерфейс (или интерфейсы), необходимо

описать те методы (и аксессоры), которые объявлены в интерфейсе (или

интерфейсах). Если, помимо реализации интерфейсов, класс создается

еще и на основе базового класса, то этот базовый класс возглавляет список

реализуемых интерфейсов.

Простой пример использования интерфейса приведен в программном

коде в листинге 6.4. Соответствующий проект реализуется как Windows-

приложение. Результат наших программных изысканий, реализуемых че-

рез приведенный ниже программный код, предстанет в виде окна с двумя

кнопками и текстовой меткой по центру окна. Щелчок на кнопке Отмена

приводит к закрытию окна и завершению работы приложения. Щелчок на

кнопке OK приводит к изменению тестового содержимого метки — в тексте

содержится информация о том, сколько раз выполнялся щелчок на кнопке

OK. Теперь приступим к анализу программного кода.

ПРИМЕЧАНИЕ В программном коде используется интерфейс. Откровенно говоря, в данном случае можно было бы обойтись и без него. Искусство про-

граммирования от этого не пострадало бы. Но мы программировать

только учимся, поэтому для нас важен сам процесс, а не результат. На

эту ситуацию можно посмотреть и по-иному: интерфейсы настолько

хороши, что не помешают в любой ситуации.

Листинг 6.4.  Знакомство с интерфейсами

using System;

using System.Drawing;

using System.Windows.Forms;

// Описание интерфейса:

interface IBase{

// Интерфейсный индексатор:

продолжение

230

Глава 6. Важные конструкции

Листинг 6.4 (продолжение)

Button this[bool s]{

get; // Аксессор для считывания значения

set; // Аксессор для присваивания значения

}

// Интерфейсное свойство:

string text{

set; // Аксессор для присваивания значения

}

// Интерфейсный метод (для обработки

// щелчка на кнопке):

void OnBtnClick(Object btn,EventArgs ea);

// Интерфейсный метод (для изменения

// текста метки):

void textChange();

}

// Класс, наследующий класс Form и

// реализующий интерфейс IBase:

class MForm:Form,IBase{

// Закрытое поле - ссылка на объект кнопки:

private Button bOK;

// Еще одно закрытое поле - ссылка на кнопку:

private Button bCancel;

// Закрытое поле - ссылка на текстовую метку:

private Label lbl;

// Закрытое целочисленное поле-счетчик:

private int count;

// Индексатор:

public Button this[bool s]{

get{ // Аксессор для считывания значения

if(s) return bOK;

else return bCancel;

}

set{ // Аксессор для присваивания значения

if(s) bOK=value;

else bCancel=value;

}

}

// Свойство:

public string text{

set{ // Аксессор для присваивания значения

lbl.Text=value;

}

}

// Конструктор класса:

public MForm(){

Интерфейсы           231

// Положение и размер окна формы:

Bounds=new Rectangle(500,300,450,250);

// Тип границы формы:

FormBorderStyle=FormBorderStyle.Fixed3D;

// Заголовок окна формы:

Text="Окно с двумя кнопками";

int h=30; // Высота кнопок

int w=150; // Ширина кнопок

// Создание объекта для шрифта:

Font fnt=new Font("Arial",13,FontStyle.Bold);

// Применяем шрифт для формы:

Font=fnt;

// Начальное значение для счетчика:

count=0;

// Создание первой кнопки:

this[true]=new Button();

// Текст первой кнопки:

this[true].Text="OK";

// Положение и размеры кнопки:

this[true].Bounds=new Rectangle(50,180,w,h);

// Создание второй кнопки:

this[false]=new Button();

// Текст второй кнопки:

this[false].Text="Отмена";

// Положение и размер кнопки:

this[false].SetBounds(250,180,w,h);

// Создание делегата обработчика сразу

// для двух кнопок:

EventHandler eh=new EventHandler(OnBtnClick);

// Регистрация делегата для первой кнопки:

this[true].Click+=eh;

// Регистрация делегата для второй кнопки:

this[false].Click+=eh;

// Создание текстовой метки:

lbl=new Label();

// Положение и размеры области метки:

lbl.SetBounds(50,30,350,120);

// Способ выравнивания текста в области метки:

lbl.TextAlign=ContentAlignment.MiddleCenter;

// Присваивание (неявное) текстового

// значения метке:

textChange();

// Добавление текстовой метки в окно формы:

Controls.Add(lbl);

// Добавление первой кнопки в окно формы:

Controls.Add(this[true]);

продолжение

232

Глава 6. Важные конструкции

Листинг 6.4 (продолжение)

// Добавление второй кнопки в окно формы:

Controls.Add(this[false]);

}

// Метод для обработки щелчков на кнопках:

public void OnBtnClick(Object btn,EventArgs ea){

// Проверяем, на какой кнопке выполнен щелчок:

if(btn==this[true]){ // Если щелкнули на первой кнопке

count++;

textChange();

}

else Application.Exit(); // Если щелкнули на второй кнопке

}

// Метод для изменения текстового свойства:

public void textChange(){

// Значение текстового свойства - оно же

// текстовое значение метки:

text="Кнопка OK нажата "+count+" раз!";

}

}

// Класс с главным методом программы:

class InterfaceDEmo{

// Инструкция выполнять программу

// в едином потоке:

[STAThread]

// Главный метод программы:

public static void Main(){

// Отображение окна:

Application.Run(new MForm());

}

}

Поскольку с интерфейсом мы сталкиваемся впервые, имеет смысл оста-

новиться на его программном коде подробнее. Итак, в программе описан

интерфейс IBase. Для этого использован следующий программный код: interface IBase{

Button this[bool s]{

get;

set;

}

string text{

set;

}

void OnBtnClick(Object btn,EventArgs ea);

void textChange();

}

Интерфейсы           233

Заголовок интерфейса состоит из ключевого слова interface и име-

ни интерфейса IBase. В интерфейсе описаны два метода, свойство и ин-

дексатор. Описание начинается с индексатора. Заголовок индексатора

Button this[bool s] означает, что элементом индексатора является объ-

ектная ссылка типа Button (то есть объект кнопки). Индексом индексатора

может выступать переменная логического типа bool. Таким образом мы

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

индексатор мы впоследствии «спрячем» две кнопки нашей оконной фор-

мы. Аксессоры в индексаторе не описаны. Там только есть ключевые слова

get и set. Это говорит о том, что индексатор должен иметь как аксессор для

доступа к значению индексатора, так и аксессор для присваивания значе-

ния индексатору.

Свойство текстовое и называется text. Тело свойства содержит единствен-

ную инструкцию set. Поэтому при определении свойства в классе, кото-

рый реализует свойство, нужно будет описать только аксессор для присва-

ивания значения свойству. Забегая вперед заметим, что в свойство будет

«упаковано» текстовое содержимое метки формы.

Объявленный в интерфейсе метод void OnBtnClick(Object btn,EventArgs ea) имеет все признаки обработчика события — он не возвращает результат

и имеет «правильные» аргументы. Мы будем использовать этот метод, по-

сле определения его кода в классе, именно как обработчик. Причем здесь

мы применяем небольшую хитрость. Кнопок у нас две, и для каждой из

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

кнопке. Поэтому метод, как мы увидим это далее, определяется так, что

выполняемые в нем команды зависят от того, на какой кнопке выполнен

щелчок.

Еще один объявленный в интерфейсе метод void textChange() также не

возвращает результат, и у него нет аргументов. Через этот метод мы реа-

лизуем процесс изменения текстового значения метки формы. Но все это

будет происходить в классе, которые реализует метод. Класс объявляется

с заголовком class MForm:Form,IBase. Класс MForm создается путем насле-

дования библиотечного класса Form и реализует интерфейс IBase. Послед-

нее обстоятельство означает, что в классе MForm должны быть описаны все

методы, свойства и индикаторы, объявленные в интерфейсе IBase. Но кое-

что в классе есть и свое. Так, у класса есть два закрытых поля, bOK и bCancel, класса Button (кнопки), а также закрытое поле lbl класса Label (текстовая

метка). Еще имеется закрытое целочисленное поле count, которое призва-

но служить счетчиком количества щелчков на первой кнопке (первой в на-

шем случае будет кнопка bOK). Также в классе описывается то, что должно

быть описано, равно как и конструктор класса.

Описание индексатора — это, по большому счету, описание его аксессоров

(тех из них, что объявлены в интерфейсе). Заголовок индексатора такой

234

Глава 6. Важные конструкции

же, как в интерфейсе — за исключением, разве что, атрибута public, кото-

рый является обязательным как для индексатора, так и для прочих членов

интерфейса, описываемых в классе, реализующем интерфейс.

ПРИМЕЧАНИЕ Хотя члены интерфейса описываются без атрибута уровня доступа, по умолчанию все они являются открытыми. Поэтому при описании

этих членов в классе, реализующем интерфейс, необходимо указывать

атрибут public.

У индексатора имеются оба аксессора. Аксессор для считывания значения

определяется с помощью условного оператора. В условном операторе про-

веряется индекс индексатора. Поскольку это логическое значение, такая

ситуация корректна. Если индекс равен true, в качестве результата возвра-

щается ссылка на кнопку bOK. В противном случае возвращается ссылка

на кнопку bCancel. По похожей схеме выполняется и set-аксессор. Если

индекс индексатора равен true, значение присваивается переменной bOK, а в противном случае значение присваивается переменной bCancel. Хотя

острой необходимости в этом нет, ссылки на кнопки мы будем выполнять

через индексатор.

Текстовое свойство text содержит описание set-аксессор, в котором ко-

мандой lbl.Text=value присваивается значение свойству Text метки lbl.

Поэтому, обращаясь к свойству text, мы на самом деле будем обращаться

к свойству lbl.Text. Как говорится, мелочь, а приятно.

Все самое интересное происходит в конструкторе класса. Некоторые ко-

манды конструктора нам уже знакомы. А некоторые знакомые операции

выполняются способом, отличным от тех, которые мы использовали ранее.

Например, положение и размер окна формы мы задаем «одним махом», с по-

мощью команды Bounds=new Rectangle(500,300,450,250). Здесь свойству

Bounds формы в качестве значения присваивается экземпляр структуры

Rectangle. Аргументами конструктору передаются четыре целочисленных

значения. Первые два определяют положение (координаты относительно

левого верхнего угла экрана) окна формы на экране, а два других — шири-

на и высота окна формы соответственно. Чтобы можно было использовать

структуру Rectangle, в шапку программного кода была добавлена инструк-

ция подключения пространства имен using System.Drawing.

Тип границы формы определяется командой FormBorderStyle=FormBorder­

Style.Fixed3D. Константа Fixed3D перечисления FormBorderStyle означает, что у формы будут объемные края, что придает форме эффект вдавлива-

ния. Заголовок окна формы определяется командой Text="Окно с дву­

мя кнопками". Целочисленные переменные h и w мы вводим для удобства.

Они определяют высоту и ширину кнопок.

Интерфейсы           235

Мы потихоньку начинаем проявлять наши дизайнерские наклонности.

Начнем с малого — переопределим шрифт для формы. Для начала нам

нужно создать объект, который впитал бы в себя наши представления об

удачном шрифте. Исполненные решимости, командой Font fnt=new Font ("Arial",13,FontStyle.Bold) создаем объект класса Font. Этот объект со-

ответствует жирному шрифту типа Arial размера 13. Чтобы использовать

этот шрифт в форме, ссылку на созданный объект следует присвоить свой-

ству Font формы, что мы, собственно, и делаем командой Font=fnt. На этом

блок команд по настройке параметров формы завершен.

Командой count=0 для надежности присваиваем начальное нулевое значе-

ние счетчику count.

«Для надежности» — потому что по умолчанию поле и так получит

нулевое  значение.  Но  в  жизни  действует  один  простой  принцип:

«Хочешь, чтобы все было сделано правильно — сделай сам». Поэтому

на случай не полагаемся и, несмотря ни на что, присваиваем полю

начальное нулевое значение.

Первую кнопку (объект) создаем командой this[true]=new Button().

В данном случае вместо прямой ссылки bOK мы использовали индексатор

this[true]. Соответственно, для второй кнопки вместо ссылки bCancel бу-

дем использовать индексатор this[false], а команда создания этой кнопки

имеет вид this[false]=new Button(). И здесь важно понимать, что никакой

необходимости в таком пижонстве нет.

Текст первой кнопки задаем командой this[true].Text="OK", а размеры

и по ложение — с помощью команды this[true].Bounds=new Rectangle(50, 180,w,h). Нечто похожее мы уже видели. Только раньше речь шла о свойстве

Bounds формы, а теперь это свойство кнопки. Поэтому экземпляр структу-

ры Rectangle определяет в данном случае положение в форме кнопки (два

первых аргумента конструктора) и ее геометрические параметры (два дру-

гих аргумента конструктора). Название второй кнопки определяем с помо-

щью команды this[false].Text="Отмена". Что касается положения кнопки

и границ, то для второй кнопки мы используем подход, альтернативный

рассмотренному выше. Командой this[false].SetBounds(250,180,w,h) за-

даем нужные настройки. Здесь мы прибегли к помощи метода SetBounds(), аргументы которого имеют тот же смысл, что и аргументы конструктора

структуры Rectangle.

Создание делегата обработчика сразу для двух кнопок выполняется коман-

дой EventHandler eh=new EventHandler(OnBtnClick). Таким образом, экзем-

пляр делегата eh ссылается на метод OnBtnClick(). Регистрируем экземпляр

236

Глава 6. Важные конструкции

делегата командами this[true].Click+=eh (регистрация для первой кноп-

ки) и this[false].Click+=eh (регистрация для второй кнопки).

Текстовая метка создается уже знакомым для нас способом — командой

lbl=new Label(). Положение и размеры области метки задаем с помощью

метода SetBounds(), вызвав его в команде lbl.SetBounds(50,30,350,120).

Способ выравнивания текста в области метки (по высоте — выравнивание

по середине, по горизонтали — выравнивание по центру) определяется ко-

мандой lbl.TextAlign=ContentAlignment.MiddleCenter. Чтобы присвоить

тексту метки значение, вызываем метод textChange().

Что касается метода textChange(), то определен он достаточно про-

сто: в теле метода командой text=«Кнопка OK нажата "+count+" раз!»

текстовому свойству text присваивается строка, содержащая, кроме

прочего, текущее значение счетчика count. Счетчик этот имеет на-

чальное  нулевое  значение,  как  мы  увидим  дальше,  увеличивается

на единицу каждый раз, когда пользователь выполняет щелчок на

первой кнопке (кнопка OK).

Наконец, добавляем созданные элементы в окно формы. Для этого исполь-

зуем метод Controls.Add(): текстовую метку добавляем командой Controls.

Add(lbl), кнопки добавляются командами Controls.Add(this[true]) и Controls.Add(this[false]).

Как отмечалось ранее, поскольку обе кнопки регистрируют обработчиком

щелчка один и тот же метод (а именно, метод OnBtnClik()), то метод для об-

работки щелчков на кнопках должен иметь возможность как-то эти кнопки

«различать». И здесь на помощь приходит первый аргумент метода. Имен-

но этот аргумент «знает», на какой кнопке выполнен щелчок. Более того, объект является ссылкой на объект, вызвавший событие. Поэтому резуль-

татом выражения btn==this[true], которое указано условием в условном

операторе в теле метода, является значение true, если щелчок выполнен

на первой кнопке, и false в противном случае (методом исключения полу-

чается, что в этом случае щелчок выполнен на второй кнопке). Для первой

кнопки (если щелчок выполнен на ней) предназначены команды count++

(увеличение значения счетчика щелчков на первой кнопки) и textChange() (изменение текстового значения метки). Для случая, когда щелчок вы-

полнен на второй кнопке, команда одна — инструкция завершить работу

Application.Exit().

В главном методе программы командой Application.Run(new MForm()) за-

пускаем оконную форму. В результате выполнения программы отобража-

ется графическое окно с двумя кнопками и текстом. Текст сообщает о том, что кнопка OK еще ни разу не нажималась. Как выглядит окно в самом на-

чале выполнения программы, показано на рис. 6.5.

Интерфейсные переменные           237

Рис. 6.5.  Так выглядит окно при запуске программы

Как ситуация будет разворачиваться в дальнейшем, зависит во многом от

наших героических действий. Каждый наш щелчок на кнопке OK приводит

к тому, что на единицу увеличивается число щелчков на кнопке в тексто-

вом сообщении в центральной части окна. На рис. 6.6 показано, как будет

выглядеть окно после нескольких щелчков на кнопке OK.

Рис. 6.6.  Вид окна после нескольких щелчков на кнопке OK —

изменилось содержание текстового поля

Но стоит нам щелкнуть на кнопке Отмена, как окно будет закрыто, а работа

приложения завершена.

Интерфейсные переменные

Эта теория недостаточно безумна, чтобы быть верной.

Н. Бор

Есть одна очень интересная и полезная особенность интерфейсов. Заклю-

чается она в том, что можно объявлять переменные, которые имеют тип

238

Глава 6. Важные конструкции

интерфейса. Такие переменные называются интерфейсными. Во многом

интерфейсные переменные напоминают объектные переменные. Как

и объектная переменная, интерфейсная переменная может ссылаться на

объект. До этого мы встречались в основном с простыми ситуациями, когда объектная переменная определенного класса ссылается на объект

того же класса. И здесь все выглядит вполне логично. А на какой объект

может ссылаться интерфейсная переменная? Ведь для интерфейса объ-

ект не создается. Ответ простой и несколько неожиданный: интерфейсная

переменная может ссылаться на объект любого класса, который реализует

интерфейс. Правда, имеется одно существенное ограничение: через интер-

фейсную ссылку (переменную) доступ есть только к тем членам класса, которые описаны в реализуемом интерфейсе. Это непримечательное на

первый взгляд обстоятельство имеет далеко идущие последствия. Чтобы

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

в листинге 6.5.

Листинг 6.5.  Интерфейсные переменные

using System;

// Интерфейс с одним объявленным методом:

interface IMath{

// Метод с целочисленным аргументом

// и целочисленным результатом:

int GetNumber(int n);

}

// Класс, реализующий интерфейс:

class Factorial:IMath{

// Метод для вычисления факториала числа:

public int GetNumber(int n){

int res=1; // Начальное значение

// переменой-результата

for(int i=2;i<=n;i++){ // Вычисление факториала

res*=i;

}

return res; // Результат

}

}

// Еще один класс, реализующий интерфейс:

class Fibonacci:IMath{

// Метод для вычисления чисел Фибоначчи:

public int GetNumber(int n){

int a=1,b=1; // Начальные числа последовательности

for(int i=3;i<=n;i++){ // Вычисление чисел

// последовательности

b=a+b; // Последнее число

a=b-a; // Предпоследнее число

Интерфейсные переменные           239

}

return b; // Результат

}

}

// Класс с главным методом программы:

class IRefDemo{

// Главный метод программы:

public static void Main(){

// Интерфейсная переменная:

IMath r;

// Ссылка на объект класса Factorial:

r=new Factorial();

// Вызов метода GetNumber() через

// интерфейсную ссылку (переменную):

Console.WriteLine("Факториал числа 10!={0}.",r.GetNumber(10));

// Ссылка на объект класса Fibonacci():

r=new Fibonacci();

// Вызов метода GetNumber() через

// интерфейсную ссылку (переменную):

Console.WriteLine("10-е число Фибоначчи:

{0}.",r.GetNumber(10));

// Ожидание нажатия клавиши Enter:

Console.ReadLine();

}

}

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

метод GetNumber(). У метода — один целочисленный аргумент, и метод

возвращает целочисленный результат. Еще в программе есть два клас-

са: Factorial и Fibonacci. Каждый из этих классов реализует интерфейс

IMath. В каждом из этих классов описывается метод GetNumber(), но опи-

сывается по-разному. В классе Factorial этот метод вычисляет факто-

риал числа, а в классе Fibonacci метод описан так, что вычисляет число

Фибоначчи.

В главном методе программы командой IMath r объявляется интерфейсная

переменная r. В качестве типа такой переменной указывается имя интер-

фейса IMath. Командой r=new Factorial() в качестве значения этой интер-

фейсной переменной присваивается ссылка на объект класса Factorial.

Это можно делать, поскольку класс Factorial реализует интерфейс IMath.

При этом, вызывая метод GetNumber() через переменную r, вызываем на

самом деле метод, определенный в классе Factorial. Эта ситуация «про-

веряется» в команде Console.WriteLine("Факториал числа 10!={0}.",r.

GetNumber(10)). После этого командой r=new Fibonacci() переменной r присваивается ссылка на объект класса Fibonacci. Этот класс тоже реа-

лизует интерфейс IMath. Теперь при вызове метода GetNumber() через

240

Глава 6. Важные конструкции

интерфейсную переменную r выполняется код из класса Fibonacci. Так

и происходит при выполнении команды Console.WriteLine("10-е чис­

ло Фибоначчи: {0}.",r.GetNumber(10)). Результат выполнения програм-

мы показан на рис. 6.7.

На всякий случай приведем краткие пояснения по поводу вычисления

результата при определении метода GetNumber() в классах Factorial и Fibonacci. Начнем с класса Factorial — там все проще. Локальная

переменная res получает начальное значение 1, после чего в опе-

раторе цикла последовательно умножается на значение индексной

переменной, которая пробегает значения от 2 до n (аргумент метода).

В результате получаем произведение натуральных чисел до n вклю-

чительно. Это и есть результат метода.

При вычислении числа Фибоначчи в классе Fibonacci переменным

a  и  b  присваиваются  единичные  значения.  Идея  в  том,  что  пере-

менная  a  «помнит»  предпоследнее  число  в  последовательности, а переменная b «помнит» последнее число в последовательности.

В операторе цикла за один цикл вычисляется следующая пара зна-

чений. Для этого выполняются команды b=a+b и a=b-a. В результате

переменная b получает новое значение (это сумма двух предыдущих

значений), а значение переменной a равно тому значению, которое

имела переменная b. Действительно, предположим, что в какой-то

момент значение переменной a равно , а значение переменной b равно  .  Нам  нужно  добиться  того,  чтобы  значение  переменой  b стало +, а значение переменной a стало равным . После вы-

полнения команды b=a+b переменная b имеет новое значение +, а у переменной a осталось старое значение . Как из значений +

(переменная b) и  (переменная a) получить значение ? Очень

просто — от одного значения отнять другое, для чего и использована

команда a=b-a.

Рис. 6.7.  Результат выполнения программы с интерфейсной переменной

Таким образом, мы дважды использовали инструкцию r.GetNumber() и по-

лучали разные результаты, в зависимости от того, на какой объект ссыла-

лась интерфейсная переменная r на момент вызова метода GetNumber().

Интерфейсные переменные           241

Ситуация с интерфейсными ссылками/переменными может быть доста-

точно нетривиальной, особенно если речь идет о реализации в классе сра-

зу нескольких интерфейсов. Но обсуждение всех возможных вариантов

в наши планы не входит. Более того, напомним, что способностью ссылать-

ся на «чужие» объекты обладают не только интерфейсные переменные, но

и объектные переменные базовых классов. Такие объектные переменные

могут ссылаться на объекты производных классов.

Методы и классы

во всей красе

Я предупреждал. У джентльменов нет

оснований обижаться на меня.

Из к/ф «В поисках капитана Гранта»

Нами достигнуты некоторые успехи. Мы уже можем создавать приложение

с окном и кнопкой, умеем перегружать операторы, знакомы с наследовани-

ем и не пугаемся при слове «интерфейс». Может создаться впечатление, что ничего интересного в C# уже не осталось. Конечно, это совсем не так.

Часть наших иллюзий развеется в этой главе. Ее мы посвятим рассмотре-

нию тех вопросов и особенностей языка, которые мы оставили «за кавыч-

ками» в предыдущих главах. В основном вопросы, рассматриваемые здесь, имеют отношение к методам и некоторым особенностям классов. Откро-

венно говоря, материал главы несколько эклектичен. Вместе с тем вопросы

здесь мы рассмотрим полезные, а где-то, может, даже и интересные.

Механизм передачи аргументов методам

Что касается смелости, тут я спорить не

стану. Вот по частностям я готов поспорить.

Из к/ф «Семнадцать мгновений весны»

До этого мы смело использовали методы, в том числе и с аргументами.

И никаких особых проблем по поводу того, как передавать аргументы

Механизм передачи аргументов методам           243

методам, у нас не возникало даже на горизонте. То есть как бы даже на-

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

и иллюзорна. Чтобы не быть голословными, просто рассмотрим пример.

Обратимся к листингу 7.1. Сразу отметим, что, хотя формально код (син-

таксис) в листинге правильный, выполняется он не так, как можно было

бы ожидать.

Листинг 7.1.  Передача аргументов по значению

using System;

class SmallTrouble{

// Статический метод для обмена значениями

// аргументов

// (выполняется, но долг свой не выполняет):

static void swap(int a,int b){

// Значения аргументов до обмена значениями:

Console.WriteLine("До обмена: a={0} и b={1}.",a,b);

// Обмен значениями:

int t=b;

b=a;

a=t;

// Значения аргументов после обмена значениями:

Console.WriteLine("После обмена: a={0} и b={1}.",a,b);

}

public static void Main(){

// Целочисленные переменные:

int a=10,b=200;

// Производим "обмен":

swap(a,b);

// Проверяем результат:

Console.WriteLine("Проверяем: a={0} и b={1}.",a,b);

// Ожидание нажатия клавиши Enter:

Console.ReadLine();

}

}

Мы начнем с классической ситуации. В классе SmallTrouble кроме метода

Main() есть еще один статический метод — swap(). Метод не возвращает

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

создать блиц-портрет для этого метода, его характеристика звучала бы так: метод «обменивает» значения аргументов — при вызове метода перемен-

ные, указанные аргументами, обмениваются значениями.

На самом деле ничем эти переменные не обмениваются — и в этом

нам предстоит убедиться.

244

Глава 7. Методы и классы во всей красе

Хотя код метода тривиальный, выполняется он «неожиданно», поэто-

му проанализируем метод swap() в деталях. Так, командой Console.

WriteLine("До обмена: a={0} и b={1}.",a,b) перед началом манипуляций

по обмену в консольное окно выводится окно с сообщением о том, каковы

значения аргументов, переданных методу (переменные a и b). Затем с по-

мощью трех незатейливых команд (а именно, t=b, b=a и a=t) переменные a и b обмениваются значениями. В принципе, ситуация была бы банальной, не будь переменные a и b аргументами метода. Как мы узнаем дальше, это

очень важный момент. Наконец, командой Console.WriteLine("После об­

мена: a={0} и b={1}.",a,b) проверяем результат наших наивных кальку-

ляций. Схема простая:

1. Проверили значения аргументов.

2. Поменяли значения аргументов.

3. Проверили значения аргументов.

В главном методе программы проверяем работу метода swap(). Для этого

создаем две целочисленные переменные a=10 и b=200 и передаем их аргу-

ментами методу swap(). После вызова метода с указанными аргументами

командой Console.WriteLine("Проверяем: a={0} и b={1}.",a,b) проверяем

значения переменных. Можно ожидать, что переменные должны обменять-

ся значениями. Но в глубине души мы понимаем, что, если бы это было

действительно так, не было бы смысла рассматривать этот пример. Наши

самые смелые прогнозы подтверждает результат выполнения программы, представленный на рис. 7.1.

Рис. 7.1.  Результат выполнения программы с «неправильным» методом

для обмена значениями аргументов

Что мы видим? При проверке значений аргументов в методе swap() все

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

тье, последнее сообщение обескураживает — у переменных a и b значения

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

swap(), то переменные a и b демонстрируют полную лояльность. Но как

только метод завершил работу, все становится как раньше. Причины этих

симуляций со стороны аргументов метода swap() объясняются очень про-

сто (просто, но странно) — при передаче аргументов методу на самом деле

Механизм передачи аргументов методам           245

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

чем этот режим используется по умолчанию, то есть всегда. Мы до этого

такой банальной подмены не замечали, поскольку не пытались в методах

изменить значения аргументов.

ПРИМЕЧАНИЕ Здесь имеются в виду аргументы необъектных типов — те аргу-

менты, которые не относятся к объектным переменным. При пере-

даче  объектной  переменной  аргументом  для  нее  тоже  создается

копия. Но поскольку копия ссылается на тот же самый объект, что

и оригинал, то для объектных переменных ситуация с клонами не

столь трагична.

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

значению и состоит в том, что на самом деле в метод передается копия ар-

гумента. Другой механизм называется передачей аргумента по ссылке и со-

стоит в том, что в метод передается непосредственно та переменная, что

указана аргументом. По умолчанию аргументы передаются по значению.

Если не предпринимать никаких дополнительных усилий, то вместо тех

переменных, что указаны аргументами методов, в методы будут переда-

ваться копии этих переменных. Особенность этих копий в том, что они су-

ществуют до тех пор, пока выполняется метод. Как только метод завершил

свою работу, все локальные переменные, в том числе и копии переменных-

аргументов, автоматически уничтожаются.

Теперь нам легко объяснить специфическую работу метода swap(). Ког-

да выполняется команда swap(a,b), для переменных a и b автоматически

создаются копии и все операции в методе выполняются с этими копия-

ми. Именно копии обмениваются значениями. Поэтому, когда проверя-

ется результат обмена, все выглядит пристойно — поскольку обмен дей-

ствительно состоялся. Но обмен на уровне копий! Оригиналы остались

неизменными, в чем мы и убеждаемся, когда проверяем значения пере-

менных a и b после вызова метода swap(). Вот такая получается «война

клонов».

Поскольку механизм передачи аргументов по значению используется

и без нашего вмешательства, возникает вопрос: как и где нам нужно «вме-

шаться» в программный код, чтобы аргументы передавались по ссылке?

Как и все в C#, здесь ответ простой: при описании метода и его вызове

аргументы, которые передаются по ссылке, необходимо указать с атрибу-

том ref. В листинге 7.2 приведен пример программы с «исправленным»

методом swap().

246

Глава 7. Методы и классы во всей красе

Листинг 7.2.  Передача аргументов по ссылке

using System;

class NoTrouble{

// Статический метод для обмена значениями

// аргументов

// (выполняется так, как надо):

static void swap(ref int a,ref int b){

// Значения аргументов до обмена значениями:

Console.WriteLine("До обмена: a={0} и b={1}.",a,b);

// Обмен значениями:

int t=b;

b=a;

a=t;

// Значения аргументов после обмена значениями:

Console.WriteLine("После обмена: a={0} и b={1}.",a,b);

}

public static void Main(){

// Целочисленные переменные:

int a=10,b=200;

// Производим правильный "обмен":

swap(ref a,ref b);

// Проверяем результат:

Console.WriteLine("Проверяем: a={0} и b={1}.",a,b);

// Ожидание нажатия клавиши Enter:

Console.ReadLine();

}

}

Результат выполнения этой программы представлен на рис. 7.2.

Рис. 7.2.  Результат выполнения программы с «правильным» методом

для обмена значениями аргументов

Несложно убедиться, что переменные a и b в результате выполнения ко-

манды swap(ref a,ref b) действительно обменялись значениями.

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

типа. В качестве небольшой иллюстрации рассмотрим пример в лис-

тинге 7.3.

Механизм передачи аргументов методам           247

По сравнению с первоначальным примером, изменения в программ-

ный код внесены лишь в двух местах. Во-первых, заголовок метода

описан как static void swap (ref int a,ref int b), и, во-вторых, при вы-

зове метода использована инструкция swap(ref a,ref b).

Листинг 7.3.  Изменение аргументов ссылочных типов

using System;

// Класс с целочисленным полем:

class Nums{

// Открытое целочисленное поле:

public int num;

// Конструктор с одним аргументом:

public Nums(int n){

num=n;

}

// Метод для отображения значения

// целочисленного поля:

public void show(){

Console.WriteLine("поле объекта: "+num);

}

}

// Класс с несколькими статическими методами:

class RefDemo{

// Статический метод для увеличения на единицу

// значения поля объекта-аргумента:

public static void up(Nums obj){

// Увеличиваем на единицу значение поля

// объекта-аргумента:

obj.num++;

// Текстовое сообщение в консольное окно:

Console.Write("Объект-аргумент: ");

obj.show(); // Отображение значения поля объекта

}

// Статический метод для "обмена"

// объектными ссылками.

// Аргументы передаются по ссылке:

public static void swap(ref Nums x,ref Nums y){

Nums t=x; // Локальная объектная переменная

x=y;

y=t;

// Проверка результата:

Console.Write("Первый объект-аргумент: ");

продолжение

248

Глава 7. Методы и классы во всей красе

Листинг 7.3 (продолжение)

x.show(); // Отображение поля первого

// объекта-аргумента

Console.Write("Второй объект-аргумент: ");

y.show(); // Отображение поля второго

// объекта-аргумента

}

// Главный метод программы:

public static void Main(){

// Создаем объекты класса Nums:

Nums a=new Nums(10);

Nums b=new Nums(200);

// Изменяем объект - увеличиваем значение поля:

up(a);

Console.Write ("Проверка: ");

a.show(); // Проверяем результат увеличения поля

// Объектные переменные обмениваются

// значениями:

swap(ref a,ref b);

// Проверка результата обмена:

Console.Write ("Проверка. Первый объект: ");

a.show(); // Отображение значения поля

// первого объекта

Console.Write ("Проверка. Второй объект: ");

b.show();// Отображение значения поля

// второго объекта

// Ожидание нажатия какой-нибудь клавиши:

Console.ReadKey();

}

}

Мы описываем класс Nums, у которого есть целочисленное поле num, кон-

структор с одним аргументом, а также метод show(), который позволяет

отобразить в консольном окне значение поля num. Объекты класса Nums будут «подопытными кроликами», на которых мы проверим корректность

выполнения двух статических методов. Методы называются up() и swap(), и описаны они в классе RefDemo (в этом классе, кстати, описан и главный ме-

тод программы). Оба метода не возвращают результат. У метода up() один

аргумент — это объект obj класса Nums. Аргумент передается «в обычном

режиме» — инструкция ref не используется. В ней просто нет необходимо-

сти. В теле метода командой obj.num++ на 1 увеличивается значение поля

num объекта-аргумента obj, а результат изменений проверяется командой

obj.show(). Благодаря этому мы узнаем, как ситуация с увеличением поля

объекта-аргумента выглядит изнутри метода up().

Механизм передачи аргументов методам           249

У метода swap() два аргумента (обозначены как a и b), и оба являются

объектами класса Nums. Метод с таким названием традиционно использу-

ется нами для взаимовыгодных обменов. В данном случае обмениваться

значениями будут объектные переменные. То, что до вызова метода было

первым объектом, станет вторым, а второй объект станет первым. Причем

аргументы, несмотря на то что они относятся к ссылочному типу (это объ-

ектные переменные), передаются по ссылке — оба аргумента метода опи-

саны с инструкцией ref. В теле метода объектные переменные по традици-

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

очередь, будет ссылаться на тот объект, на который до вызова метода ссы-

лалась переменная a. С помощью команд a.show() и b.show() мы проверя-

ем, каковы значения полей объектов a и b после обмена значениями аргу-

ментов метода.

В главном методе программы мы создаем два объекта класса Nums: объект

a со значением поля 10 и объект b со значением поля 200. После выполне-

ния команды up(a) значение поля a увеличивается с 10 до 11. Результат

проверяем командой a.show(). Затем командой swap(ref a,ref b) меняем

объекты a и b местами. Поверка последствий осуществляется с помощью

команд a.show() и b.show(). Результат выполнения программы представ-

лен на рис. 7.3.

Рис. 7.3.  Изменение аргументов ссылочных типов:

результат выполнения программы

Вывод у нас один — все работает правильно. Об этом свидетельствует

хотя бы тот факт, что после выполнения соответствующих манипуляций

в статических методах проверка внутри метода и проверка по завершении

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

же закрадывается подспудное сомнение по поводу метода swap(): а может, все работало бы и без использования передачи аргументов этому методу

по ссылке? Другими словами, вопрос такой: будет ли все выполняться так

же корректно, если из программного кода удалить все инструкции ref?

Ответ такой: нет, не будет. Желающие могут проделать процедуру само-

стоятельно: в программном коде листинга 7.3 удалить четыре инструкции

250

Глава 7. Методы и классы во всей красе

ref — две в описании метода swap() и две в команде вызова этого метода.

Если после этого запустить команду на выполнение, получим результат, как на рис. 7.4.

Рис. 7.4.  Изменение аргументов ссылочных типов:

некорректный обмен ссылок в объектных переменных

Обратите внимание: по сравнению с предыдущим случаем в консоль-

ном окне изменились две последние строки.

На словах добавим, что метод up() свою работу выполняет честно, хотя

ему аргумент как передавался по значению, так и передается. А вот метод

swap() местами объекты не поменял, хотя при проверке внутри метода все

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

руем, что будет, если в метод swap() аргументы передавать не по ссылке, а по значению. Для удобства и большей наглядности наших абстрактных

рассуждений обозначим аргументы, которые передаются методу, как a и b.

Хотя это и объектные переменные, при их передаче в качестве аргументов

автоматически создаются копии — назовем их A и B. Значение копии A та-

кое же, как и переменной a, а значение копии B такое же, как и переменной

b. Поэтому переменные a и A ссылаются на один и тот же объект, и пере-

менные b и B ссылаются на один и тот же объект. Но вот операции по об-

мену выполняются с копиями. Поэтому после того, как обмен произведен, копия A ссылается на объект b, а копия B ссылается на объект A, что и под-

тверждает вызов метода show() в теле статического метода swap(). Здесь

следует помнить, что метод show() вызывается из объектов-копий. А что

же с переменными a и b? Их значения стались прежними, в чем мы и убеж-

даемся после завершения работы метода swap(). Вот почему методу swap() аргументы нужно передавать по ссылке несмотря на то, что они являются

переменными ссылочного типа.

С методом up() таких неприятностей не происходит. Объяснение тоже

достаточно простое. Если объектная переменная передается аргументом

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

Аргументы без значений и переменное количество аргументов           251

с объектом, а не с объектной переменной, результат такой, как надо, и без

использования механизма передачи аргумента по ссылке.

Аргументы без значений и переменное

количество аргументов

Может, где-нибудь высоко в горах,

но не в нашем районе, вы что-нибудь

обнаружите для вашей науки.

Из к/ф «Кавказская пленница»

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

ментов метода, которые позволяют сделать программный код достаточно

гибким и эффективным, а иногда и просто эффектным. В этом разделе мы

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

фиксировано (то есть количество аргументов на момент описания мето-

да неизвестно), а также передачу аргументов методам без значений. В по-

следнем случае речь идет о том, что в C# разрешается (при определенном

стечении обстоятельств) передавать в качестве аргументов методам пере-

менные, которые объявлены, но которым не присвоено значение. Другое

дело, зачем нужно так поступать. Но мы и это обсудим. Начнем же с того, как описать метод, у которого неизвестно сколько аргументов.

Общий рецепт состоит в том, что для описания метода с нефиксированным

количеством аргументов использовать массив с идентификатором params.

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

торого наперед неизвестно, аргумент метода описывается с атрибутом

params, а сам список аргументов отождествляется с массивом элементов

соответствующего типа. В качестве простенькой иллюстрации рассмотрим

пример, представленный в листинге 7.4. В этой программе описан метод, который позволяет вычислять среднее арифметическое значение для на-

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

Листинг 7.4.  Метод с нефиксированным количеством аргументов

using System;

// Класс со статическим методом с переменным

// количеством аргументов:

class ParamsDemo{

// Метод с переменным количеством аргументов:

static double average(params double[] nums){

продолжение

252

Глава 7. Методы и классы во всей красе

Листинг 7.4 (продолжение)

double res=0; // Начальное значение

// переменной-результата

// Информационное сообщение:

Console.WriteLine("Числовой ряд:");

// Перебор элементов массива - аргументов

// метода:

foreach(double s in nums){

Console.Write(s+" "); // Аргумент отображается

// в консоли

res+=s; // Вычисляется сумма аргументов

}

Console.WriteLine(); // Переход к новой строке

// Вычисление среднего значения:

res/=nums.Length;

// Результат метода:

return res;

}

// Главный метод программы:

public static void Main(){

// Вызов метода с 10 аргументами:

double r=average(1,3,6,8,2,-4,2,1,-5,-3);

// Проверяем результат:

Console.WriteLine("Среднее значение равно "+r);

// Вызов метода с 15 аргументами:

r=average(-1,2,-5,8,2,-4,7,2,-1,5,10,-5,12,-7,-4,2);

// Проверяем результат:

Console.WriteLine("Среднее значение равно "+r);

// Ожидание нажатия какой-нибудь клавиши:

Console.ReadKey();

}

}

Сигнатура статического метода average(), который в качестве значения воз-

вращает число типа double, выглядит как average(params double[] nums).

Конечно, примечателен здесь способ описания аргумента (или аргумен-

тов — зависит от того, как на это все смотреть). Атрибут params подает нам

сигнал о том, что речь идет о методе, у которого может быть сколько угодно

числовых аргументов типа double. Формально эти аргументы интерпрети-

руются как массив, который мы назвали nums, а тип этого массива, в силу

очевидных причин, есть тип переменной массива с double-элементами. Та-

ким образом, при обработке аргументов метода average() иллюзия такая, как если бы аргументы были не отдельными числами, а числовым масси-

вом. Это удобно хотя бы потому, что при таком подходе количество аргу-

ментов в методе определяется как nums.Length.

Аргументы без значений и переменное количество аргументов           253

В теле метода инициализируется с нулевым начальным значением double-

переменная res. Эта переменная, после выполнения всех нужных вычисле-

ний, будет возвращаться как результат метода. А результатом метода, на-

помним, является среднее значение аргументов, которое определяется как

сумма аргументов, деленная на их количество. В операторе цикла foreach() перебираются все элементы массива. Элементы выводятся в консольном

окне в одну строку. Но не это главное. Главное то, что в результате выпол-

нения оператора цикла вычисляется сумма элементов массива — то есть

сумма аргументов метода. Сумма записывается в переменную res. После

завершения оператора цикла командой res/=nums.Length вычисляется

среднее значение. Оно и возвращается как результат.

В главном методе программы метод average() вызывается дважды с раз-

ным количеством аргументов. Результаты вычислений представлены на

рис. 7.5.

Рис. 7.5.  Метод с переменным количеством аргументов: результат выполнения программы

Обращаем внимание читателя на то, что, хотя при описании метода average() мы отталкивались от того, что его аргументы реализованы в виде массива, при вызове метода аргументы передаются простым перечислением в кру-

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

Теперь обсудим способ передачи методу в качестве аргумента переменной, которой не присвоено значение. Сразу отметим, что вообще такая ситуа-

ция интерпретируется как ошибочная, поэтому, если уж мы используем

подобный экзотический код, нам предстоит каким-то образом предупре-

дить о наших планах компилятор. Благо предупредить его несложно. При

описании метода соответствующий аргумент объявляется с атрибутом out.

Такой же атрибут для аргумента указывается при вызове метода. Что ка-

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

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

разному, но один из возможных способов состоит в том, чтобы один из «ре-

зультатов» записывать в переменную, переданную аргументом методу. По-

нятно, что это далеко не единственный подход, но он допустим. Например,

254

Глава 7. Методы и классы во всей красе

мы хотим написать метод, который для числового ряда значений вычис-

ляет наибольшее и наименьшее значение. Вариантов организации такого

метода — неисчислимое множество. Один из них такой: наибольшее число

метод возвращает в качестве результата, а наименьшее число записывается

в переменную, которая передана первым аргументом методу. Именно та-

кой пример представлен в программном коде в листинге 7.5.

Листинг 7.5.  Аргумент метода — неинициализированная переменная

using System;

class OutDemo{

// Статический метод с неинициализированным

// первым аргументом:

static int MinMax(out int min,params int[] n){

// Начальное значение для результата метода:

int max=n[0];

// Минимальное значение:

min=n[0];

Console.WriteLine("Числовой ряд:"); // Сообщение в консоль

// Оператор цикла для перебора аргументов

// метода:

foreach(int s in n){

// Значение аргумента выводится в консоль:

Console.Write(s+" ");

// Группа условных операторов:

if(s>max) max=s; // Изменяем максимальное значение

if(s

}

// Переход к новой строке:

Console.WriteLine();

// Результат метода:

return max;

}

// Главный метод программы:

public static void Main(){

// Объявление целочисленных переменных:

int min,max;

// Вызов метода с первым неинициализированным

// аргументом:

max=MinMax(out min,1,0,-5,8,21,-9,11,-10,25,16);

// Сообщаем результат вычислений:

Console.WriteLine("Экстремальные значения: min={0}


и max={1}",min,max);

// Ожидание ввода символа:

Console.ReadKey();

}

}

Аргументы без значений и переменное количество аргументов           255

Статический метод с заголовком int MinMax(out int min,params int[] n) предназначен для вычисления минимального и максимального значений

среди набора числовых переменных, переданных аргументами методу.

Максимальное значение возвращается методом в качестве результата, а вот минимальное записывается в переменную, которая передана первым

аргументом методу. Этот аргумент метода описан как out int min, то есть

с атрибутом out. Количество прочих аргументов метода не фиксирова-

но, поэтому их мы описываем params int[] n, то есть с ключевым словом

params, как в предыдущем примере.

В теле метода переменной max, которую планируем возвращать в качестве

результата метода, записываем значение n[0] (второй аргумент в списке

аргументов метода — первым является переменная min для записи ми-

нимального значения). Такое же значение присваивается переменной-

аргументу min. Затем в операторе цикла перебираются аргументы метода.

Каждый элемент (значение) выводится на экран. Кроме того, каждый счи-

танный аргумент сравнивается с текущим минимальным и максимальным

значениями, и, если нужно, эти значения обновляются. Реализуется соот-

ветствующая проверка с помощью двух условных операторов.

В главном методе программы командой объявляются (но не инициализи-

руются) две целочисленные переменные, min и max, после чего с помощью

команды max=MinMax(out min,1,0,-5,8,21,-9,11,-10,25,16) эти перемен-

ные получают свои значения. По-разному, но получают: переменная max как результат метода MinMax(), а переменная min — как его аргумент. Ре-

зультат выполнения программы представлен на рис. 7.6.

Рис. 7.6.  Метод с неинициализированным аргументом: результат выполнения программы

Стоит заметить, что атрибут out (так же, как и атрибут ref, который

рассматривался ранее) указывается как при описании метода, так

и в команде вызова метода.

Кроме того, out-аргумент автоматически передается по ссылке, то есть

в метод передается «оригинал», а не «копия». Это вполне объяснимо, поскольку копию такого аргумента передавать в метод совершенно

нет никакого смысла.

256

Глава 7. Методы и классы во всей красе

Передача типа в качестве параметра

Зато так поступают одни лишь мудрецы,

Зато так наступают одни лишь храбрецы.

Из к/ф «Айболит 66»

Выше мы несколько раз описывали метод, который менял местами зна-

чения аргументов. Делали мы это для аргументов разных типов, но на

самом деле делали каждый раз одно и то же — в том смысле, что алгоритм

вычислений совершенно не зависел от типа аргументов. И такие ситуа-

ции встречаются достаточно часто. На какую мысль это нас наводит? На-

водит это нас на такую мысль, что неплохо было бы писать программные

коды, которые «лояльно» относились бы к типу данных — в том смысле, что код мы пишем один раз, а затем можем вызывать метод с данными

различных типов. Причем перегрузка метода в данном случае не очень

подходит, поскольку при перегрузке метода каждая его версия описыва-

ется явно. Здесь речь идет о программных кодах иного рода. Нас интере-

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

тип данных в виде параметра практически так же, как мы указываем ар-

гументы у метода.

Итак, переходим к обсуждению вопроса о том, как создавать методы и клас-

сы, в которых тип данных является формальным параметром.

ПРИМЕЧАНИЕ Класс с параметрами типа называется обобщенным классом, а метод

с параметрами типа называется обобщенным методом.

Соответствующая процедура может быть применена как к отдельным

методам, так и к целым классам. Мы начнем с малого — с описания ме-

тодов. Здесь есть два момента, которые нужно иметь в виду, если мы хо-

тим создать метод, в котором тип данных играет ну очень формальную

роль. Во-первых, для типа данных следует ввести идентификатор, или

параметр типа. Другими словами, необходимо придумать обозначение

для типа данных. Это обозначение (которое и будем называть параме-

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

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

формального типа, используем параметр типа из треугольных скобок.

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

глядности рассмотрим пример, в котором метод для обмена значениями

Передача типа в качестве параметра           257

своих аргументов реализован с использованием параметра типа. Пример

представлен в листинге 7.6.

Листинг 7.6.  Метод с параметром типа

using System;

// Класс пользователя:

class MyClass{

// Символьное поле:

public char s;

// Конструктор класса:

public MyClass(char s){

this.s=s;

}

}

// Класс содержит метод с параметром типа

// и главный метод программы:

class TypeParametersDemo{

// Метод с параметром типа.

// Идентификатор X обозначает тип данных:

static void swap(ref X a,ref X b){ // Два аргумента типа X

X t=a; // Локальная переменная типа X

// Присваивание переменных типа X:

a=b;

b=t;

}

// Главный метод программы:

public static void Main(){

// Объявляем и инициализируем

// целочисленные переменные:

int a=10,b=200;

// Объявляем и инициализируем

// текстовые переменные:

string A="Первый",B="Второй";

// Объявляем объектные переменные

// и создаем объекты:

MyClass objA=new MyClass('A');

MyClass objB=new MyClass('B');

// Вызываем метод с параметром типа:

swap(ref a,ref b); // Вместо X используем int

// Проверяем результат:

Console.WriteLine("Проверка: a={0} и b={1}.",a,b);

// Вызываем метод с параметром типа:

swap(ref A,ref B); // Вместо X используем string

// Проверяем результат:

Console.WriteLine("Проверка: A={0} и B={1}.",A,B); продолжение

258

Глава 7. Методы и классы во всей красе

Листинг 7.6 (продолжение)

// Вызываем метод с параметром типа:

swap(ref objA,ref objB); // Вместо X используем MyClass

// Проверяем результат:

Console.WriteLine("Проверка: objA->{0} и


objB->{1}.",objA.s,objB.s);

// Ожидание нажатия клавиши:

Console.ReadKey();

}

}

В программе для технических нужд описывается класс MyClass, у которого

есть одно символьное поле и конструктор с одним аргументом. В классе

TypeParametersDemo описывается статический метод swap(). Метод не воз-

вращает результат и содержит параметр типа X, который указан в угловых

скобках после имени метода. Сигнатура метода swap(ref X a,ref X b) означает буквально следующее:

 Идентификатор X обозначает какой-то определенный тип данных. Если

несколько переменных объявлены с типом X, то это означает, что все они

относятся к одному и тому же типу. Какой именно это тип — определя-

ется при вызове метода.


 Аргументы метода (их два) имеют тип X.


 Инструкция ref, как и ранее, означает, что аргументы типа X передаются

по ссылке.

В теле метода также встречается параметр типа X. Например, команду X t=a следует понимать так: объявляется локальная переменная типа X, и ей в ка-

честве значения присваивается переменная a. Эта команда корректна, по-

скольку обе переменные относятся к одному и тому же типу X. Правда, мы

пока не знаем, что это за тип, но это точно один и тот же тип для обеих пере-

менных. В этом смысле команды a=b и b=t не являются оригинальными.

Но в результате, к какому бы типу ни относились аргументы метода, мы их

значения «поменяли местами».

Как вызывается метод с параметром типа, показано в главном методе про-

граммы. Там создаются две целочисленные переменные, a и b, две текстовые

переменные, A и B, а также два объекта, objA и objB, класса MyClass. Пары этих

переменных по очереди передаются аргументами методу swap(), после чего

проверяется результат «обмена» значениями. Какое значение необходимо

передать методу в качестве параметра типа, мы указываем команде вызова

метода в угловых скобках после имени метода. Например, когда аргумента-

ми метода swap() являются целочисленные значения a и b, команда вызова

метода выглядит как swap(ref a,ref b). Это означает, что при выпол-

нении программного кода метода swap() все будет происходить так, как если

бы мы заменили X на int. Аналогично, команду swap(ref A,ref B)

Передача типа в качестве параметра           259

следует понимать так, что роль X играет тип string, а для команды swap

lass>(ref objA,ref objB) параметр типа X заменяется на значение MyClass.

Результат выполнения программы представлен на рис. 7.7.

Рис. 7.7.  Метод с параметром типа: результат выполнения программы

В принципе, при вызове метода с параметром типа значение параметра

типа можно явно не указывать. В этом случае будет предпринята по-

пытка определить нужное значение для параметра типа по контексту

вызова метода (по типу его аргументов). Например, вместо команд

swap(ref a,ref b), swap(ref A,ref B) и swap(ref o bjA,ref objB) можно было бы использовать, соответственно, команды

swap(ref a,ref b), swap(ref A,ref B) и swap(ref objA,ref objB). Какое значе-

ние (какой тип) подставлять вместо параметра X, в этом случае можно

определить по типу аргументов, которые передаются методу swap().

Как  следствие, программный код  остается корректным. Например, в команде swap(ref a,ref b) аргументы типа int, а при описании их тип

был обозначен как X. Это означает, что X есть int. И так далее.

Метод может содержать несколько параметров типа. В этом случае

идентификаторы типов указываются через запятую в общих угловых

скобках после имени метода.

По тому же принципу создаются обобщенные классы — классы, содержа-

щие параметры типа. Только теперь в описании класса идентификаторы

типа указываются в угловых скобках после имени класса. При создании

объекта класса в угловых скобках указывают идентификаторы типа, кото-

рые следует использовать в качестве значений параметров типа. Такая же

процедура проделывается при объявлении объектных переменных. Ситуа-

цию иллюстрирует программный код в листинге 7.7.

Листинг 7.7.  Обобщенный класс (класс с параметрами типа) using System;

// Обобщенный класс с двумя параметрами типа:

class GClass{

// Открытое поле обобщенного типа X:

public X first;

продолжение

260

Глава 7. Методы и классы во всей красе

Листинг 7.7 (продолжение)

// Открытое поле обобщенного типа Y:

public Y second;

// Конструктор класса с двумя аргументами обобщенных типов: public GClass(X f,Y s){

first=f; // Присваивается значение первому полю

second=s; // Присваивается значение второму полю

}

// Открытый метод для отображения значения полей:

public void show(){

Console.WriteLine("Первое поле {0}, второе поле {1}.",first,second);

}

}

// Класс с главным методом программы:

class GClassDemo{

// Главный метод программы:

public static void Main(){

// Объектная переменная и объект

// обобщенного класса

// со значениями параметров типа int и char:

GClass A=new GClass(100,'A');

// Отображение полей объекта:

A.show();

// Объектная переменная обобщенного класса

// со значениями параметров типа string и string:

GClass B;

// Объект обобщенного класса

// со значениями параметров типа string и string:

B=new GClass("ПЕРВОЕ","ВТОРОЕ");

// Отображение полей объекта:

B.show();

// Ожидание нажатия клавиши Enter:

Console.ReadKey();

}

}

Мы объявляем обобщенный класс с заголовком class GClass. Угло-

вые скобки в заголовке класса говорят о том, что в классе использовано два

параметра типа — X и Y. В самом классе описывается два поля — одно типа

X, а другое типа Y. Также у класса есть конструктор с двумя аргументами.

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

го поля класса, а второй аргумент конструктора имеет тип Y и определяет

значение второго поля класса. Методом show(), который описан в классе, в консольное окно выводится сообщение с информацией о значении полей

объекта класса.

Использование обобщенного типа данных           261

ПРИМЕЧАНИЕ Таким образом, неявно на типы X и Y накладывается небольшое ограни-

чение: они должны быть такими, чтобы переменные/объекты этих типов

можно было передавать аргументами методу Console.WriteLine().

В главном методе программы командой GClass A=new GClass(100,'A') создаются объектная переменная и объект обобщенного

класса GClass со значениями параметров типа int (для параметра X) и char (для параметра Y). Пара значений для параметров типов в угловых скобках

указывается после имени класса GClass как в части объявления объектной

переменной, так и в части создания объекта. Как и в случае с обычными, не

обобщенными классами, процесс объявления объектной переменной и соз-

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

и поступили, объявив командой GClass B объектную пере-

менную B обобщенного класса GClass со значениями параметров типа string (для X) и string (для Y). Создание объекта (с такими же значениями пара-

метров типа) и присваивание его в качестве значения объектной перемен-

ной выполняется командой B=new GClass("ПЕРВОЕ","ВТО -

РОЕ"). Проверка значений полей созданных объектов выполняется вызовом

метода show(). Результат выполнения программы представлен на рис. 7.8.

Рис. 7.8.  Обобщенный класс: результат выполнения программы

Разумеется, мы рассмотрели достаточно простой пример. Вместе с тем даже

он дает неплохое представление о том, насколько эффективным может

быть использование обобщенных классов и обобщенных методов, особенно

в комбинации с другими эффективными приемами программирования.

Использование обобщенного

типа данных

Эх, погубят тебя слишком широкие возможности.

Из к/ф «Айболит 66»

Особенность языка C# такова, что в вершине иерархии классов, как библи-

отечных, так и тех, что создаются пользователем, находится класс object.

262

Глава 7. Методы и классы во всей красе

Причем это относится не только к объектным (или ссылочным) типам дан-

ных, но и к нессылочным типам (таким, например, как int или double).

Напомним,  что  название  класса  object  является  синонимом,  или

псевдонимом, класса System.Object.

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

серьезные последствия, если вспомнить, что переменная базового типа мо-

жет ссылаться на объект производного типа. Более того, в C# есть так назы-

ваемая процедура приведения к объектному типу и извлечения значения из

объектного типа. Эта процедура дает возможность связать данные нессы-

лочного типа со ссылочным типом, то есть «упаковать» обычную перемен-

ную в объект. Приведение к объектному типу автоматически выполняется, когда переменная нессылочного типа (то есть обычная, а не объектная пере-

менная) присваивается переменной класса object. Для обратного преобра-

зования необходимо перед object-значением указать инструкцию явного

приведения типа (в круглых скобках идентификатор конечного типа).

В классе object объявляется виртуальный метод ToString(). Этот метод воз-

вращает в качестве результата текстовое значение и наследуется во всех клас-

сах. Более того, даже для базовых типов этот метод доступен. Его особенность

в том, что метод вызывается автоматически каждый раз, когда объект должен

быть преобразован в текстовое значение. Каждый раз, когда объект оказыва-

ется в месте, где по логике должен был бы быть текст (например, когда объект

передан аргументом методу Console.WriteLine()), автоматически вызывается

метод ToString(), переопределенный в классе объекта или унаследованный

этим классом. Поэтому если мы в классе переопределим метод ToString(), то

в принципе объект можно будет использовать в качестве текста.

Для явного преобразования объекта в текст из объекта можно вы-

звать метод ToString().

Здесь мы рассмотрим небольшой пример того, как могут использоваться

перечисленные выше особенности при написании программных кодов. Ис-

следуем программный код, представленный в листинге 7.8.

Листинг 7.8.  Использование класса object

using System;

// Класс с использованием object-типа:

class OClass{

// Открытое поле класса object:

Использование обобщенного типа данных           263

public object one;

// Открытое поле класса object:

public object two;

// Конструктор класса с двумя аргументами:

public OClass(object one, object two) {

this.one=one; // Значение первого поля

this.two=two; // Значение второго поля

}

// Метод для отображения значения полей объекта:

public void show(){

// Неявно используем переопределенный метод ToString():

Console.WriteLine(this); // Аргументом указан объект вызова

}

// Переопределение метода ToString() для класса OClass:

public override string ToString(){

// Текстовый "эквивалент" объекта:

return "Первый аргумент "+one+". Второй аргумент "+two+".";

}

}

// Класс с главным методом программы:

class ObjectTypeDemo{

// Главный метод программы:

public static void Main(){

// Объектная переменная класса OClass:

OClass obj;

// Создание объекта класса OClass c полями

// целочисленного и символьного типа:

obj=new OClass(10,'A');

// Проверяем "содержимое" объекта:

obj.show();

// Создание объекта класса OClass c двумя

// текстовыми полями:

obj=new OClass("ПЕРВЫЙ","ВТОРОЙ");

// Проверяем "содержимое" объекта:

obj.show();

// Текстовому полю присваиваем

// целочисленное значение:

obj.one=1;

// Проверяем "содержимое" объекта:

obj.show();

// Создаем и инициализируем массив

// объектов класса object.

// Значения элементов - самые разные:

object[] m=new object[]{"Элемент № 1",2,'Ы',new OClass(1.23,100)}; продолжение

264

Глава 7. Методы и классы во всей красе

Листинг 7.8 (продолжение)

// Отображаем элементы массива:

for(int i=0;i

Console.WriteLine(i+1+": "+m[i]);

}

// Ожидание нажатия клавиши:

Console.ReadKey();

}

}

Как ни странно, программный код не только компилируется, но еще и до-

вольно неплохо выполняется. На рис. 7.9 представлен результат выполне-

ния программы.

Рис. 7.9.  Использование класса object: результат выполнения программы

Нам остается понять, почему написанный нами код является корректным.

Все «хитрости» этого кода спрятаны в классе OClass. Структура класса до-

статочно простая. У него есть два поля с названиями one и two. Оба поля

указаны как объекты класса object. Есть у класса конструктор с двумя

аргументами. Код конструктора тривиальный — аргументы конструктора

присваиваются в качестве значений полям объекта.

Более примечательными являются методы show() и ToString(). Мы нач-

нем анализ именно с метода ToString(), поскольку в методе show() просто

пожинаются плоды переопределения метода ToString(). Итак, в заголов-

ке метода мы видим атрибуты public (метод, открытый по определению), override (имеет место переопределение метода) и string (метод в качестве

результата возвращает текстовое значение). Аргументов у метода нет. Это

фактически стандартная шапка метода при его переопределении. Здесь мы

делаем только то, что должны делать. От нас в первую очередь зависит, что

будет внутри метода. В рассматриваемом примере там всего одна команда

"Первый аргумент "+one+". Второй аргумент "+two+".", которой в качестве

результата возвращается текстовая строка, в которую «вмонтированы»

ссылки на поля объекта (в некотором смысле их можно рассматривать как

значения полей — но это только для нессылочных типов). Именно такая

строка будет использоваться каждый раз, когда объект класса OClass ока-

жется в «текстовом» месте.

Обработка исключительных ситуаций           265

Первая проверка на надежность метода выполняется в методе show(), в теле

которого мы поместили всего одну команду — Console.WriteLine(this). Здесь

аргументом метода Console.WriteLine() указана ссылка на объект вызова — то

есть на объект класса OClass. Поэтому эффект такой, как если бы аргументом

методу Console.WriteLine() передавался результат вызова метода ToString().

С кодом класса OClass мы ситуацию разъяснили. Теперь посмотрим, что

происходит в главном методе программы.

Командой OClass obj мы объявляем объектную переменную obj класса

OClass, и в этом нет пока ничего необычного. Небольшая экзотика начи-

нается, когда мы встречаем команду obj=new OClass(10,'A'). Особенность

команды — в аргументах конструктора. Они не только разного типа (целое

число и символ), но еще и формально не относятся к классу object. Но

это только формально. Поскольку класс object является базовым для всех

классов и нессылочных типов, то аргументы конструктора неявно преоб-

разуются в тип object. Аналогичная ситуация имеет место при выполне-

нии команды obj=new OClass("ПЕРВЫЙ","ВТОРОЙ"), здесь принцип тот же, но

только аргументы конструктора — оба текстовые. Более того, корректной

является и obj.one=1. Здесь полю, которое до этого имело фактически тек-

стовое значение, в качестве нового значение присваивается целое число.

Каждый раз результат манипуляций с объектами мы проверяем с помощью

метода show(), который вызывается из объекта obj командой obj.show().

Но на этом наше исследование могущества класса object не закан-

чивается. Командой object[] m=new object[]{"Элемент № 1",2,'Ы', new OClass(1.23,100)} мы создаем массив объектов класса object, причем

инициализация массива выполняется значениями самых разных типов: текстовым значением, числом, символом и объектом класса OClass (у ко-

торого два «числовых» поля: действительное и целое число). С помощью

оператора цикла значения элементов массива выводятся в консоль. При

этом, когда очередь доходит до отображения «значения» последнего эле-

мента массива-объекта класса OClass, в игру вновь вступает переопреде-

ленный метод ToString() этого класса.

Обработка исключительных ситуаций

— Простите, часовню тоже я развалил?

— Нет, это было до вас, в XIV веке.

Из к/ф «Кавказская пленница»

С обработкой исключительных ситуаций мы уже встречались. Здесь под-

ведем под этот процесс некоторую теоретическую основу. Но сначала

266

Глава 7. Методы и классы во всей красе

немного освежим память. Для нас важными будут следующие обстоятель-

ства.


 Если при выполнении программного кода происходит ошибка, авто-

матически создается объект специального класса, который содержит

описание ошибки и имеет ряд специфических свойств.


 Этот объект «вбрасывается» в программу, которая вызвала ошибку. Если

объект ошибки не обрабатывается, программа экстренно (в «аварийном»

режиме) завершает работу.


 Чтобы программа при возникновении ошибки (исключения или исклю-

чительной ситуации) работу не завершала, исключительная ситуация

должна быть «обработана».


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

блок, а код, который выполняется при обработке ошибки, помещается

в catch-блок.

ПРИМЕЧАНИЕ Собственно, мы уже видели (в минимальном объеме, правда), как

работает try-catch конструкция. Теперь настало время поближе по-

знакомиться с объектами ошибок, или исключениями.

Что касается объекта, который создается при возникновении ошибки, то уже в силу того обстоятельства, что это объект, он должен относиться

к какому-то классу. Для всех основных ошибок, которые в принципе могут

возникнуть, предусмотрены специальные классы. В известном смысле эти

классы описывают всевозможные типы ошибок. При возникновении опре-

деленной ошибки на основе класса, который соответствует этой ошибке, создается объект. Классы ошибок не разрозненные. У них строгая иерар-

хия, в вершине которой находится класс Exception, который описан в про-

странстве System. У класса Exception имеются подклассы SystemException и ApplicationException. Классы для основных «стандартных» ошибок (или ис-

ключений) относятся к ветке иерархии наследования класса SystemException.

Чтобы понять, как эффективно использовать классы исключений, разберем-

ся с тем, каким образом реализуется обработка исключительных ситуаций

через систему try­catch блоков. Достаточно общий шаблон использования

соответствующей «пожарной» конструкции выглядит примерно так:

// Начальный try-блок:

try{

// Контролируемый программный код

}

// Первый catch-блок:

catch(Класс_исключения_1 объект_1){

Обработка исключительных ситуаций           267

// Программный код на случай возникновения

// ошибки типа Класс_исключения_1

}

// Второй catch-блок:

catch(Класс_исключения_2 объект_2){

// Программный код на случай возникновения

// ошибки типа Класс_исключения_2

}

...

// N-й catch-блок:

catch(Класс_исключения_N объект_N){

// Программный код на случай возникновения

// ошибки типа Класс_исключения_N

}

// Следующая команда

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

ошибки, заключается в try-блок: код помещается в фигурных скобках по-

сле ключевого слова try. После try-блока следует несколько catch-блоков, обычно тоже с программным кодом. Количество блоков не регламенти-

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

в catch-блоках «вступает в игру» только в том случае, если при выполнении

программного кода в try-блоке возникла ошибка. Если ошибка не возник-

ла, что именно содержится в catch-блоках — непринципиально, поскольку

этот код не выполняется, а управление передается той команде, которая на-

ходится после всей try­catch конструкции. Все намного интереснее, если

ошибка возникла. В этом случае, как мы знаем, в зависимости от типа воз-

никшей ошибки создается объект, а дальше начинается последовательный

перебор catch-блоков. Обычно catch-блоки имеют нечто наподобие аргу-

мента — в круглых скобках после ключевого слова catch указывается имя

класса ошибки и, по желанию, объектная переменная, которая играет роль

аргумента. Эти catch-блоки один за другим проверяются на предмет того, совпадает ли класс объекта ошибки с тем классом, что указан в круглых

скобках после ключевого слова catch. Если совпадения нет, то проверяет-

ся следующий блок, и т. д. Как только совпадение найдено, начинает вы-

полняться программный код соответствующего catch-блока. При этом если

кроме типа ошибки в скобках после ключевого слова catch указана и объ-

ектная переменная, то этой объектной переменной в качестве значения при-

сваивается ссылка на объект ошибки. Но нередко обработка ошибки вы-

полняется без непосредственного обращения к объекту ошибки.

В случае, когда при переборе catch-блоков совпадение не найдено, ошиб-

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

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

268

Глава 7. Методы и классы во всей красе

концов, исключение не будет перехвачено и обработано, программа завер-

шит работу в аварийном режиме.

Если в catch-блоке не указать тип исключения, такой catch-блок будет

перехватывать все ошибки. Обычно такой блок добавляют в конце

конструкции try-catch. В случае если нужно, чтобы при завершении

try-блока какой-то код выполнялся при любых раскладах, в конструк-

цию try-catch можно добавить блок finally.

Еще одно важное замечание касается способа поиска совпадений

типов ошибок при переборе catch-блоков. Важно знать, что если тип

(класс) ошибки является производным классом от класса ошибки, указанного  в  catch-блоке,  то  считается,  что  имеет  место  совпаде-

ние. Поэтому, например, если в качестве класса исключения указать

Exception, то перехватываться будет практически все.

Настал момент рассмотреть небольшой пример. Обратимся к программно-

му коду, представленному в листинге 7.9.

Листинг 7.9.  Обработка исключительных ситуаций

using System;

// Класс с главным методом программы:

class ECatchDemo{

// Главный метод с обработкой

// исключительных ситуаций:

public static void Main(){

// Объект rnd класса Random для

// генерирования случайных чисел:

Random rnd=new Random();

// Целочисленный массив из трех элементов:

int[] n=new int[3];

// Целочисленные переменные:

int i,k,a;

// Оператор цикла:

for(i=1;i<=20;i++){

k=rnd.Next(0,4); // Случайное целое число

// от 0 до 3 включительно

a=rnd.Next(0,4); // Случайное целое число

// от 0 до 3 включительно

// Блок контроля программного кода:

try{

// Возможна ошибка: деление на нуль

// или выход за пределы массива:

n[k]=6/a; // Элементу массива

Обработка исключительных ситуаций           269

// присваивается значение

// Команда выполняется, если выше

// не произошла ошибка:

Console.WriteLine("Индекс {0}. Значение {1}.",k,n[k]);

}

// Перехват ошибки выхода за пределы массива:

catch(IndexOutOfRangeException){

Console.WriteLine("Выход за пределы массива.");

}

// Перехват ошибки деления на нуль:

catch(DivideByZeroException){

Console.WriteLine("Деление на нуль.");

}

}

// Ожидание нажатия клавиши:

Console.ReadKey();

}

}

Программа простая и бесполезная. В главном методе программы создает-

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

ратор из 20 циклов, и за каждый цикл выполняются некоторые нехитрые

действия: генерируются два целых случайных числа в диапазоне от 0 до 3

включительно.

Для генерирования случайных чисел мы создаем объект rnd библио-

течного класса Random. В классе прописан метод Next(), который

позволяет генерировать случайные целые числа. Результатом выра-

жения вида rnd.Next(m,M+1) является случайное число в диапазоне

от m до M.

Одно случайное число используется в качестве индекса элемента массива, а второе фигурирует в знаменателе в операции присваивания значения эле-

менту массива. Помимо штатных ситуаций, когда элементу с легитимным

индексом присваивается значение 6, 3 или 2, возможны две нештатные си-

туации: деление на нуль и выход индекса за пределы массива. Поэтому фраг-

мент кода, который может сгенерировать нам неприятность (а это команда

присваивания значения элементу массива с примкнувшей к ней командой

вывода результата на экран), помещается в try-блок. На случай возникно-

вения ошибок после try-блока есть два catch-блока. Ошибке деления на

ноль соответствует класс DivideByZeroException. Ошибке выхода индекса

за пределы массива соответствует класс IndexOutOfBoundsException. Со-

ответствующие классы указываются в круглых скобках после ключевого

270

Глава 7. Методы и классы во всей красе

слова catch. Поскольку сам объект ошибки нам в данном случае не нужен, объектные переменные для этих классов мы не указываем.

В случае если возникает ошибка, выполнение команд try-блока прекра-

щается и выполняется код одного из catch-блоков. Затем начинает вы-

полняться следующий цикл внешнего в try­catch конструкции оператора

цикла. Результат выполнения программы показан на рис. 7.10.

Рис. 7.10.  Возможный результат выполнения программы с перехватом

исключений деления на ноль и выхода за пределы массива

Следует иметь в виду, что поскольку здесь мы используем случайные чис-

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

этому от запуска к запуску картинка будет меняться.

Хотя это может показаться странным, но можно генерировать исключе-

ния вручную. Другими словами, можно так написать код, что как бы будет

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

используют инструкцию throw, после которой указывается объект генери-

руемой ошибки. Поскольку словами это объяснять все равно бесполезно, изучим программный код в листинге 7.10. Это несколько модифицирован-

ный программный код из предыдущего примера.

Листинг 7.10.  Искусственное генерирование ошибки

using System;

class ThrowDemo{

// Главный метод с обработкой

// исключительных ситуаций:

public static void Main(){

// Объект rnd класса Random для

// генерирования случайных чисел:

Обработка исключительных ситуаций           271

Random rnd=new Random();

// Целочисленный массив из трех элементов:

int[] n=new int[3];

// Целочисленные переменные:

int i,k,a;

// Оператор цикла:

for(i=1;i<=20;i++){

k=rnd.Next(0,4); // Случайное целое число

// от 0 до 3 включительно

a=rnd.Next(0,4); // Случайное целое число

// от 0 до 3 включительно

// Блок контроля программного кода:

try{

// Генерирование искусственной ошибки:

if(a==0&&k>n.Length­1) throw new Exception();

// Возможна ошибка: деление на нуль

// или выход за пределы массива:

n[k]=6/a; // Элементу массива

// присваивается значение

// Команда выполняется, если выше

// не произошла ошибка:

Console.WriteLine("Индекс {0}. Значение {1}.",k,n[k]);

}

// Перехват ошибки выхода за пределы массива:

catch(IndexOutOfRangeException){

Console.WriteLine("Выход за пределы массива.");

}

// Перехват ошибки деления на нуль:

catch(DivideByZeroException){

Console.WriteLine("Деление на нуль.");

}

// Перехват "двойной" ошибки:

catch(Exception){

Console.WriteLine("Двойная ошибка!");

}

}

// Ожидание нажатия клавиши:

Console.ReadKey();

}

}

По сравнению с предыдущим примером (см. листинг 7.9) изменения мини-

мальные. А именно, в начале try-блока добавлена команда if(a==0&&k>n.

Length­1) throw new Exception() для генерирования искусственной ошиб-

ки, и появился еще один, третий, catch-блок с аргументом-классом Exception.

Что это дает? Если ситуация такова, что случайное число, обозначающее ин-

декс элемента массива, выходит за допустимые пределы, а случайное число,

272

Глава 7. Методы и классы во всей красе

отправляемое в знаменатель дробного выражения равно нулю, получим как

бы «двойную» ошибку. Эту «двойную» ошибку мы хотим обрабатывать по

особым правилам. Поэтому, если выполнено условие a==0&&k>n.Length­1, ко-

мандой throw new Exception() генерируется исключение класса Exception.

ПРИМЕЧАНИЕ После инструкции throw мы указали анонимный объект new Exception() класса Exception.

Для обработки этой исключительной ситуации в третьем catch-блоке есть

команда Console.WriteLine("Двойная ошибка!"). Причем важно, чтобы блок

для обработки исключения класса Exception был последним. Если его, на-

пример, гипотетически поставить первым, то он перехватывал бы и две

другие ошибки — деление на нуль и выход за пределы массива. Это на-

столько трагичная ситуация, что ее даже компилятор не допустит. Резуль-

тат (возможный) выполнения программы представлен на рис. 7.11.

Рис. 7.11.  Генерирование искусственной ошибки: возможный результат выполнения

программы

ПРИМЕЧАНИЕ Надо понимать, что сообщение Двойная ошибка! — редкий гость

в консольном окне. Если случайные числа генерируются с равной

вероятностью, то вероятность для каждого из событий «деление на

нуль» и «выход за пределы массива» составляет 1/4. Вероятность

того, что произойдет хоть одно из этих событий, равна 7/16. А вероят-

ность того, что произойдут оба события, равняется 1/16. Математиче-

ское ожидание (оценка для среднего количества появления двойной

ошибки) для 20 запусков цикла составляет 20/16=1,25, то есть чуть

больше единицы.

Многопоточное программирование           273

Многопоточное программирование

Куда? Эй, куда же вы все-то разбежались?

Кто-нибудь, держите меня!

Из к/ф «Айболит 66»

Еще одна полезная возможность, с которой мы познакомимся (достаточно

кратко) — это возможность создавать в программе потоки. Потоками назы-

ваются отдельные части программы, которые выполняются одновременно.

Такое программирование называется многопоточным программировани-

ем. В C# многопоточность встроенная. Это означает, что язык обладает на-

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

потоков исключительно программными средствами C#.

Элементарная логика подсказывает, что если программа выполняется, то

по крайней мере один поток имеется. Этот поток обычно называют глав-

ным. Из главного потока можно запускать другие потоки, которые тоже

могут запускать потоки, и т. д. При этом важно не потерять логику выпол-

нения программы.

Понятно, что тема эта перспективная и очень обширная. Мы, в силу объек-

тивных причин, освоим лишь азы. Другими словами, наша задача состоит

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

Для этого нам понадобятся классы (точнее, мы будем использовать лишь

один класс), которые позволяют создавать многопоточные программы.

Классы описаны в пространстве имен System.Threading. Поэтому програм-

мы, в которых реализуется многопоточное программирование, должны

содержать инструкцию using System.Threading. Тот класс, который инте-

ресует нас, называется Thread. Он важен тем, что создание потока как та-

кового означает создание объекта класса Thread. Поэтому нам важно знать

побольше об этом классе.

Класс Thread не может быть базовым. Он объявлен с ключевым сло-

вом sealed, а это означает, что на основе класса нельзя создавать

производные классы.

После того как объект класса Thread создан (объект потока), нужно запу-

стить поток. Запуск потока выполняется вызовом метода Start() из объ-

екта потока. В результате поток начинает выполняться. Но поток — это, по большому счету, последовательность команд. Откуда им взяться? Из

метода, который запускается вследствие вызова метода Start(). Пытли-

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

274

Глава 7. Методы и классы во всей красе

метод при вызове метода Start()? Это правильный вопрос. Правильный

ответ такой: при создании объекта потока, то есть объекта класса Thread, аргументом конструктору этого класса передается экземпляр делегата того

метода, который запускается при вызове метода Start(). Делегат называ-

ется ThreadStart, а его экземпляры могут ссылаться на открытые методы, которые не имеют аргументов и не возвращают результат. Вся эта «кухня»

может быть реализована совершенно разными способами. Но есть некие

ключевые моменты:

1. В наличии должен быть метод, открытый, без аргументов и не возвра-

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

ется с выполнением потока. Те команды, которые есть в методе, и будут

выполняться в рамках потока.

2. Должен быть создан экземпляр делегата ThreadStart, ссылающийся на

указанный выше метод.

3. На основе экземпляра делегата ThreadStart создается объект класса

Thread. Экземпляр делегата передается конструктору класса Tread в ка-

честве аргумента.

4. Из объекта класса Thread следует запустить метод Start().

ПРИМЕЧАНИЕ Программа, которую мы рассматриваем, имеет прямое отношение

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

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

цию (42 195 метров) двух пушистых спортсменов: Зайца и Лисы. Оба

спортсмена одновременно начинают забег и двигаются по дистанции

со  средней  скоростью  30  км/ч,  что  составляет  500  м/мин.  Такая

скорость более-менее отвечает реальным возможностям пушистой

братии. Для сравнения — заяц-русак, по некоторым данным, может

развивать скорость до 60 км/ч.

Программа, как отмечалось, предназначена для имитации такого за-

бега. Для этого в программе создается и запускается два потока (не

считая главного). Каждый из потоков имитирует движение спортсмена.

Имитация выполняется так. Для каждого потока выделена специаль-

ная целочисленная переменная с начальным нулевым значением. Эта

переменная  определяет  расстояние,  которое  пробежал  спортсмен.

В каждом потоке через определенные промежутки времени значение

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

спортсменов, кто быстрее придет к финишу — то есть чья «переменная

пройденного пути» первая достигнет марафонского значения 42 195.

Оба вспомогательных потока запускаются из главного потока. Через

одинаковые промежутки времени главный поток «считывает» текущее

значение переменных, которые определяют пройденный спортсменами

путь. Соответствующая информация отображается в консольном окне.

Многопоточное программирование           275

Рассмотрим программный код в листинге 7.11, в котором вся эта схема

и реализована.

Листинг 7.11.  Программа с несколькими потоками

using System;

using System.Threading;

// Все происходит в одном классе:

class Marathon{

// Марафонское расстояние:

const int Dist=42195;

// "Путь" Зайца:

private static int HareDist;

// "Путь" Лисы:

private static int FoxDist;

// Метод для потока "забег Лисы":

public static void goFox(){

FoxDist=0; // Начальное значение "пути" Лисы

Random rnd=new Random(); // Будем генерировать

// случайные числа

// Лиса ушла в забег:

Console.WriteLine("Лиса стартовала!");

do{

Thread.Sleep(20); // Небольшая задержка

// Рывок после отдыха:

FoxDist+=rnd.Next(200)+1;

}while(FoxDist

// Да, это он:

Console.WriteLine("Лиса финишировала!");

}

// Метод для потока "забег Зайца":

public static void goHare(){

HareDist=0; // Заяц на старте

Random rnd=new Random(); // Класс random - двигатель прогресса

// Заяц ушел в отрыв:

Console.WriteLine("Заяц стартовал!");

do{

Thread.Sleep(10); // Небольшой отдых

HareDist+=rnd.Next(100)+1; // Небольшой рывок

}while(HareDist

// Вот он:

Console.WriteLine("Заяц финишировал!");

}

продолжение

276

Глава 7. Методы и классы во всей красе

Листинг 7.11 (продолжение)

// Главный метод программы (главный поток):

public static void Main(){

// Готовим секундомер:

int count=0;

// "Служба информации":

string txt="-я минута: Заяц пробежал {0} метров,


Лиса - {1} метров.";

// Объектные переменные для потоков:

Thread Hare,Fox; // Каждому спортсмену - по дорожке!

// Экземпляры делегатов для передачи в потоки:

ThreadStart hare,fox;

// Экземплярам делегатов присваиваются

// значения:

hare=goHare; // Для потока "забег Зайца"

fox=goFox; // Для потока "забег Лисы"

// Создание объекта для потока Зайца:

Hare=new Thread(hare);

// Создание объекта для потока Лисы:

Fox=new Thread(fox);

// На старт, внимание, марш!

Console.WriteLine("Мы начинаем марафон!");

Hare.Start(); // Первый пошел!

Fox.Start(); // Второй пошел!

do{

count+=5; // Интервал в "минутах"

Thread.Sleep(500); // Даем время разогнаться

// Снимаем звериные "показания":

Console.WriteLine(count+txt,HareDist,FoxDist);

}while(Hare.IsAlive||Fox.IsAlive); // Пока хоть кто-то бежит

// Главный олимпийский принцип:

Console.WriteLine("Главное не победа, а участие!");

// Наслаждаемся результатом:

Console.ReadKey();

}

}

Весь процесс реализован в одном классе Marathon. В нем мы определяем

поле-константу Dist со значением 42195 (марафонская дистанция в ме-

трах), а также два статических целочисленных поля HareDist (расстояние, преодоленное Зайцем) и FoxDist (расстояние, преодоленное Лисой). Ме-

тод goFox() не возвращает результат и не имеет аргументов. Этот метод

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

ды в теле метода — это команды, которые выполняются при выполнении

потока. В теле метода переменной FoxDist присваивается начальное ну-

левое значение и создается объект rnd класса Random (для генерирования

Многопоточное программирование           277

случайных чисел). Затем командой Console.WriteLine("Лиса стартовала!") в консоль выводится сообщение о том, что спортсмен вступил в борьбу.

Но все самое интересное происходит в операторе do­while(). Командой

Thread.Sleep(20) выполняется задержка в 20 миллисекунд. После такой

вынужденной задержки командой FoxDist+=rnd.Next(200)+1 значение пе-

ременной FoxDist увеличивается на случайное число от 1 до 200. Оператор

цикла выполняется, пока переменная FoxDist меньше марафонской кон-

станты Dist (условие FoxDist

ния оператора цикла, командой Console.WriteLine("Лиса финишировала!") в консоль выводится сообщение с оптимистичным содержанием.

В классе Thread описан статический метод Sleep(). Если вызывать

этот метод с целочисленным аргументом, то выполнение потока, из

которого вызывается метод, будет приостановлено на время (в мил-

лисекундах), указанное аргументом метода. К помощи метода Thread.

Sleep() мы будем прибегать неоднократно.

Метод goHare(), который выполняется для второго потока («поток Зай-

ца»), от метода goFox() принципиально отличается лишь тем, что задержка

по времени там в 2 раза меньше (10 миллисекунд) и в 2 раза меньше диа-

пазон генерирования случайных чисел (от 1 до 100). Таким образом, наш

Заяц прыгает чаще, но на меньшие расстояния.

Но все это были предварительные размышления о том, какой поток как

выполняется. Эти самые потоки где-то надо создать и как-то надо запу-

стить. Подходящий в этом смысле метод — главный метод программы.

Локальная целочисленная переменная count, инициализированная с на-

чальным нулевым значением, послужит «секундомером» — мы с ее по-

мощью будем отмечать моменты времени, в которые производятся за-

меры преодоленных спортсменами расстояний. Вспомогательным целям

служит и текстовая строка txt — она содержит текст, на основе которого

будет формироваться выводимое в консоль сообщение о результатах кон-

троля.

Командой Thread Hare,Fox мы объявляем две объектные переменные, Hare и Fox, класса Thread. Попозже в эти переменные мы запишем ссыл-

ки на соответствующие объекты потоков. Но предварительно эти объек-

ты следует создать. Для создания объектов, в свою очередь, необходимо

объявить экземпляры делегата ThreadStart. Для этой цели служит коман-

да ThreadStart hare,fox. Командами hare=goHare и fox=goFox экземплярам

делегатов присваиваем в качестве значений ссылки на соответствующие

методы. Теперь можно создавать объекты потоков, что мы и делаем с помо-

щью команд Hare=new Thread(hare) и Fox=new Thread(fox). Осталось только

278

Глава 7. Методы и классы во всей красе

запустить потоки. Предваряя это, мы командой Console.Write Line("Мы на­

чинаем марафон!") выводим в консоль сообщение угрожающего свойства, и командами Hare.Start() и Fox.Start() последовательно запускаем два

потока.

Здесь  есть  важный  идеологический  момент.  После  того  как  поток

запущен (например, командой Hare.Start()), он начинает жить своей

почти независимой жизнью. А метод Main() продолжает выполняться

своим чередом.

В методе Main() тем временем запускается оператор цикла, в котором за

каждый цикл переменная-счетчик count увеличивает с дискретностью 5.

Командой Thread.Sleep(500) выполняется задержка главного потока (того

потока, в котором метод Main() выполняется) на 500 миллисекунд, после

чего командой Console.WriteLine(count+txt,HareDist,FoxDist) отобража-

ется сообщение с информацией о том, какая зверушка сколько успела про-

бежать. Соответствующие значения считываются из переменных HareDist и FoxDist. В качестве условия продолжения оператора цикла указана кон-

струкция Hare.IsAlive||Fox.IsAlive. В ней из объектов потока запрашива-

ется свойство IsAlive. Свойство возвращает логическое значение true, если

соответствующий поток выполняется. Если поток уже завершен, возвра-

щается значение false. Поэтому значением выражения Hare.IsAlive||Fox.

IsAlive является true, если хотя бы один из потоков выполняется. Таким

образом, оператор цикла в главном завершается только после завершения

выполнения потоков для объектов Hare и Fox. В конце выполнения про-

граммы командой Console.WriteLine("Главное не победа, а участие!") на

экран выводится главный олимпийский принцип. Вот, собственно, и все.

На рис. 7.12 показан возможный результат выполнения программы.

В данном случае победила Лиса, хотя она и стартовала второй.

ПРИМЕЧАНИЕ Несложно заметить, что сообщение о положении спортсменов на по-

следней секунде появляется после сообщения о приходе к финишу.

Причина в следующем. Сообщение о приходе к финишу отображается

из потока, который завершается немного раньше завершения опера-

тора цикла в главном методе программы. Последний цикл начинает

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

ственной временной задержки вывод информации происходит после

вывода сообщений о завершении потоков. Вообще, синхронизация

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

статочно понять ее значимость.

Многопоточное программирование           279

Рис. 7.12.  Возможные результаты «зверского марафона»

Мы рассмотрели очень простой пример, связанный с использованием по-

токов. Это действительно очень мощное и гибкое средство программиро-

вания. Но, увы, полностью осветить эту тему здесь мы все равно не смо-

жем. Кроме того, следует иметь в виду, что предложенный выше способ

организации потоков хотя и правильный, но не очень «классический». Это

и не плохо, и не хорошо — просто по-другому. Тем не менее, если читате-

лю удалось уловить основную суть, или идею, многопоточности, то можно

считать, что свою задачу пример выполнил.

Приложение

с графическим

интерфейсом:

учебный проект

Форму будете создавать под моим

личным контролем. Форме сегодня

придается большое ... содержание.

Из к/ф «Чародеи»

Эта глава всецело посвящена одному-единственному примеру о том, как

создавать приложения с графическим интерфейсом, в котором не только

одни кнопки с текстовыми метками, но и некоторые другие графические

элементы. Справедливости ради следует отметить, что создание графиче-

ского интерфейса представляется делом малоперспективным в том смыс-

ле, что процесс, по своей сути, достаточно шаблонный и с точки зрения

программного искусства малотворческий. С другой стороны, язык про-

граммирования C# как раз и хорош тем, что с его помощью достаточно

легко создаются приложения с графическим интерфейсом. Поэтому не на-

писать в книге по C# о том, как создать форму с кнопочками, пиктограмм-

ками, переключателями и другими деликатесами — все равно что объявить

войну, а военных об этом не предупредить. Но это еще не все. В этой главе

мы несколько изменим базовый подход и, в некотором смысле, предоста-

вим читателя самому себе. Читатель сможет найти, конечно же, полный

программный код (с комментариями в коде), описание идеи, положенной

Многопоточное программирование           281

в основу программы, а также демонстрацию (в разумных пределах) функ-

циональных возможностей программы. Также в главе описаны наиболее

трудно воспринимаемые моменты и на общем уровне базовые алгоритмы.

Есть и краткая справка по способам работы с графическими элементами.

Тем не менее материал главы предполагает, что читатель затратит серьез-

ное время на «самоподготовку» и детальный разбор программного кода.

ПРИМЕЧАНИЕ Важно помнить, что пример все-таки учебный. Поэтому во многих

случаях, выбирая между эффективностью и наглядностью программ-

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

однотипные действия выполнялись (реализовывались в командах) по-разному.  Причина  банальна  —  желание  проиллюстрировать

спектр  возможностей  и  гибкость  языка  C#.  Опять  же,  в  примере

упор делается на вопрос «как сделать?», а не на вопрос «зачем это

делать?». Поэтому глубокого философского смысла в предназначении

описываемой далее программы искать не стоит.

Что касается самого примера, то мы пытаемся создать программу, в ре-

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

окне есть область с постоянным текстовым значением (реализуется через

текстовую метку). Для отображения текстового содержимого можно при-

менять различные шрифты. Настройка параметров шрифта выполняется

непосредственно в окне формы. Можно выбрать тип шрифта (Arial, Times и Courier), стиль шрифта (Жирный и Курсив) и размер шрифта (в диапазоне от

10 до 20). Утилита выбора типа шрифта реализуется через группу из трех

переключателей (радиокнопок). Выбор стиля шрифта выполняется с по-

мощью опций. Размер шрифта вводится с клавиатуры в специальном поле.

На форме имеется две кнопки: одна — кнопка применения настроек, дру-

гая кнопка позволяет завершить работу приложения.

При выполнении настроек они автоматически в силу не вступают.

Для их применения необходимо щелкнуть на специальной кнопке.

Исключение составляют переключатели выбора типа шрифта — из-

менение  положения  переключателя  приводит  к  автоматическому

применению настроек.

Также у формы есть главное меню, которое дублирует выполнение всех

перечисленных операций.

Перед тем, как приступить к анализу программного кода и всего, что с ним

связано, сделаем краткий экскурс в мир графических элементов оконных

форм.

282

Глава 8. Приложение с графическим интерфейсом: учебный проект

Общие сведения о графических

элементах

Отлично, отлично!

Простенько, и со вкусом!

Из к/ф «Бриллиантовая рука»

Мы рассмотрим и обсудим только те элементы и классы, которые имеют

непосредственное отношение к нашей задаче. С кнопками и метками мы

уже знакомы. Кнопкам соответствует класс Button, а для реализации меток

используют класс Label. Кроме этого, нам понадобятся кнопки-опции (эле-

менты с полем для того, чтобы устанавливать/убирать галочку). Опции

реализуются через класс CheckBox. Для ввода размера шрифта нам понадо-

бится текстовое поле — объект класса TextBox. Кнопки-переключатели (или

радиокнопки) реализуются в виде объектов класса RadioButton. Но здесь

есть один тонкий момент. Дело в том, что такие кнопки-переключатели ис-

пользуют для организации групп переключателей. В каждой группе только

один и только один переключатель может быть выделен (или установлен).

Поэтому радиокнопки мало добавить в форму — их еще нужно сгруппиро-

вать. Для группы кнопок создается объект класса GroupBox.

Главное меню формы — это меню, которое находится в верхней части фор-

мы под строкой названия. А еще главное меню формы — это объект клас-

са MainMenu. Отдельные пункты меню, которые входят в состав главного

меню, являются объектами класса MenuItem. Команды или подменю, из

которых состоят отдельные пункты главного меню, также являются объ-

ектами класса MenuItem. Меню создается путем добавления подпунктов

к пунктам меню. У объектов класса MenuItem имеется два полезных в на-

шем деле свойства. Свойство Text предсказуемым образом возвращает тек-

стовое название пункта меню. Свойство Index возвращает индекс пункта

меню в коллекции пунктов (то есть порядковый индекс, начиная с нуля, команды или подменю в пункте меню).

Мы достаточно часто будем использовать свойство Text для самых

разных объектов. Понятно, что многое зависит от объекта, но в прин-

ципе это свойство определяет текст, который отображается в области

соответствующего элемента.

Для «вкладывания» подпункта меню/команды в пункт меню из коллекции

MenuItems объекта «внешнего» пункта меню (контейнера) вызывается ме-

тод Add(), аргументом которого указывается объект добавляемого пункта

Общие сведения о графических элементах           283

меню или команды. Чтобы связать главное меню с формой, необходимо

свойству Menu формы в качестве значения присвоить ссылку на объект

главного меню.

Достаточно полезный метод SetBounds() позволяет задать положение и раз-

меры элемента. Этот метод имеется для большинства классов элементов, которые мы будем использовать. Первые два аргумента метода опреде-

ляют координаты левого верхнего угла элемента по отношению к своему

контейнеру (элементу, который содержит другие элементы). Два других

аргумента метода — это линейные размеры элемента (ширина и высота).

Полезнейшее свойство элементов — свойство Font. В качестве значения

свойству присваивается объект одноименного класса, который и определя-

ет шрифт, применяемый для отображения текстовых надписей в области

элемента. В программе это свойство задается для всей формы и для метки

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

если мы задаем шрифт формы, то мы автоматически задаем его для всех

элементов. Для тех элементов, которые должны иметь «особый» шрифт, объект с параметрами шрифта присваивается в качестве свойства Font со-

ответствующего графического элемента. Для создания объекта класса Font мы будем использовать конструктор с тремя аргументами. Первым аргу-

ментом указывается текстовое название шрифта. Второй аргумент — это

размер шрифта. Третий аргумент — константа перечисления FontStyle.

В частности, нас будут интересовать значения FontStyle.Regular (обыч-

ный шрифт), FontStyle.Bold (жирный шрифт) и FontStyle.Italic (кур-

сив). Особенность значений перечисления FontStyle такова, что если по-

надобится применять сразу несколько стилей (например, жирный курсив), то стили объединяются с помощью оператора побитового или |. Напри-

мер, чтобы получить жирный курсив, используем инструкцию FontStyle.

Bold|FontStyle.Italic. С другой стороны, добавление жирного стиля или

курсива к обычному шрифту означает применение, соответственно, жир-

ного стиля или курсива. На этой особенности базируются некоторые не-

сложные вычисления при обработке настроек в окне формы.

В качестве текстовых названий шрифтов мы используем названия

«Arial»,  «Times»  и  «Courier»,  а  в  результате,  скорее  всего,  будут

применяться шрифты Arial, Times New Roman и Courier New соот-

ветственно. Вообще же в таких вопросах лучше отталкиваться от

системных параметров — в данном случае списка установленных

шрифтов.

Обработка событий базируется на присваивании значений событиям Click таких элементов, как кнопки и пункты меню. Событие происходит, когда

284

Глава 8. Приложение с графическим интерфейсом: учебный проект

пользователь щелкает на соответствующем элементе. Для радиокнопок мы

используем событие CheckedChanged, которое происходит при изменении

состояния переключателя. Для опций полезным событием-членом будет

Checked, которое позволяет определить, установлена опция или нет. Что

касается обработчиков событий, то, напомним, это должны быть мето-

ды, не возвращающие результат, с двумя аргументами: объектом класса

Object, который определяет вызвавший событие объект, и объектом клас-

Загрузка...