Академический Документы
Профессиональный Документы
Культура Документы
1я7
УДК 004.43(075)
В19
Васильев А.
В19 C#. Объектно-ориентированное программирование: Учебный курс. — СПб.: Пи-
тер, 2012. — 320 с.: ил.
ISBN 978-5-459-01238-5
Книга представляет собой учебный курс по объектно-ориентированному программированию на
языке C#. Описаны синтаксические конструкции, операторы управления и объектная модель, ис-
пользуемые в C#. В издание включены основные темы для изучения данного языка программиро-
вания, а именно: базовые типы данных и операторы, управляющие инструкции, массивы, классы
и объекты, наследование, индексаторы, свойства, делегаты, обработка исключительных ситуаций,
многопоточное программирование, перегрузка операторов, разработка Windows-приложений
и многое другое. Большое внимание уделяется созданию программ с графическим интерфейсом.
ББК 32.973.2-018.1я7
УДК 004.43(075)
Все права защищены. Никакая часть данной книги не может быть воспроизведена в какой бы то ни было фор-
ме без письменного разрешения владельцев авторских прав.
который создал себе имя как идейный архитектор таких чудесных проек-
тов, как Turbo Pascal и Delphi. Идеологически и синтаксически язык C#
близок к С++ и Java. Во всяком случае, если читатель знаком хотя бы с од-
ним из этих языков, он найдет для себя много знакомых пассажей. Все это
не случайно, поскольку язык C# является логическим продолжением язы-
ка C++ (во всяком случае, по мнению разработчиков языка) и в некотором
смысле конкурентом языка Java. Но обо всем по порядку.
Microsoft, на самом деле тесно связанное с языком C#. Bот c этой парой
нам надо бы разобраться.
Особенности и идеология C#
Идеи — редкая дичь в лесу слов.
В. Гюго
Программное обеспечение
Это дело очень интересное. И простое.
Из к/ф «Приключения Шерлока Холмса
и доктора Ватсона. Знакомство»
Что касается набора программного кода, делать это можно хоть в тексто-
вом редакторе — главное, чтобы программа, в которой набирается код, не
добавляла свои специфические инструкции в файл (который должен быть
сохранен с расширением .cs). Само собой разумеется, что описанный выше
способ программирования в C# совершенно неприемлем. Мы им пользо-
ваться не будем.
Программировать (в том числе и на C#) лучше и проще всего с помо-
щью интегрированной среды разработки (IDE от Integrated Development
Environment). Интегрированная среда разработки — это специальная про-
грамма, которая обычно включает в себя редактор программных кодов и на-
бор всевозможных утилит. Нередко в состав среды входят и необходимые
компиляторы, интерпретаторы и надстройки. Все зависит от того, на каком
языке мы собираемся программировать. Нас в данном конкретном случае
интересует C#, поэтому и интегрированная среда разработки нам нужна
для программирования на C#. Поскольку язык C# разработан и поддер-
живается компанией Microsoft, интегрированную среду для нашего обще-
го дела тоже разумно искать в линейке программных продуктов Microsoft.
Здесь можно выделить Visual Studio, но это продукт коммерческий и не-
дешевый. Есть более простая и бесплатная версия интегрированной среды
разработки из серии Express Edition. Ее можно свободно (бесплатно) загру-
зить с сайта компании Microsoft. В книге мы будем использовать версию
Visual C# 2010 Express. Процесс установки этой интегрированной среды
кратко описан в следующем разделе.
Установка Visual C# Express
— Ладно, все. Надо что-то делать.
Давай-ка, может быть, сами изобретем.
— Витя, не надо! Я прошу тебя.
Не дразни начальство!
Из к/ф «Чародеи»
Перед тем, как все начнет устанавливаться, необходимо указать место (ко-
нечную папку) установки. На рис. В.5 показано соответствующее диалого-
вое окно.
Установка Visual C# Express 17
Немного о книге
— Товарищ Тройкина, мы вас целых пять
минут уже здесь ждем. Вы же знаете, у нас
срочная работа.
— Извините. Читала – увлеклась. Такая
книжка интересная попалась!
Из к/ф «Безумный день инженера
Баркасова»
Конечно же, в книгу вошло далеко не все, что касается, так или иначе, язы-
ка программирования C#. Вместе с тем основные темы здесь собраны. Так
что достаточно объективное и во многом полное представление о возмож-
ностях языка C# читатель составить сможет.
Благодарности
Благодарность большинства людей
обычно скрывает ожидание еще больших
благодеяний.
Ф. Ларошфуко
От издательства
Ваши замечания, предложения, вопросы отправляйте по адресу электрон-
ной почты comp@piter.com (издательство «Питер», компьютерная редак-
ция).
Мы будем рады узнать ваше мнение!
Все исходные тексты, приведенные в книге, вы можете найти по адресу
http://www.piter.com.
На веб-сайте издательства http://www.piter.com вы найдете подробную ин-
формацию о наших книгах.
Информация
к размышлению:
язык C# и даже
больше
После ввода программного кода окно среды должно иметь вид, как на
рис. 1.1. В принципе, на этом процесс создания программы завершен.
Осталось только сохранить проект: выбираем команду Сохранить все в меню
Файл или щелкаем на соответствующей кнопке на панели инструментов
(рис. 1.7).
два способа постройки дома. Вариант первый: у нас есть кирпичи, оконные
рамы и дверные блоки. В этом случае мы из кирпичей выкладываем стены,
вставляя дверные блоки и оконные рамы. Каковы преимущества данного
подхода? Выложить можно здание практически любой формы с любой
комбинацией и размещением дверей и окон. Каковы недостатки? Он, по-
жалуй, один: если здание очень большое, придется строить долго. К тому
же, если работают несколько бригад строителей, всяких нестыковок и бра-
ка будет более чем достаточно.
Вариант второй. Заказываем на заводе готовые панельные блоки: блок
с окном, блок с дверью, блок с дверью и двумя окнами, и т. д. Складываем
дом из блоков. Какие преимущества? Быстро и просто. Какие недостатки?
Далеко не все можно построить. Если, например, блоки цилиндрической
формы не заказали, то башню уже не построишь.
Постройка дома — это и есть написание программы. Кирпичи играют
роль данных, а двери и окна — это функции (процедуры), которые вы-
полняют некоторые действия. Первый способ построения дома соответ-
ствует классическому процедурному программированию, когда данные
и функции (процедуры) существуют независимо друг от друга и объе-
диняются вместе по потребности, по воле программиста. Этот подход
достаточно эффективен при написании не очень больших программ.
Если же программы большие, то в принципе несложно запутаться среди
огромного набора данных и списка функций. Поэтому при написании
больших и сложных программ прибегают к объектно-ориентированному
программированию — то есть строят дом из блоков. Такой отдельный
блок в ООП называется объектом. В объекте спаяны воедино и данные,
и функции — точно так же, как в строительном блоке объединены в одно
целое панели, оконные рамы и дверные проемы. Объект создается по об-
разцу. Этим образцом является класс. Аналог класса — это чертеж, по
которому на заводе изготовляется блок. Таким образом, класс задает
шаблон, по которому создаются объекты. Наличие класса не означает
наличие объекта, точно так же, как наличие чертежа не означает, что соз-
дан строительный блок. При этом создать объект без класса нельзя (во
всяком случае, в C#). На основании одного класса можно создать много
объектов, а можно не создать ни одного. Это именно наш случай — в про-
грамме мы описали класс, но объект на основании этого класса создавать
не будем.
В известном смысле класс напоминает описание типа данных, с той лишь
принципиальной разницей, что кроме непосредственно данных в класс
включаются и функции (как правило, предназначенные для обработки
этих данных).
32 Глава 1. Информация к размышлению: язык C# и даже больше
ПРИМЕЧАНИЕ Эта пара фигурных скобок очень часто используется в C# для вы-
деления программных кодов. Место размещения фигурных скобок
крайне демократично – их можно располагать где угодно, лишь бы
последовательность скобок и команд была правильной.
Как отмечалось выше, класс может содержать данные и методы для их об-
работки. Класс HelloWindow состоит всего из одного метода, который на-
зывается Main().
Перед именем метода Main() указаны атрибуты static и void. Атрибут void
означает, что метод не возвращает результат. Атрибут static означает, что
метод статический. О статических методах речь пойдет далее. Важным
Очень простая программа 33
Собственно, все, что нам нужно сделать для составления кода програм-
мы, — это указать имя класса и непосредственно код программы — код
метода Main(). Ну, конечно, еще подключить необходимые простран-
ства имен.
ПРИМЕЧАНИЕ Хотя главный метод программы должен называться Main(), его атри-
буты могут незначительно варьироваться. Например, метод может
возвращать целочисленный результат или принимать аргументы (па-
раметры командной строки).
Что касается полиморфизма, здесь главная идея состоит в том, чтобы уни-
фицировать однотипные действия, сведя к минимуму количество исполь-
зуемых методов. На практике это сводится к тому, что методы, выполняю-
щие схожие действия, называются одним именем, даже если действия эти
выполняются над данными разных типов.
Конечно, в ООП не все так гладко, как об этом пишут в книгах. У ООП есть
критики, причем вплоть до полного его неприятия. Но поскольку у нас
выхода другого нет (ведь в C# реализуется парадигма ООП), мы воспри-
нимаем ООП как данность и искренне верим в то, что ООП — это новый
и исключительно прогрессивный этап в развитии программирования.
36 Глава 1. Информация к размышлению: язык C# и даже больше
class SayHello{
// Главный метод программы:
static void Main(){
// В эту текстовую переменную запишем имя:
string name;
// Отображение окна с полем ввода:
name=Interaction.InputBox("Как Вас зовут?",
"Давайте познакомимся");
// Текст приветствия:
string msg = "Очень приятно, " + name + "!";
// Текст заголовка окна приветствия:
string title = "Окно приветствия";
// Отображение окна приветствия:
MessageBox.Show(msg,title,MessageBoxButtons. OK,
MessageBoxIcon.// Warning);
}
}
В поле ввода этого окна указываем имя и щелкаем на кнопке OK. В резуль-
тате первое окно закроется, а вместо него появится второе (рис. 1.13).
В окне с полем ввода (см. рис. 1.12) кроме кнопки OK есть еще
и кнопка Отмена. Если щелкнуть на кнопке Отмена, окно будет за-
крыто, а в качестве результата возвращается пустая текстовая строка,
которая и будет записана в переменную name. Как следствие, второе
окно появится, но в том месте, где в тексте должно быть имя пользо-
вателя, не будет ничего.
Консольная программа
— Что за вздор. Как вам это в голову взбрело?
— Да не взбрело бы, но факты, как говорится,
упрямая вещь.
Из к/ф «Чародеи»
ПРИМЕЧАНИЕ Значение символьной переменой — это символ (или буква, если под
буквой подразумевать любой символ). Символьный литерал (буква)
заключается в одинарные кавычки. Если букву заключить в двойные
кавычки, это будет текстовый литерал.
Описание класса
Это экспонаты. Отходы, так сказать,
магического производства.
Из к/ф «Чародеи»
ПРИМЕЧАНИЕ Это не всегда так. Есть классы, которые представляют интерес сами
по себе, без всяких там объектов. Обычно это классы со статическими
методами, которые играют роль библиотеки.
ПРИМЕЧАНИЕ Члены класса (в данном случае поля и методы) могут быть закрытыми
и открытыми. Закрытые члены класса — это члены, которые класс
приберегает «для себя», то есть для использования исключительно
в пределах класса. Открытые члены класса можно использовать не
только внутри класса, но и вне его. Именно такие члены класса пока
что нас и будут интересовать.
Объектные переменные
и создание объектов
Очень убедительно. Мы подумаем,
к кому это применить.
Из к/ф «31 июня»
Перегрузка методов
Нет, такой хоккей нам не нужен!
Н. Озеров
ПРИМЕЧАНИЕ Поля у класса тоже не очень простые. Они описаны без ключе-
вого слова public. Такие поля являются закрытыми и недоступны
вне класса. Поэтому в программном коде класса эти поля можно
использовать, а вот обратиться напрямую к полям вне класса не
получится. Например, в главном методе программы создается объ-
ект fellow класса Person. И хотя у объекта fellow есть поля name
и age, использовать инструкцию вида fellow.name или fellow.age
не получится.
Если метод set() вызывается без аргументов, поле age получит нулевое зна-
чение, а значением поля name будет текст "Нет имени". Кроме этого, можно
передать только один аргумент методу set(). Если это числовой аргумент,
то соответствующее значение получает поле age. Поле name, которое обде-
лено вниманием при передаче аргументов методу set(), получит значение
"Нет имени". В случае, когда единственный аргумент метода set() тексто-
вый, это текстовое значение будет присвоено полю name объекта. Числовое
поле age получит нулевое значение.
Конструкторы и деструкторы
Нам песня строить и жить помогает.
Из к/ф «Веселые ребята»
В коде конструктора есть команда вызова метода show(). Этот метод ото-
бражает диалоговое окно с информацией о том, каковы значения полей
объекта, из которого вызван метод. Поскольку метод вызывается из кон-
структора, в окне сообщения будут отображены значения полей вновь соз-
данного объекта.
Также у класса есть конструктор с одним текстовым аргументом. Аргумент
конструктора определяет значение поля name. Два других поля получают
значения по умолчанию — у поля number будет значение 10000, а поле category
получит значение 'B'. Как и в случае конструктора с тремя аргументами,
напоследок в конструкторе с одним аргументом вызывается метод show().
Помимо этих двух конструкторов, у класса есть еще один, достаточно по-
лезный конструктор создания копии. Это общее установившееся название
для конструкторов, которые позволяют создавать новые объекты на основе
уже существующих объектов. При этом новый объект на самом деле совсем
не обязательно должен быть копией исходного объекта (того объекта, что
передается аргументом конструктору). Просто параметры объекта, пере-
данного аргументом конструктору, используются для вычисления значе-
ний полей создаваемого объекта. У конструктора создания копии сигнатура
такая: License(License obj). У этого конструктора один аргумент, который
является объектом класса License. Значения полей создаваемого объекта
формируются на основе полей объекта-аргумента конструктора. Значе-
ние поля name создаваемого объекта получается добавлением к текстово-
му значению поля name объекта-аргумента текстовой фразы " - дубликат".
Поле number создаваемого объекта на единицу больше соответствующего
поля объекта-аргумента конструктора. Значение поля category у обоих
Конструкторы и деструкторы 69
Для того чтобы создать новый класс на основе уже существующего, в опи-
сании нового (производного) класса после имени класса через двоеточие
указывается базовый класс. Другими словами, синтаксис создания произ-
водного класса такой:
class производный_класс: базовый_класс{
// код производного класса
}
В результате наследования вся «начинка» базового класса автоматически
переносится в производный класс. Другими словами, производный класс
в подарок от базового получает все поля и методы базового класса. Кроме
полученного наследства, производный класс может содержать описание
дополнительных членов. Более того, в производном классе только допол-
нительные члены и описываются.
Console.WriteLine("Цвет: "+color);
}
// Конструктор производного класса
// без аргументов:
public ColoredBox():base(){
color="красный";
// Отображаем сообщение:
showAll();
}
// Конструктор производного класса
// с одним аргументом:
public ColoredBox(int size):base(size){
color="желтый";
// Отображаем сообщение:
showAll();
}
// Конструктор производного класса
// с двумя аргументами:
public ColoredBox(int size,string color):base(size){
this.color=color;
// Отображаем сообщение:
showAll();
}
}
// Класс с главным методом:
class ExtDemo{
// Главный метод программы:
public static void Main(){
// Объектные переменные производного класса:
ColoredBox redBox,yellowBox,greenBox;
// Создание объектов производного класса:
redBox=new ColoredBox();
yellowBox=new ColoredBox(100);
greenBox=new ColoredBox(1000,"зеленый");
Console.ReadLine();
}
}
Идея очень простая: сначала создаем базовый класс (который называет-
ся Box), а затем на его основе производный класс (который называется
ColoredBox).
Text=txt;
// Высота окна:
Height=100;
// Ширина окна:
Width=300;
}
}
class MyFormDemo{
// Единый поток:
[STAThread]
// Главный метод программы:
public static void Main(){
// Создание объекта окна:
MyForm mf=new MyForm("Всем большой привет!");
// Отображение формы:
Application.Run(mf);
}
}
В результате выполнения этого программного кода появляется окно, по-
казанное на рис. 2.4.
Объектные переменные
и наследование
Я унаследовал всех врагов своего отца
и лишь половину его друзей.
Дж. Буш-младший
Итак, допустим, что у нас есть класс, который создан путем наследования
на основе базового класса. Для производного класса описан такой же член,
как и в базовом классе. Неприятность в том, что член базового класса на-
следуется. Получается, что в производном классе как бы два члена, и оба
они как бы один член. Возникает два вопроса: как все это понимать, и что
в такой ситуации делать?
Ответы достаточно простые и во многом возвращают кризисную ситуацию
в обычное русло. Во-первых, технически существует два члена. Во-вторых,
по умолчанию, если выполняется обращение к такому двойному члену,
обращение это выполняется на самом деле к тому, который явно описан
в производном классе. Этот член как бы заслоняет или замещает собой
член, наследуемый из базового класса. Вместе с тем второй (замещенный)
член никуда не девается, просто доступ к нему скрыт. В программном коде
производного класса к замещенному члену из базового класса можно вы-
полнить обращение с помощью инструкции base, указав после нее через
точку имя соответствующего поля или заголовок метода. В качестве иллю-
страции рассмотрим пример из листинга 2.8.
Замещение членов класса и переопределение методов 87
name=txt;
}
// Открытый виртуальный метод для
// отображения значения поля:
virtual public void show(){
Console.WriteLine("Класс А: "+name);
}
}
// Производный класс от класса A:
class B:A{
// Конструктор класса:
public B(string txt):base(txt){}
// Переопределение метода в производном классе:
override public void show(){
Console.WriteLine("Класс B: "+name);
}
}
// Производный класс от класса B:
class C:B{
// Конструктор класса:
public C(string txt):base(txt){}
}
// Производный класс от класса C:
class D:C{
// Конструктор класса:
public D(string txt):base(txt){}
// Переопределение метода в производном классе:
override public void show(){
Console.WriteLine("Класс D: "+name);
}
}
// Класс с главным методом программы:
class VirtualDemo{
// Главный метод программы:
public static void Main(){
// Объектная переменная класса A:
A obj;
// Переменная класса A ссылается на
// объект класса A:
obj=new A("поле класса А");
// Вызов метода show() объекта класса A
// через объектную переменную класса A:
obj.show();
// Переменная класса A ссылается на
// объект класса B:
obj=new B("поле класса B"); продолжение
92 Глава 2. Классы и объекты
Несмотря на то, что для объектов каждого из четырех классов метод show()
вызывается через объектную переменную класса A (который находится
в вершине нашей импровизированной иерархии наследования), для каж-
дого из объектов вызывается правильный метод — тот метод, который опи-
сан в классе объекта, а не в классе объектной переменной. Таким образом,
для виртуальных переопределенных методов вопрос о том, какую версию
метода вызывать (старую, унаследованную из базового класса, или новую,
переопределенную в производном классе) решается на основе типа объ-
екта, на который ссылается объектная переменная, а не на основе типа объ-
ектной переменной.
ный тип int или числовой тип с плавающей точкой double. Более полное
представление о базовых типах языка C# дает табл. 3.1.
ПРИМЕЧАНИЕ Есть еще тип decimal, под который отводится аж 128 бит. В известном
смысле это экзотика. Тип предназначен для выполнения расчетов,
в которых критичны ошибки округления. Обычно это финансовые
расчеты.
Переменные логического типа (тип bool) могут принимать всего два зна-
чения: true (истина) и false (ложь). Обычно значения логического типа
используются в условных операторах для проверки условий.
Таблица 3.2. Арифметические операторы C#
Оператор Описание
+ Сложение: бинарный оператор. В результате вычисления выражения вида
A+B в качестве результата возвращается сумма значений числовых пере-
менных A и B. Если переменные текстовые, результатом является строка,
полученная объединением текстовых значений переменных
- Вычитание: бинарный оператор. В результате вычисления выражения вида
A-B в качестве результата возвращается разность значений числовых пере-
менных A и B. Оператор может также использоваться как унарный (перед
переменной, например -A) для противоположного (умноженного на -1)
числа, по отношению к тому, что записано в переменную
продолжение
102 Глава 3. Основы синтаксиса языка C#
Оператор Описание
* Умножение: бинарный оператор. В результате вычисления выражения вида
A*B в качестве результата возвращается произведение значений числовых
переменных A и B
/ Деление: бинарный оператор. В результате вычисления выражения вида
A/B в качестве результата возвращается частное значений числовых пере-
менных A и B. Если операнды (переменные A и B) целочисленные, деление
выполняется нацело. Для вычисления результата на множестве действи-
тельных чисел (при целочисленных операндах) можно использовать коман-
ду вида (double)A/B
% Остаток от деления: бинарный оператор. Оператор применим не только
к целочисленным операндам, но и к действительным числам. В результате
вычисления выражения A%B возвращается остаток от целочисленного
деления значения переменной A на значение переменной B
++ Инкремент: унарный оператор. В результате вычисления выражения
++A (префиксная форма оператора инкремента) или А++ (постфиксная
форма оператора инкремента) значение переменной A увеличивается
на единицу. Оператор возвращает результат. Префиксная форма опера-
тора инкремента возвращает новое (увеличенное на единицу) значение
переменной. Постфиксная форма оператора инкремента возвращает
старое значение переменной (значение переменной до увеличения на
единицу)
-- Декремент: унарный оператор. В результате вычисления выражения
--A (префиксная форма оператора декремента) или А-- (постфиксная
форма оператора декремента) значение переменной A уменьшается на
единицу. Оператор возвращает результат. Префиксная форма операто-
ра декремента возвращает новое (уменьшенное на единицу) значение
переменной. Постфиксная форма оператора декремента возвращает
старое значение переменной (значение переменной до уменьшения
на единицу)
Таблица 3.3. Операторы сравнения C#
Оператор Описание
== Оператор «равно»: результатом выражения A==B является логическое
значение true, если значения переменных A и B одинаковы, и false в про-
тивном случае
!= Оператор «не равно»: результатом выражения A!=B является логическое
значение true, если значения переменных A и B разные, и false в про-
тивном случае
> Оператор «больше»: результатом выражения A>B является логическое зна-
чение true, если значение переменной A больше, чем значение переменной
B, и false в противном случае
< Оператор «меньше»: результатом выражения A<B является логическое зна-
чение true, если значение переменной A меньше, чем значение переменной
B, и false в противном случае
>= Оператор «больше или равно»: результатом выражения A>=B является
логическое значение true, если значение переменной A не меньше, чем
значение переменной B, и false в противном случае
<= Оператор «меньше или равно»: результатом выражения A<=B является
логическое значение true, если значение переменной A не больше, чем
значение переменной B, и false в противном случае
Таблица 3.4. Логические операторы C#
Оператор Описание
& Оператор логического «и» (бинарный). Результатом выражения A&B является
логическое значение true, если оба логических операнда A и B равны true.
Если хотя бы один из операндов равен false, результатом будет false
| Оператор логического «или» (бинарный). Результатом выражения A|B
является логическое значение true, если хотя бы один из логических
операндов A и B равен true. Если оба операнда равны false, результатом
будет false
^ Оператор логического «исключающего или» (бинарный). Результатом вы-
ражения A^B является логическое значение true, если один из операндов,
A или B, равен true, а другой равен false. Если оба операнда равны true
или оба операнда равны false, результатом будет false
продолжение
104 Глава 3. Основы синтаксиса языка C#
Оператор Описание
&& Сокращенная форма логического оператора «и». От обычной формы
логического оператора «и» отличие && состоит в том, что при вычислении
выражения A&&B второй операнд, B, вычисляется, только если первый опе-
ранд, A, равен true. Если первый операнд, A, равен false, то в качестве
результата выражения A&&B возвращается значение false без вычисления
второго операнда, B
|| Сокращенная форма логического оператора «или». От обычной формы
логического оператора «или» отличие || состоит в том, что при вычислении
выражения A||B второй операнд, B, вычисляется, только если первый опе-
ранд, A, равен false. Если первый операнд, A, равен true, то в качестве
результата выражения A||B возвращается значение true без вычисления
второго операнда, B
! Оператор логического отрицания (унарный). Результатом выражения !A яв-
ляется значение true, если операнд A равен false. Если операнд A равен
true, результатом выражения !A возвращается значение false
Таблица 3.5. Побитовые операторы C#
Оператор Описание
& Оператор поразрядного «и». Сопоставляются соответствующие биты двух
чисел. Если оба сопоставляемых бита равны единице, на выходе получаем
единичный бит. Если хотя бы один из двух сопоставляемых битов равен
нулю, на выходе получаем нуль
| Оператор поразрядного «или». Сопоставляются соответствующие биты
двух чисел. Если хотя бы один из сопоставляемых битов равен единице,
на выходе получаем единичный бит. Если оба бита равны нулю, на выходе
получаем нуль
^ Оператор поразрядного «исключающего или». Сопоставляются соответ-
ствующие биты двух чисел. Если сопоставляемые биты разные, на выходе
получаем единицу. Если сопоставляемые биты одинаковы, на выходе по-
лучаем нуль
Базовые типы данных и основные операторы 105
Оператор Описание
>> Оператор сдвига вправо. Бинарный оператор для выполнения сдвига
вправо битов в побитовом представлении числа. Результат получается
смещением битов в значении переменной, указанной слева от оператора,
на количество битов, указанное справа от оператора. При этом старший
знаковый бит сохраняется
<< Оператор сдвига влево. Бинарный оператор для выполнения сдвига влево
битов в побитовом представлении числа. Результат получается смещением
битов в значении переменной, указанной слева от оператора, на количе-
ство битов, указанное справа от оператора. Младшие биты заполняются
нулями
~ Оператор «дополнение до единицы». В двоичном представлении числа
нули заменяются единицами, а единицы — нулями
n
числа при этом находится по формуле anan -1...a1a 0 = å k =0 ak 10k .
Для двоичной системы (которая нас в данном случае интересует боль-
ше всего) параметры ak могут принимать всего два значения: 0 и 1.
n
Значение числа вычисляется по формуле anan -1...a1a 0 = å k =0 ak 2k .
Если бы речь шла о шестнадцатеричной системе счисления, то пара-
метры ak принимали бы значения от 0 до 9 и еще шесть букв A, B,
C, D, E и F (обозначают числа от 10 до 15 соответственно). Значение
n
числа вычисляется по формуле anan -1...a1a 0 = å k =0 ak 16k .
Как отмечалось выше, побитовые операторы оперируют на уровне
двоичного кода числа. В этом представлении число является по-
следовательностью нулей и единиц. Сколько этих нулей и единиц
(в совокупности) определяется количеством бит, отводимых для
записи числа. Например, значения типа int запоминаются в виде
последовательности из 32 нулей и единиц. Скажем, число 5 в двоич-
ном представлении типа int имеет вид 00...0101 (всего 32 цифры).
Старшие нулевые биты, как правило, не упоминают, поэтому про
число 5 обычно говорят, что в двоичном представлении это 101
(поскольку 1 × 20 + 0 × 21 + 1 × 22 = 5 ). Однако здесь появляется
совершенно неожиданная проблема. А именно, как представлять
отрицательные числа? Если бы мы записывали двоичный код на
бумаге, то особых проблем не было бы — достаточно перед числом
дописать знак «минус». Но компьютер не знает, что такое «минус».
Он понимает только «0» и «1». Поэтому для записи отрицательных
чисел используют военную хитрость, которая у интеллигентных
людей проходит под кодовым названием «дополнение до нуля».
Это как раз тот случай, когда недостатки компьютера обращены во
всеобщее благо. Вместо абстрактных рассуждений поясним все на
примере поиска двоичного кода для числа -5. Сразу же зададимся
вопросом: что такое число -5? Ответ может быть такой: это число,
которое в сумме с числом 5 дает значение 0. Именно от этого посыла
и будем отталкиваться. Решаем задачу «от обратного». Для этого
рассмотрим код, который получается в результате инвертирования
бинарного кода числа 5: когда нули заменяются на единицы, а еди-
ницы заменяются на нули. Кстати, соответствующую операцию можно
проделать с помощью побитового оператора ~. Несложно догадаться,
что результатом выражения ~5 является код 11...1010 (всего 32
позиции). Если мы сложим значение 5 и значение ~5, получим код
из всех единиц, то есть значением выражения 5+~5 будет 11...1111
(32 единицы). Добавим к полученному значению число 1. Получим
значение 100..0000 — то есть единица в старшем разряде и еще 32
нуля. Но компьютер в нашем случае запоминает только 32 позиции,
поэтому старший единичный бит теряется. А что остается? А остается
32 нуля. Эти 32 нуля на самом деле не что иное, как самый обычный
ноль. Таким образом, для компьютера значение ~5+1 все равно что
число -5 (с точки зрения конечного результата). Несложно дога-
даться, что это правило остается справедливым и в общем случае:
Базовые типы данных и основные операторы 107
Рис. 3.2. Схема работы упрощенной формы (без else-блока) условного оператора
this.n=100;
}
else{
if(n<1){ // Проверка условия
// Если аргумент меньше 1:
Console.WriteLine("Слишком маленькое число! Изменено на 1.");
this.n=1;
}
else{
// Если аргумент попадает в диапазон
// от 1 до 100:
this.n=n;
Console.WriteLine("Значение "+this.n+" принято.");
}
}
// Отображается сообщение о вычислении суммы:
Console.WriteLine("Вычисление суммы от 1 до "+this.n+".");
}
// Вычисление суммы с помощью оператора while:
int useWhile(){
// Сообщение о том, какой оператор используется:
Console.Write("Используем оператор while. ");
// Индексная переменная и переменная
// для вычисления суммы:
int i=0,s=0;
// Оператор цикла while:
while(i<n){ // Проверка условия
// Изменение индексной переменной
// (для подсчета циклов):
i++;
// Изменение переменной для подсчета суммы:
s+=i;
}
// Результат метода:
return s;
}
// Вычисление суммы с помощью оператора do-while:
int useDoWhile(){
// Сообщение о том, какой оператор используется:
Console.Write("Используем оператор do-while. ");
// Индексная переменная и переменная
// для вычисления суммы:
int i=0,s=0;
// Оператор цикла do-while:
do{
продолжение
118 Глава 3. Основы синтаксиса языка C#
obj=new Summator(i);
// Оператор цикла с символьной
// индексной переменной:
for(char s='A';s<'H';s++){
// Отображение результата
// вычислений выбранным методом:
obj.show(s);
}
// Переход к новой строке:
Console.WriteLine();
}
// Ожидание нажатия клавиши Enter:
Console.ReadLine();
}
}
ПРИМЕЧАНИЕ Кстати, с названием массива дела обстоят не так просто, как может
показаться на первый взгляд. Дело в том, что в C# массивы реализу-
ются по тому же принципу, что и объекты. То, что мы будем называть
именем массива, на самом деле будет переменной массива — пере-
менной, в которой записана ссылка на реальный массив. Идея такая
же, как и в случае с объектными ссылками. Хотя такой подход может
показаться вычурным, во многих отношениях он себя оправдывает.
char[,] symbols;
// Конструктор класса:
public MyArray(int n){
int i,j;
// Создание объекта для генерирования
// случайных чисел:
Random rnd=new Random();
// Создание одномерного целочисленного
// массива:
fibonacci=new int[n];
// Создание символьного двумерного массива:
symbols=new char[n-2,n+2];
// Начальные числа в последовательности
// Фибоначчи:
fibonacci[0]=1;
fibonacci[1]=1;
// Заполнение целочисленного массива
// числами Фибоначчи:
for(i=2;i<fibonacci.Length;i++){
fibonacci[i]=fibonacci[i-1]+fibonacci[i-2];
}
// Заполнение двумерного массива
// случайными буквами:
for(i=0;i<symbols.GetLength(0);i++){
for(j=0;j<symbols.GetLength(1);j++){
// Команда с явным преобразованием типа:
symbols[i,j]=(char)('A'+rnd.Next(n));
}
}
}
// Метод для отображения числового массива:
void showNums(){
Console.WriteLine("Числа Фибоначчи:");
for(int i=0;i<fibonacci.Length;i++){
Console.Write(fibonacci[i]+" ");
}
Console.WriteLine();
}
// Метод для отображения символьного массива:
void showSyms(){
Console.WriteLine("Случайные буквы:");
for(int i=0;i<symbols.GetLength(0);i++){
for(int j=0;j<symbols.GetLength(1);j++){
Console.Write(symbols[i,j]+" ");
}
продолжение
130 Глава 3. Основы синтаксиса языка C#
class CNumDemo{
// Главный метод программы:
public static void Main(){
// Размер массива:
int n=9;
// Модуль комплексного числа:
double r=10;
// Локальные переменные:
double x,y;
// Создание массива из объектных переменных:
CNum[] nums=new CNum[n];
// Заполнение массива:
for(int i=0;i<nums.Length;i++){
x=r*Math.Cos(2*Math.PI*i/n); // Действительная часть
y=r*Math.Sin(2*Math.PI*i/n); // Мнимая часть
nums[i]=new CNum(x,y); // Создание нового объекта
Console.Write(i+1+"-е число: "); // Отображение текста
nums[i].show(); // Отображение параметров числа
}
// Ожидание нажатия клавиши Enter:
Console.ReadLine();
}
}
В программе описывается класс CNum, который имеет некоторую аналогию
с классом для описания комплексных чисел. У класса CNum есть два откры-
тых поля, Re и Im, типа double, которые предназначены для записи, соответ-
ственно, действительной и мнимой частей комплексного числа. У класса
есть конструктор с двумя аргументами и метод show() для отображения
в консоли параметров объекта класса (значений полей Re и Im).
foreach(int s in m){
Console.Write (s+" "); // Элементы отображаются в ряд
}
Console.WriteLine(); // Переход к новой строке
}
// Главный метод программы:
public static void Main(){
int n=15; // Размер массива
// Создание массива из массивов:
int[][] binom=new int[n][];
// Заполнение массива:
for(int i=0;i<binom.Length;i++){
binom[i]=new int[i+1]; // Создаем массив-элемент
binom[i][0]=1; // Первый элемент массива-элемента
// Последний элемент массива-элемента:
binom[i][binom[i].Length-1]=1;
// Заполнение внутренних элементов
// массива-элемента:
for(int k=1;k<binom[i].Length-k;k++){
// Вычисляем биномиальные коэффициенты:
binom[i][k]=binom[i-1][k-1]+binom[i-1][k];
binom[i][binom[i].Length-k-1]=binom[i][k];
}
// Отображаем массив-элемент:
show(binom[i]);
}
// Ожидание нажатия клавиши Enter:
Console.ReadLine();
}
}
Знакомство с указателями
Ну зачем такие сложности?!
Из к/ф «Приключения Шерлока Холмса
и доктора Ватсона. Собака Баскервилей»
ПРИМЕЧАНИЕ Указатели в C# — это отголосок языка С++, в котором без них и шагу не
ступить. Правда, в C# указатели намного консервативнее. Например,
указатели могут ссылаться только на нессылочные данные — то есть
на объект указатели не ссылаются (но зато могут ссылаться на поля
объекта). Но это все же лучше, чем их полное отсутствие — как, на-
пример, в языке Java.
Знакомство с указателями 141
У нас уже заходила речь о том, что действие некоторых базовых операторов
в C# можно доопределить так, что, можно будет эти самые операторы при-
менять в отношении объектов, созданных силой воображения пользовате-
ля (или программиста — это как посмотреть). Называется данное действо
перегрузкой операторов. Именно она будет занимать все наши помыслы
вплоть до окончания данной главы. А может и больше — кому как повезет.
ПРИМЕЧАНИЕ Если быть более точным, это означает, что результатом метода явля-
ется ссылка на объект класса Currency.
Перегрузка арифметических
операторов и операторов
приведения типа
— Скажите, доктор Ватсон, вы понимаете
всю важность моего открытия?
— Да, как эксперимент это интересно.
Но какое практическое применение?
— Господи, именно практическое!
Из к/ф «Приключения Шерлока Холмса
и доктора Ватсона. Знакомство»
• Разность векторов: результатом является вектор c = a - b = (a1 - b1, a2 - b2,
c = a - b = (a1 - b1, a2 - b2, a 3 - b3 ) — вектор, координаты которого равны
разности соответствующих координат отнимаемых векторов.
• Умножение вектора на число (обозначим его как λl ): результатом
является вектор c = λl a = (λ
l a1,λ
l a2, λ
l a 3 ) — вектор, координаты
которого получаются умножением на число каждой координаты
исходного вектора. Деление вектора на число l означает умно-
жение вектора на число 1 lλ.
.
• Скалярное произведение векторов: результатом является число
a × b = a1b1 + a2b2 + a 3b3 — сумма произведений соответствую-
щих координат векторов.
• Модулем вектора называется корень квадратный из скаляр-
ного произведения вектора на самого себя: | a |= a × a = a12 + a22 + a 32
| a |= a × a = a12 + a22 + a 32 .
• Скалярное произведение векторов может быть вычислено и так:
это произведение модулей векторов и на косинус угла между
j ), где через ϕj обозначен угол
ними, то есть a × b =| a | × | b | × cos((ϕ),
между векторами a и b . Это соотношение обычно используют
æ a × b ö÷
для вычисления угла между векторами: ϕ ç
j = arcsin çç ÷÷ .
ç ÷
è| a | × | b |ø
•
Единичным вектором ea в направлении вектора a называется век-
a
тор ea = , то есть вектор a делится на свой модуль | a | .
|a |
// перегруженного оператора
// сложения (двух векторов) и умножения
// (числа на вектор):
return a+(-1)*b;
}
// Перегрузка унарного оператора "минус"
// для вектора:
public static Vector operator-(Vector a){
// Вычисляется как умножение вектора на -1:
return (-1)*a;
}
// Перегрузка оператора инкремента для вектора:
public static Vector operator++(Vector a){
// К вектору добавляется единичный вектор
// того же направления.
// Используем перегруженные операторы деления
// и приведения типа:
a=a+(a/(double)a);
// Возвращаем аргумент как результат:
return a;
}
// Перегрузка оператора декремента для вектора:
public static Vector operator--(Vector a){
// От вектора отнимается единичный вектор
// того же направления.
// Используем перегруженные операторы
// деления и приведения типа:
a=a-(a/(double)a);
// Возвращаем аргумент как результат:
return a;
}
// Перегрузка оператора явного приведения типа.
// Объект класса Vector приводится к значению
// типа double.
// Результатом является модуль
// соответствующего вектора:
public static explicit operator double(Vector a){
// Результат - корень квадратный из
// скалярного произведения
// вектора на самого себя:
return Math.Sqrt(a*a);
}
// Перегрузка оператора неявного
// приведения типа.
// Объект класса Vector приводится к
// текстовому значению (тип string):
продолжение
156 Глава 4. Перегрузка операторов
Как уже отмечалось, у класса Vector всего одно поле, описанное как
double[] coords — полем является переменная числового массива. Соз-
дание самого массива и заполнение его числовыми значениями вы-
полняется в конструкторе. У конструктора три аргумента. Командой
coords=new double[3] в теле конструктора сначала создается массив из
Перегрузка арифметических операторов и операторов приведения типа 157
точки зрения такая ситуация означает умножение на -1. Именно так ее по-
нимаем и мы, когда перегружаем унарный оператор «минус» для вектора:
результатом метода с заголовком Vector operator-(Vector a) является вы-
ражение (-1)*a, которое, кстати, вычисляется на основе перегруженного
оператора умножения числа на вектор.
Операторы инкремента и декремента для объектов класса Vector перегру-
жаются практически одинаково — различия минимальны. Результат для
оператора инкремента (заголовок метода Vector operator++(Vector a))
вычисляется как a=a+(a/(double)a). К вектору добавляется этот же вектор,
деленный на свой модуль, и полученный результат присваивается в каче-
стве значения операнду и возвращается как результат. В операторе декре-
мента (заголовок операторного метода Vector operator--(Vector a)) ре-
зультат вычисляется как a=a-(a/(double) a). От вектора отнимается этот
же вектор, деленный на модуль. Новое значение присваивается операнду
и является результатом метода.
}
// Перегрузка оператора "меньше":
public static bool operator<(Compl a,Compl b){
// Сравниваются модули комплексных чисел:
return (double)a<(double)b;
}
// Перегрузка оператора "больше или равно":
public static bool operator>=(Compl a,Compl b){
// Сравниваются модули комплексных чисел:
return (double)a>=(double)b;
}
// Перегрузка оператора "меньше или равно":
public static bool operator<=(Compl a,Compl b){
// Сравниваются модули комплексных чисел:
return (double)a<=(double)b;
}
// Перегрузка оператора "равно":
public static bool operator==(Compl a, Compl b){
// Вызывается метод Equals():
return a.Equals(b);
}
// Перегрузка оператора "не равно":
public static bool operator!=(Compl a,Compl b){
// Вызывается метод Equals():
return !a.Equals(b);
}
// Переопределение метода Equals():
public override bool Equals(Object obj){
Compl b=obj as Compl;
// Отдельно сравниваются действительные
// и мнимые части чисел:
if((Re==b.Re)&(Im==b.Im)) return true;
else return false;
}
// Переопределение метода GetHashCode():
public override int GetHashCode(){
return Re.GetHashCode();
}
}
// Класс с главным методом программы:
class ComplDemo{
// Главный метод программы:
public static void Main(){
// Объекты для комплексных чисел:
Compl a=new Compl(4,-3);
продолжение
168 Глава 4. Перегрузка операторов
Здесь тоже не так все просто, как может показаться на первый взгляд.
Есть одно важное обстоятельство. Состоит оно в том, что объектная
переменная базового класса может ссылаться на объект производного
Перегрузка операторов отношений 173
Свойства
Это мелочи. Но нет ничего важнее мелочей!
Из к/ф «Приключения Шерлока Холмса
и доктора Ватсона. Знакомство»
Свойство в C# — это нечто среднее между методом и полем. Свойство яв-
ляется симбиозом поля и методов для обработки этого поля. Другими сло-
вами, свойство — это поле, для обработки которого предназначены очень
специальные методы, которые автоматически вызываются при обращении
к свойству. Есть очень специальный метод, который вызывается при при-
сваивании свойству значения, и есть очень специальный метод, который
автоматически вызывается при считывании значения свойства. Оба этих
очень специальных метода называются аксессорами. Аксессоры специфич-
ны не только по сути, но и по форме — описываются они очень необычным
образом. Чтобы понять, что же такое, в конце концов, свойство и как свой-
ство связано с аксессорами, рассмотрим общий шаблон описания свой-
ства:
тип_свойства имя_свойства{
// Аксессор для считывания значения свойства:
get{
// Код get-аксессора
}
// Аксессор для присваивания значения свойству:
set{
// Код set-аксессора
}
}
Начинается все очень традиционно: указывается идентификатор типа свой
ства и имя свойства — все так же, как и для обычного поля.
Мораль очень простая — хотя свойство внешне ведет себя как поле, само по
себе свойство переменную не определяет. Другими словами, даже если мы
описали в классе свойство, это еще не означает, что в памяти появилось ме-
сто для запоминания значения этого свойства. Чтобы было, куда записать
значение свойства, необходимо предусмотреть наличие поля (или полей)
для хранения столь ценной информации. То есть фактически свойство
представляет собой некую оболочку, в которую упаковано обычное (как
правило, закрытое) поле (или нечто другое).
Индексаторы
Ну, хватит! Что вы словно мальчик пускаете
туман? Или вас зовут Монте-Кристо?
Из к/ф «Семнадцать мгновений весны»
ПРИМЕЧАНИЕ Векторным
произведением c = a ´ b векторов a = (a1, a2, a 3 )
и b = (b1,b2,b3 ) называется вектор c = (c1, c2, c3 ) с координатами
c1 = a2b3 - a 3b2 , c2 = a 3b1 - a1b3 и c3 = a1b2 - a2b1. Три последние
формулы можно записать в общем виде как ck = ak +1bk +2 - ak +2bk +1
(индекс k = 1,2, 3 ) при условии циклической перестановки индексов:
индекс 4 следует интерпретировать как индекс 1, а индекс 5 должен
интерпретироваться как индекс 2. Именно этим обстоятельством мы
воспользовались, когда определяли индексатор с целочисленным
аргументом — там вместо индекса берется остаток от деления на 3
(напомним, что индексация элементов массива начинается с нуля).
Такой подход не только обеспечил попадание любого целочислен-
ного индекса индексатора в допустимый диапазон, но и серьезно
упростил задачу по вычислению векторного произведения (которое
реализуется через индексатор с индексом-объектом).
Двойное
векторное произведение мы вычисляем как выражение вида
a ´ (b ´ c ). Двойное векторное произведение реализуется через
индексатор с двумя индексами-объектами.
Как видим, все индексаторы ведут себя вполне прилично. И хотя может
показаться, что индексатор представляет собой очень уж экзотическую
конструкцию, тем не менее в сочетании с механизмом перегрузки операто-
ров он становится грозным оружием в борьбе за написание непонятных, но
исправно работающих кодов. Что касается экзотики, то наше представле-
ние о ней сильно изменится после того, как мы познакомимся с делегатами
и событиями.
Делегаты 193
Делегаты
Его связи там важнее его самого здесь.
Из к/ф «Семнадцать мгновений весны»
}
class DelegateDemo{
// Главный метод программы:
public static void Main(){
// Создание целочисленного массива:
int[] nums={1,-3,5,8,-9,11,-6,15,10,3,-2};
// Создание объекта:
Nums obj=new Nums();
// Создание экземпляра делегата
// и его инициализация:
GetNum FindIt=new GetNum(obj.max);
// Использование экземпляра делегата
// для вызова метода:
Console.WriteLine("Максимальное значение: "+FindIt(nums));
// Присваивание экземпляру делегата
// нового значения:
FindIt=obj.min;
// Использование экземпляра делегата
// для вызова метода:
Console.WriteLine("Минимальное значение: "+FindIt(nums));
// Ожидание нажатия клавиши Enter:
Console.ReadLine();
}
}
В программе для числового массива вычисляется максимальное и мини-
мальное значения. При этом используется экземпляр делегата. Результат
выполнения программы представлен на рис. 5.6.
}
// Класс с главным методом программы:
class MListDemo{
// Главный метод программы:
public static void Main(){
// Создание экземпляра делегата
// (с пустой ссылкой в качестве значения):
MList ShowItAll=null;
// Формируется значение экземпляра делегата:
for(int i=0;i<=20;i++){
// В список делегата добавляется ссылка
// на новый метод:
ShowItAll+=new Pow(i).GetPower;
}
// Вызываются методы из списка-значения
// экземпляра делегата:
ShowItAll(2);
// Ожидание нажатия клавиши Enter:
Console.ReadLine();
}
}
Делегат в этом примере описывается командой delegate void MList
(double x). В данном случае область интересов делегата ограничивается
методами с одним аргументом типа double, не возвращающими результат.
Еще мы описали класс с названием Pow, у которого есть целочисленное
закрытое поле, конструктор с одним аргументом (значение аргумента
конструктора определяет значение закрытого поля создаваемого объек-
та), а также открытый метод GetPower(), у которого один аргумент типа
double и который не возвращает результат. Методом вычисляется такое
значение: аргумент метода возводится в степень, которая определяется
значением закрытого поля power. Полученное значение, как часть тексто-
вого сообщения, отображается в консольном окне. Сообщение содержит
информацию о том, какое число и в какую степень возводилось и какой
при этом получен результат.
Все самое интересное происходит в главном методе программы. Про-
граммного кода там немного, но код этот очень занимательный. Сначала
командой MList ShowItAll=null мы создаем экземпляр делегата с назва-
нием ShowItAll. В качестве значения экземпляр делегата получает пустую
ссылку (значение null). Такой экземпляр пока что ни на что приличное не
ссылается. Но это пока — точнее, до тех пор, пока не запускается оператор
цикла, в котором индексная переменная i пробегает значения от 0 до 20
включительно. В теле оператора цикла командой ShowItAll+=new Pow(i).
GetPower значение экземпляра делегата «пополняется» ссылкой на метод
GetPower() анонимного объекта, который создается командой new Pow(i).
198 Глава 5. Свойства, индексаторы и прочая экзотика
Знакомство с событиями
Я считаю своим долгом поведать наконец,
как все было на самом деле.
Из к/ф «Приключения принца Флоризеля»
Проблема усугубляется тем, что есть события — члены класса, а есть со-
бытия в общем филологическом смысле этого слова — когда что-то где-то
происходит. Эти понятия взаимосвязаны, но не тождественны.
ПРИМЕЧАНИЕ Откровенно говоря, это тоже несложная задача. Другое дело, что
соответствующий несложный код для понимания требует некоторых
нетривиальных разъяснений.
Чтобы понять, какой код нам следует написать, выясним, что происходит,
когда мы щелкаем на кнопке. А в этом случае по большому счету и генери-
руется событие. Если выполнен щелчок на кнопке, программа знает, что
такой щелчок выполнен. Чего она не знает — это как на щелчок реагиро-
вать. Нам нужно сделать две вещи:
написать программный код, который будет выполняться при щелчке на
кнопке;
предпринять необходимые меры, чтобы пометить, что написанный код
выполняется в случае, если произошло событие «щелчок на кнопке».
Нечто похожее мы уже делали в предыдущем разделе. Здесь мы по многим
пунктам будем повторяться, но оправдывает нас важность поставленной
задачи.
Метод, который выполняется при генерировании события (то есть метод,
который реагирует на событие), называется обработчиком события. По
большому счету, обработчик события — это обычный метод, помеченный
специальным образом так, что он выполняется каждый раз, когда про-
исходит событие. То обстоятельство, что метод является обработчиком
события, следует как-то отразить в программном коде. Правильная фра-
за на этот жизненный случай звучит примерно так: «для метода-обра
ботчика необходимо создать экземпляр делегата и зарегистрировать его
в списке обработчиков события элемента графического интерфейса».
Поскольку эта фраза немного туманная, расшифруем ее — медленно
и подробно.
Элементы графического интерфейса реализуются через объекты специ-
альных библиотечных классов или классов, производных от них. Для та-
ких элементов существует предопределенный набор событий (уведомле-
ний о выполнении определенных действий), которые этот элемент может
сгенерировать. Эти события реализуются в виде членов класса. Все, как
в предыдущем разделе, но только события-члены класса уже определены
заранее, и определены не нами. Это во-первых. Есть и во-вторых: мето-
Элементарная обработка событий 205
Как уже отмечалось выше, в классе Form есть свойство Controls, кото-
рое представляет собой коллекцию тех объектов, которые включены в
форму. Поэтому, чтобы включить новый компонент в форму, объекту
этого элемента необходимо «отметиться» в свойстве Controls. Специ-
ально для этих целей у свойства есть метод Add(). Объект добавляе-
мого в форму компонента указывается аргументом метода.
Перечисления
Мы продолжаем то, что мы уже много наделали.
В. Черномырдин
Знакомство со структурами
Ничего, ослы даже лучше, чем дикие
скакуны. Они не будут умничать!
Из к/ф «Айболит 66»
Абстрактные классы
— Пойдем в обход!
— Зачем? Он же вот он!
— Тихо! В обход!
Из к/ф «Айболит 66»
ПРИМЕЧАНИЕ Как минимум, это говорит о том, что мы будем иметь дело с графиче-
скими окнами, и созданием одного класса дело не ограничится.
У создаваемого нами класса есть два защищенных поля: поле Button btn
является ссылкой на кнопку, а поле Label lbl является ссылкой на тексто-
вую метку.
Интерфейсы
Наше повеление: этот танец не вяжется
с королевской честью, мы запрещаем его
на веки веков!
Из к/ф «31 июня»
Но стоит нам щелкнуть на кнопке Отмена, как окно будет закрыто, а работа
приложения завершена.
Интерфейсные переменные
Эта теория недостаточно безумна, чтобы быть верной.
Н. Бор
}
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. Важные конструкции
ref — две в описании метода swap() и две в команде вызова этого метода.
Если после этого запустить команду на выполнение, получим результат,
как на рис. 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
Обращаем внимание читателя на то, что, хотя при описании метода average()
мы отталкивались от того, что его аргументы реализованы в виде массива,
при вызове метода аргументы передаются простым перечислением в кру-
глых скобках после имени метода. Никаких массивов создавать не нужно.
Теперь обсудим способ передачи методу в качестве аргумента переменной,
которой не присвоено значение. Сразу отметим, что вообще такая ситуа-
ция интерпретируется как ошибочная, поэтому, если уж мы используем
подобный экзотический код, нам предстоит каким-то образом предупре-
дить о наших планах компилятор. Благо предупредить его несложно. При
описании метода соответствующий аргумент объявляется с атрибутом out.
Такой же атрибут для аргумента указывается при вызове метода. Что ка-
сается причин, по которым вообще может понадобиться столь хитрый спо-
соб аргументации, то нередко за всем этим стоит желание описать метод,
который вычисляет сразу несколько результатов. Поступать можно по-
разному, но один из возможных способов состоит в том, чтобы один из «ре-
зультатов» записывать в переменную, переданную аргументом методу. По-
нятно, что это далеко не единственный подход, но он допустим. Например,
254 Глава 7. Методы и классы во всей красе
Стоит заметить, что атрибут out (так же, как и атрибут ref, который
рассматривался ранее) указывается как при описании метода, так
и в команде вызова метода.
Кроме того, out-аргумент автоматически передается по ссылке, то есть
в метод передается «оригинал», а не «копия». Это вполне объяснимо,
поскольку копию такого аргумента передавать в метод совершенно
нет никакого смысла.
256 Глава 7. Методы и классы во всей красе
следует понимать так, что роль X играет тип string, а для команды swap<MyC
lass>(ref objA,ref objB) параметр типа X заменяется на значение MyClass.
Результат выполнения программы представлен на рис. 7.7.
Использование обобщенного
типа данных
Эх, погубят тебя слишком широкие возможности.
Из к/ф «Айболит 66»
// присваивается значение
// Команда выполняется, если выше
// не произошла ошибка:
Console.WriteLine("Индекс {0}. Значение {1}.",k,n[k]);
}
// Перехват ошибки выхода за пределы массива:
catch(IndexOutOfRangeException){
Console.WriteLine("Выход за пределы массива.");
}
// Перехват ошибки деления на нуль:
catch(DivideByZeroException){
Console.WriteLine("Деление на нуль.");
}
}
// Ожидание нажатия клавиши:
Console.ReadKey();
}
}
Программа простая и бесполезная. В главном методе программы создает-
ся целочисленный массив из трех элементов. Индексы элементов массива,
таким образом, могут изменяться от 0 до 2 включительно. Запускается опе-
ратор из 20 циклов, и за каждый цикл выполняются некоторые нехитрые
действия: генерируются два целых случайных числа в диапазоне от 0 до 3
включительно.
слова catch. Поскольку сам объект ошибки нам в данном случае не нужен,
объектные переменные для этих классов мы не указываем.
В случае если возникает ошибка, выполнение команд try-блока прекра-
щается и выполняется код одного из catch-блоков. Затем начинает вы-
полняться следующий цикл внешнего в try-catch конструкции оператора
цикла. Результат выполнения программы показан на рис. 7.10.
Многопоточное программирование
Куда? Эй, куда же вы все-то разбежались?
Кто-нибудь, держите меня!
Из к/ф «Айболит 66»
После того как объект класса Thread создан (объект потока), нужно запу-
стить поток. Запуск потока выполняется вызовом метода Start() из объ-
екта потока. В результате поток начинает выполняться. Но поток — это,
по большому счету, последовательность команд. Откуда им взяться? Из
метода, который запускается вследствие вызова метода Start(). Пытли-
вый читатель может спросить: а откуда известно, какой следует запускать
274 Глава 7. Методы и классы во всей красе
// Стили шрифтов:
private string[] FS={"Жирный","Курсив"};
// Названия кнопок:
private string[] BN={"Применить","Выход"};
// Минимальный размер шрифта:
private int min=10;
// Максимальный размер шрифта:
private int max=20;
// Метод для "вычисления" текстового массива
// целочисленных значений:
private string[] FSz(){
// Текстовый массив нужного размера:
string[] fs=new string[max-min+1];
// Оператор цикла для заполнения текстового
// массива:
for(int i=0;i<fs.Length;i++){
fs[i]=(min+i).ToString(); // Преобразование числа в текст
}
return fs; // Результат метода - массив
}
// Метка с образцом текста:
private MyLabel sample;
// Кнопки:
private Button[] Btns;
// Переключатели (для выбора типа шрифта):
private RadioButton[] RBtns;
// Группа переключателей:
private GroupBox FName=new GroupBox();
// Опции (для выбора стиля шрифта):
private CheckBox[] CBtns;
// Текстовое поле для ввода размера текста:
private TextBox tsize;
/*
Группа экземпляров делегатов, используемых
при обработке событий.
*/
private EventHandler[] BH; // Массив экземпляров делегатов
// для кнопок
private EventHandler RBH; // Экземпляр делегата
// для кнопок-переключателей
private EventHandler CBH; // Экземпляр делегата
// для опций
private EventHandler TBH; // Экземпляр делегата
// для текстового поля
// Свойство для определения размера шрифта:
private int FSize{
get{
продолжение
288 Глава 8. Приложение с графическим интерфейсом: учебный проект
}
// Создается объект шрифта:
Font F=new Font(fn,FSize,fs);
// Результат свойства:
return F;
}
}
/*
Метод, который используется в качестве обработчика события
выбора пункта меню, связанного с определением типа шрифта.
*/
public void setType(Object obj,EventArgs ea){
string menu; // Локальная текстовая переменная
menu=(obj as MenuItem).Text; // Текст выбранного пункта меню
// Оператор цикла для перебора
// кнопок-переключателей:
for(int i=0;i<RBtns.Length;i++){
if(menu==RBtns[i].Text){
// Если текст пункта меню совпадает
// с текстом кнопки, переключатель
// устанавливается в выделенное положение:
RBtns[i].Checked=true;
return; // Завершается работа метода
}
}
}
/*
Метод, который используется в качестве обработчика события
выбора пункта меню, связанного с определением стиля шрифта.
*/
public void setStyle(Object obj,EventArgs ea){
int index; // Локальная целочисленная переменная
index=(obj as MenuItem).Index; // Индекс выбранного пункта
// в меню
// Изменение (инверсия) статуса опции:
CBtns[index].Checked=!CBtns[index].Checked;
}
/*
Метод используется для обработки события выбора пункта меню,
связанного с определением размера шрифта.
*/
public void setSize(Object obj,EventArgs ea){
string size; // Локальная текстовая переменная
size=(obj as MenuItem).Text; // Текст выбранного пункта меню
tsize.Text=size; // Присваивание нового значения
продолжение
290 Глава 8. Приложение с графическим интерфейсом: учебный проект
Рис. 8.8. При изменении типа шрифта изменения вступают в силу автоматически
Интересно в данном случае то, что размер шрифта стал равен 20. Анало-
гично обрабатывается ситуация, когда в поле размера шрифта указано
слишком маленькое значение (меньшее 10). Разница в этом случае лишь
такая, что применяется не «максимальный» шрифт 20, а «минимальный»
шрифт 10.
Рис. З.12. Здесь ничего добавлять не нужно — все добавлено без нас
С
Свойство, 32, 175
Событие, 32, 175, 199, 203
Статический член, 93
Структура, 211, 214
У
Указатель, 140
Ц
Цикл, 47
Алексей Николаевич Васильев
C#. Объектно-ориентированное программирование: Учебный курс
ООО «Мир книг», 198206, Санкт-Петербург, Петергофское шоссе, 73, лит. А29.
Налоговая льгота — общероссийский классификатор продукции ОК 005-93, том 2; 95 3005 — литература учебная.
Подписано в печать 05.03.12. Формат 70х100/16. Усл. п. л. 25,800. Тираж 2000. Заказ 0000.
Отпечатано по технологии CtP в ОАО «Первая Образцовая типография», обособленное подразделение «Печатный двор».
197110, Санкт-Петербург, Чкаловский пр., 15.