Листинг 2.4. Класс с конструкторами и деструкторами
using System.Windows.Forms;
class License{
// Закрытые поля класса:
string name;
int number;
char category;
// Конструктор класса с тремя аргументами:
public License(string name,int number,char category){
// Полям присваиваются значения.
// Ключевое слово this является ссылкой на объект,
// из которого вызывается метод
// (в данном случае конструктор):
this.name=name;
this.number=number;
this.category=category;
// Отображаем результат — окно
// со значениями полей:
show();
}
// Конструктор с одним тестовым аргументом:
public License(string name){
// Присваиваем полям значения:
this.name=name;
this.number=10000;
this.category='B';
// Отображаем результат — окно
// со значениями полей:
show();
}
// Конструктор создания "копии" — создание
// объекта на основе
// уже существующего объекта того же класса:
public License(License obj){
продолжение
66
Глава 2. Классы и объекты
Листинг 2.4 (продолжение)
// Значения полей создаваемого объекта
// формируются на основе
// полей объекта-аргумента конструктора:
name=obj.name+" - дубликат";
number=obj.number+1;
category=obj.category;
// Отображаем результат — окно
// со значениями полей:
show();
}
// Деструктор класса:
~License(){
// Формируем текст для отображения
// в окне сообщения:
string txt="Удаление объекта!\n"+getInfo();
// Отображение окна с сообщением
// об удалении объекта:
MessageBox.Show(txt,"Удаление",MessageBoxButtons.OK, MessageBoxIcon.Error);
}
// Закрытый метод для формирования
// текстовой информации на основе
// значений полей объекта:
string getInfo(){
// Начальное значение формируемого текста,
// '\t' — символ табуляции,
// '\n' — переход к новой строке:
string text="Имя:\t"+name+"\n";
text=text+"Номер:\t"+number+"\n";
text=text+"Категория: "+category;
// Метод возвращает результат:
return text;
}
// Метод для отображения окна с сообщением:
public void show(){
// Формируем текст для сообщения:
string txt=getInfo();
// Отображаем окно сообщения:
MessageBox.Show(txt,"Лицензия",MessageBoxButtons.OK, MessageBoxIcon.Information);
}
}
class LicenseDemo{
// Главный метод программы:
public static void Main(){
Конструкторы и деструкторы 67
// Две объектные переменные:
License Lic1,Lic2;
// Создание объекта с помощью конструктора
// с тремя аргументами:
Lic1=new License("Иванов И.И.",11111,'A');
// Создание объекта с помощью конструктора
// создания "копии":
Lic2=new License(Lic1);
// Создание объекта с помощью конструктора
// с одним текстовым аргументом:
Lic2=new License("Петров П.П.");
}
}
У класса License есть три закрытых поля: текстовое (тип string) поле
name, целочисленное (тип int) поле number и символьное (тип char) поле
category. Все вместе представляют собой бледную аналогию водитель-
ской лицензии. У класса есть несколько конструкторов. В частности, есть
конструктор с тремя аргументами. Этот конструктор описан с сигнатурой
License(string name,int number,char category) (и атрибутом public).
Каждый из трех аргументов соответствует полю класса. Более того, аргу-
менты конструктора имеют такие же названия, как названия полей класса.
Поэтому мы столкнулись с неожиданной проблемой: как различить в про-
граммном коде конструктора имена полей и имена аргументов? Ответ
прост и состоит в том, что ссылки на поля следует выполнять с помощью
ключевого слова this, которое обозначает объект, из которого вызывается
метод (или конструктор, как в нашем случае, — ведь конструктор это тоже
метод). Например, ссылка на поле name может быть выполнена как this.
name. Аналогично, инструкции this.number и this.category являются, со-
ответственно, ссылками на поля number и category создаваемого объекта.
Разумеется, не все так просто, как кажется на первый взгляд. Мы
знаем, что обращение к нестатическим полям и методам выполняется
с указанием объекта. Если мы обращаемся к полю при описании про-
граммного кода метода внутри класса, объект как бы отсутствует. Мы
в таких случаях просто писали имя поля или имя метода (с аргумен-
тами или без). Так делать можно — это упрощенная форма ссылки
на поля и методы внутри класса. Но это идеологически не совсем
правильно. Другими словами, объект все равно есть, потому что без
объекта о нестатическом поле или методе говорить нет никакого
смысла. Просто в случае внутреннего кода класса под объектом под-
разумевается тот, из которого вызывается метод, или к полю которого
выполняется обращение. Для формальной ссылки на этот объект ис-
пользуют ключевое слово this. Поэтому если в коде метода встречается
инструкция вида this.поле, это означает обращение к полю объекта, из
68
Глава 2. Классы и объекты
которого вызывается метод. Это же касается и вызова методов. Другое
дело, что вместо этой классической формы внутри класса ссылка на
поля и методы выполняется в упрощенной форме.
Выше мы столкнулись с неоднозначностью — и поля класса, и аргу-
менты конструктора имеют совпадающие имена. Аргумент метода или
конструктора во многом соответствует представлению о локальной
переменной — эта переменная известна и доступна только внутри
метода или конструктора. Если имя локальной переменной совпа-
дает с полем класса, приоритет остается за локальной переменной.
Следовательно, если внутри метода (или класса) просто написать имя
переменной, это будет именно локальная переменная (в нашем случае
аргумент). Поэтому по необходимости ссылку на одноименные поля
выполняем с использованием ключевого слова this.
Следует также отметить, что это не единственный способ использова-
ния ключевого слова this. В этом мы убедимся несколько позже.
В коде конструктора есть команда вызова метода show(). Этот метод ото-
бражает диалоговое окно с информацией о том, каковы значения полей
объекта, из которого вызван метод. Поскольку метод вызывается из кон-
структора, в окне сообщения будут отображены значения полей вновь соз-
данного объекта.
Также у класса есть конструктор с одним текстовым аргументом. Аргумент
конструктора определяет значение поля name. Два других поля получают
значения по умолчанию — у поля number будет значение 10000, а поле catego ry получит значение 'B'. Как и в случае конструктора с тремя аргументами, напоследок в конструкторе с одним аргументом вызывается метод show().
Помимо этих двух конструкторов, у класса есть еще один, достаточно по-
лезный конструктор создания копии. Это общее установившееся название
для конструкторов, которые позволяют создавать новые объекты на основе
уже существующих объектов. При этом новый объект на самом деле совсем
не обязательно должен быть копией исходного объекта (того объекта, что
передается аргументом конструктору). Просто параметры объекта, пере-
данного аргументом конструктору, используются для вычисления значе-
ний полей создаваемого объекта. У конструктора создания копии сигнатура
такая: License(License obj). У этого конструктора один аргумент, который
является объектом класса License. Значения полей создаваемого объекта
формируются на основе полей объекта-аргумента конструктора. Значе-
ние поля name создаваемого объекта получается добавлением к текстово-
му значению поля name объекта-аргумента текстовой фразы " - дубликат".
Поле number создаваемого объекта на единицу больше соответствующего
поля объекта-аргумента конструктора. Значение поля category у обоих
Конструкторы и деструкторы 69
объектов совпадает. Традиционно в конце выполнения всех вычислений
результат отображаем с помощью метода show().
У деструктора класса License сигнатура простая и лаконичная: ~License().
Что касается программного кода деструктора, то сначала командой
string txt="Удаление объекта!\n"+getInfo() инициализируется текстовая
переменная txt со значением, которое получается объединением текстовой
фразы "Удаление объекта!\n" и текста, который возвращается в качестве
результата закрытым методом класса getInfo().
ПРИМЕЧАНИЕ Инструкция \n означает переход к новой строке. Метод getInfo() возвращает в качестве результата текстовую фразу, которая содержит
информацию о значении полей объекта.
Командой MessageBox.Show(txt,"Удаление",MessageBoxButtons.OK, Mes sa-ge Box Icon.Er ror) отображаем окно с сообщением об удалении объекта.
ПРИМЕЧАНИЕ Инструкция MessageBoxIcon.Error в списке аргументов метода
MessageBox.Show() означает, что в окне сообщения будет отобра-
жаться красная пиктограмма с белым крестом — как в классическом
окне с сообщением об ошибке.
Закрытый метод getInfo() для формирования текстовой информации на
основе значений полей объекта не имеет объекта, и в качестве значения —
текст (объектная переменная класса string). Будущий результат метода
поэтапно записывается во внутреннюю локальную текстовую переменную
text. При этом мы используем текстовые фразы, значения полей объекта
и инструкции \n (переход к новой строке) и \t (символ табуляции). После
того как нужное значение сформировано, возвращаем переменную text в качестве результата метода с помощью инструкции return text.
Инструкция return завершает выполнение метода. Если после ин-
струкции указано значение (переменная), это значение возвращается
в качестве результата метода.
Метод show() для отображения окна с сообщением не возвращает резуль-
тата и не имеет аргументов. Командой string txt=getInfo() формируется
текст для отображения в окне сообщения, а само окно отображаем с по-
мощью команды MessageBox.Show(txt,"Лицензия",MessageBoxButtons.OK, Mes sageBoxIcon.Information).
70
Глава 2. Классы и объекты
ПРИМЕЧАНИЕ Инструкция MessageBoxIcon.Information в списке аргументов метода
MessageBox.Show() означает, что в окне сообщения будет отображать-
ся синяя пиктограмма с белой буквой i — как в классическом окне
с информационным сообщением.
В главном методе программы Main() в классе LicenseDemo создаются две
объектные переменные, Lic1 и Lic2, класса License. После этого разными
методами создается несколько объектов. Так, команда создания объекта
с помощью конструктора с тремя аргументами имеет вид Lic1=new License("Иванов
И.И.",11111,'A'). «Копия» объекта создается командой
Lic2=new Licen se(Lic1). Наконец, команда создания объекта с помощью
конструктора с одним текстовым аргументом выглядит как Lic2=new License("Петров П.П."). В результате выполнения этого несложного программ-
ного кода последовательно появляется несколько диалоговых окон, кото-
рые представлены и прокомментированы в табл. 2.1.
ПРИМЕЧАНИЕ Первые три информационных окна, которые отображаются конструк-
торами, отображаются одно за другим после щелчка на кнопке ОК
предыдущего окна. Три окна с предупреждением об удалении объ-
екта отображаются в результате выполнения деструктора. И если
время и место вызова конструктора можно определить достаточно
точно, то, когда именно будет вызван деструктор, сказать можно
только примерно. В C# используется система автоматической сборки
мусора — если в программе на объект утрачены ссылки, то такой
объект будет автоматически удален из памяти. Правда, не факт, что
это произойдет сразу после утраты ссылки. Например, командой
Lic2=new License(Lic1) создается новый объект, и ссылка на него
записывается в переменную Lic2. Однако после выполнения ко-
манды Lic2=new License("Петров П.П.") ссылка на этот объект будет
утрачена, поскольку теперь переменная Lic2 ссылается на другой
объект, созданный инструкцией new License("Петров П.П."). Это
повод для удаления объекта из памяти (и вызова деструктора). Еще
одна хорошая причина вызова деструкторов — удаление объектов
перед завершением работы программы. В нашем случае в программе
(в главном методе) создается три разных объекта (напомним, объекты
создаются там, где есть инструкция new). Поэтому при завершении
работы программы из памяти выгружается три объекта. Три раза
будет запускаться деструктор, и гипотетически появится три окна
с предупреждением об удалении объектов. Гипотетически — потому
что, если пользователь будет нажимать кнопки ОК в последних окнах
неспешно, есть шанс увидеть далеко не все окна — программа за-
кончит работу до того, как все три окна появятся на экране.
Конструкторы и деструкторы 71
Таблица 2.1. Окна, которые отображаются при выполнении программы
Окно сообщения
Комментарий
Диалоговое окно появляется в результате выполне-
ния инструкции new License("Иванов И.И.",
11111,'A')
Диалоговое окно появляется в результате выполне-
ния инструкции new License(Lic1)
Диалоговое окно появляется в результате
new License("Петров П.П.")
Диалоговое окно появляется при удалении объекта,
который создавался в результате выполнения ко-
манды Lic2=new License("Петров П.П.")
Удаление из памяти объекта, который создавался
командой Lic2=new License(Lic1)
72
Глава 2. Классы и объекты
Окно сообщения
Комментарий
Удаление из памяти объекта, который создавался
командой Lic1=new License("Иванов И.И.",
11111,'A')
Нас в дальнейшем будут интересовать в основном конструкторы. При этом
важно помнить, что конструктор вызывается каждый раз при создании но-
вого объекта. Причем именно объекта, а не объектной переменной. Более
того, впоследствии мы узнаем, что класс объектной переменно и класс объ-
екта могут и не совпадать (хотя идеологическая связь между ними будет).
Но все это мы узнаем несколько позже.
Ранее мы работали с классами, для которых не описывались конструк-
торы, и при этом особых проблем с созданием объектов не наблюдали.
Объяснение простое (и мы его уже приводили ранее): у каждого класса
есть конструктор по умолчанию, который не предполагает передачу
аргументов. Именно этот незримый конструктор вызывается при соз-
дании объекта класса, для которого конструктор явно не описан. Как
только мы описали хотя бы один конструктор в классе, конструктор
по умолчанию прекращает свое незримое существование.
Способы создания объектов класса полностью определяются теми
конструкторами, которые описаны в классе. Например, если в классе
не описан конструктор без аргументов (но есть иные конструкторы), в команде new имя_класса() создания объекта после имени класса
пустые скобки оставлять нельзя — это ошибка.
Наследование и уровни доступа
— А рекомендацию нашего венценосного
брата короля Эдуарда этот Мальгрим имеет?
— Имеет, Ваше Величество!
— Хорошая рекомендация?
— Плохая, Ваше Величество!
Из к/ф «31 июня»
Наследование — исключительно полезный и эффективный механизм, кото-
рый значительно упрощает работу программиста и повышает надежность
Наследование и уровни доступа 73
программных кодов. Наследование позволяет создавать новые классы на
основе уже существующих. С прагматичной точки зрения все это означает, что мы можем создавать новые классы не на пустом месте, а на прочном
и проверенном фундаменте. Технически все просто: при создании нового
класса указываем уже существующий класс, на основе которого мы созда-
ем новый класс. Делается такое указание с помощью небольшой добавки
к коду создаваемого класса. Класс, на основе которого создается новый
класс, называется базовым. Класс, который создается на основе базового
класса, называется производным классом.
ПРИМЕЧАНИЕ Иногда базовый класс называют суперклассом, а производный —
подклассом. Но эта терминология скорее относится к Java.
Для того чтобы создать новый класс на основе уже существующего, в опи-
сании нового (производного) класса после имени класса через двоеточие
указывается базовый класс. Другими словами, синтаксис создания произ-
водного класса такой:
class производный_класс: базовый_класс{
// код производного класса
}
В результате наследования вся «начинка» базового класса автоматически
переносится в производный класс. Другими словами, производный класс
в подарок от базового получает все поля и методы базового класса. Кроме
полученного наследства, производный класс может содержать описание
дополнительных членов. Более того, в производном классе только допол-
нительные члены и описываются.
Идиллию нарушают закрытые члены базового класса, то есть те члены
базового класса, которые описаны с атрибутом private или вообще без
идентификатора уровня доступа. Такие члены класса, по большому
счету, наследуются производным классом, но у него нет к ним доступа.
Другими словами, в программном коде производного класса нельзя
обратиться к private-члену базового класса. При этом непрямая ссылка
возможна. Например, в базовом классе есть закрытое поле и открытый
метод, который обращается к этому полю. В производном классе мы
можем вызвать открытый метод, но не можем обратиться к закрытому
полю. Вместе с тем этот самый открытый метод преспокойно обраща-
ется к закрытому полю. Вот такой парадокс (который, разумеется, на
самом деле парадоксом не является).
Помимо ключевых слов pubic и private, есть ключевое слово protected, которое используют для создания защищенных членов класса. Если
речь не идет о наследовании, то между закрытыми и защищенными
74
Глава 2. Классы и объекты
членами класса разницы нет — они доступны внутри класса и не-
доступны за его пределами. А вот при наследовании защищенные
члены класса проявляют свою хитрую сущность — они наследуются, становясь защищенными членами производного класса.
Также можно запретить использовать класс в качестве базового. Если
класс описать с атрибутом sealed, на основе такого класса произво-
дный класс создать не удастся.
В качестве базового класса можно использовать как свои собственные (на-
писанные собственноручно) классы, так и уже готовые, библиотечные.
Рассмотрим программный код, представленный в листинге 2.5.
Листинг 2.5. Наследование классов
using System;
// Базовый класс:
class Box{
// Закрытое поле:
private int size;
// Закрытый метод для присваивания значения полю:
private void set(int size){
this.size=size;
}
// Защищенный метод для отображения
// консольного сообщения:
protected void show(){
string str="\nКоробка с размером ребра "+size+" см"; Console.WriteLine(str);
}
// Конструктор баз аргументов:
public Box():this(10){}
// Конструктор с одним аргументом:
public Box(int size){
// Присваиваем значение полю:
set(size);
}
}
// Производный класс от класса Box:
class ColoredBox:Box{
// Закрытое поле производного класса:
private string color;
// Закрытый метод для отображения значений полей:
private void showAll(){
// Отображаем "размер":
show();
// Отображаем "цвет":
Наследование и уровни доступа 75
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).
ПРИМЕЧАНИЕ В названиях классов сокрыт глубокий философский смысл. Класс
Box как бы описывает коробку (кубическую, у которой все ребра
одинаковые), а класс ColoredBox как бы описывает раскрашенную
коробку. Без этих классов работа картонно-коробочной промышлен-
ности крайне затруднительна.
76
Глава 2. Классы и объекты
Нас, собственно, интересует производный класс ColoredBox. Но, чтобы по-
нять, что он из себя представляет, необходимо сначала разобраться с ба-
зовым классом Box. А разбираться есть с чем. Так, у класса Box имеется за-
крытое поле size (которое определяет длину ребра коробки), два варианта
конструктора (без аргументов и с одним аргументом), а также несколько
методов. Для присваивания значения полю size предназначен метод set(), который не возвращает результат. Единственный аргумент определяет
значение, присваиваемое полю size. Метод объявлен с атрибутом private, что означает исключительную закрытость метода — он не только недосту-
пен вне класса, но и не будет напрямую доступен и в производном классе, как и поле size. Мы используем этот метод в конструкторах класса для
того, чтобы присвоить полю size значение. Метод show() предназначен
для отображения значения поля size (с пояснениями). Метод защищен-
ный, поэтому он недоступен за пределами базового класса, но наследуется
в производном классе (но он недоступен вне производного класса).
Конструктор класса с одним аргументом достаточно прост — методом set() полю size присваивается значение. Поэтому код этой версии конструкто-
ра где-то даже банален. А вот по-настоящему интригующим является код
конструктора без аргументов: public Box():this(10){}. Интерес читателя, возможно, вызовет инструкция this(10), указанная через двоеточие после
имени конструктора. Это команда для вызова конструктора с аргументом
10. Пустые фигурные скобки означают, что, кроме этого, больше никаких
действий выполнять не нужно (хотя при желании туда можно было бы что-
то вписать). Таким образом, вызов конструктора без аргументов означает
вызов конструктора с одним аргументом, равным 10. Все просто.
При объявлении производного класса ColoredBox после имени класса че-
рез двоеточие указываем имя базового класса Box. Это простое на первый
взгляд обстоятельство имеет серьезные последствия: класс ColoredBox по-
лучает от класса Box в полное и безвозмездное распоряжение все незакры-
тые (открытые и защищенные) члены, да и закрытые члены базового клас-
са не так недоступны, как может показаться.
При создании объекта производного класса сначала вызывается кон-
структор базового класса. Таковы суровые законы наследования. Аргу-
менты конструктора базового класса указываются в круглых скобках
после ключевого слова base. Непосредственно программный код
конструктора производного класса указывается, как обычно, в фи-
гурных скобках. Но все эти действия выполняются после того, как
будет выполнен соответствующий конструктор базового класса.
Кроме богатого и щедрого наследства, класс ColoredBox имеет и собственные
достижения в виде закрытого текстового поля color, защищенного метода
Наследование и уровни доступа 77
showAll() и трех вариантов конструктора (без аргументов, с одним аргу-
ментом и с двумя аргументами). С конструкторов и начнем. Все они имеют
некоторую особенность в виде инструкции base() (с аргументами или без), которая через двоеточие указывается после имени конструктора. Такая ин-
струкция есть не что иное, как вызов конструктора базового класса.
Другими словами, за ту часть объекта, что описана в базовом классе, отве-
чает конструктор базового класса. За поля и методы, описанные непосред-
ственно в производном классе, отвечает конструктор производного клас-
са. Ключевое слово base (с аргументами или без) можно и не указывать
в описании конструктора производного класса. В этом случае все равно
будет вызываться конструктор базового класса — это будет конструктор
по умолчанию (конструктор без аргументов).
В теле конструктора производного класса полю color присваивается зна-
чение, после чего методом showAll() информация о значениях полей size и color выводится в консоль. В методе showAll(), кроме прочего, вызыва-
ется унаследованный из базового класса метод show().
ПРИМЕЧАНИЕ Формально поля size у класса ColoredBox как бы и нет, поскольку
это поле объявлено в базовом классе Box как закрытое. Во всяком
случае, в программном коде класса ColoredBox на поле size ссылаться
бесполезно — классу об этом поле ничего неизвестно. Тем не менее
технически это поле существует, и такой метод, как show(), насле-
дуемый в производном классе, преспокойно отображается к этому
полю. Значение этому несуществующему полю присваивается, когда
в конструкторе производного класса вызывается конструктор ба-
зового класса, в котором, в свою очередь, вызывается метод set(), о котором производный класс тоже ничего не знает.
В главном методе программы мы, вызывая разные конструкторы, создаем
три объекта производного класса. При этом в консоль выводятся сообще-
ния. Результат работы программы показан на рис. 2.3.
Рис. 2.3. Результат работы программы с базовым и производным классами
78
Глава 2. Классы и объекты
ПРИМЕЧАНИЕ Особо любопытным интересно будет узнать, что, помимо атрибутов
public, private и protected, определяющих уровень доступа членов
класса, в C# есть еще и атрибут internal. Член класса, описанный с этим
атрибутом, доступен в пределах компоновочного файла. Такого типа
члены актуальны при создании компонентов. Поскольку мы в ближай-
шее время компоненты создавать не планируем, то и идентификатор
internal использовать не будем.
Для применения наследования необязательно создавать высокоинтел-
лектуальные коды, наподобие приведенных выше. Как уже отмечалось, наследовать (использовать как базисный) можно и стандартный, библи-
отечный класс. В качестве простой иллюстрации рассмотрим процесс
создания программы с графическим интерфейсом, который состоит из
одного-един ст вен ного, более чем скромного окна. Для выполнения этой
миссии мы на основе библиотечного класса Form путем наследования соз-
дадим собственный класс, через который, собственно, и реализуем окон-
ную форму.
Здесь речь идет о создании пользовательской оконной формы про-
граммными методами, без использования графического конструктора.
Для этих целей предназначен класс Form. Создание формы означает
на самом деле создание объекта этого класса. Другими словами, мы
могли бы просто в программе создать объект класса Form, а затем
с помощью статического метода Run() класса Application отобразить
эту форму на экране компьютера. На практике поступают несколь-
ко иначе, а именно, на основе класса Form создают производный
класс, сразу прописав нужные свойства/характеристики и определив
важные настройки. Для создания оконной формы создают объект
этого производного класса. Этим мы и собираемся заняться в самое
ближайшее время.
Полезный в нашей работе программный код представлен во всей красе
в листинге 2.6.
Листинг 2.6. Наследование класса Form
using System;
using System.Windows.Forms;
// Наследуется класс Form:
class MyForm:Form{
// Конструктор класса с текстовым аргументом:
public MyForm(string txt){
// Заголовок окна:
Наследование и уровни доступа 79
Text=txt;
// Высота окна:
Height=100;
// Ширина окна:
Width=300;
}
}
class MyFormDemo{
// Единый поток:
[STAThread]
// Главный метод программы:
public static void Main(){
// Создание объекта окна:
MyForm mf=new MyForm("Всем большой привет!");
// Отображение формы:
Application.Run(mf);
}
}
В результате выполнения этого программного кода появляется окно, по-
казанное на рис. 2.4.
Рис. 2.4. Такое простое окно отображается в результате выполнения программы
Окно, как уже отмечалось, настолько простое, что даже комментировать
его внешний вид нет никакой возможности — ни кнопок, ни переключате-
лей. Из всех декоративных атрибутов — только строка заголовка. Это окно
можно перемещать, изменять (с помощью мышки) его размеры, свернуть/
развернуть, а также закрыть с помощью системной пиктограммы в правом
верхнем углу окна. Но, несмотря на такую простоту, окно это примечатель-
но тем, что является первым нестандартным окном, с которым мы имеем
дело в этой книге, созданным собственноручно.
Думается, излишне напоминать, что данная программа реализуется
в среде Visual C# Express как Windows-проект.
Теперь разберем по кирпичикам наш чудесный код, выполнение которого
приводит к столь примечательным результатам. Начнем с класса MyForm,
80
Глава 2. Классы и объекты
который создается на основе класса Form. Процесс наследования стан-
дартный: после имени создаваемого производного класса через двоеточие
указываем имя базового класса. После этого в фигурных скобках описы-
ваем дополнительный код. В данном случае это код конструктора класса
MyClass. Мы описали лишь один конструктор с текстовым аргументом.
Этот аргумент используется при присваивании значения полю Text.
Поле наследуется из класса Form. Значение этого поля определяет заго-
ловок создаваемого окна. Другим словами, если мы будем реализовывать
оконную форму через объект класса MyForm, в строке названия этого окна
будет текст, присвоенный в качестве значения полю Text. Поля Height и Width ответственны за высоту и ширину окна (в пунктах) соответствен-
но. В конструкторе этим полям также присваиваются значения (целочис-
ленные).
У класса Form имеются всевозможные поля (точнее, свойства — но
пока это не принципиально) и методы, которые наследуются при
создании на основе класса Form производного класса MyForm.
Каждое поле определяет некоторое свойство или характеристику
оконной формы. Поэтому настройка параметров оконной формы
сводится в основном к присваиванию правильных значений по-
лям/свойствам объекта, через который эта форма реализуется.
В рассматриваемом примере такая настройка выполняется прямо
в конструкторе.
В главном методе программы инструкцией MyForm mf=new MyForm("Всем боль-
шой привет!") создается объект mf класса MyForm. Это объект для оконной
формы. Аргументом конструктору передан текст, который будет впослед-
ствии отображаться в строке названия оконной формы. Но создание объ-
екта еще не означает, что форма появится на экране. Мы ее пока только
создали, и она надежно хранится в «закромах родины». А вот чтобы из-
влечь ее на свет божий, нужна команда Application.Run(mf). Из класса
Application вызывается статический метод Run(), аргументом которому
передается объект формы, которую следует отобразить. Это классика
жанра — так мы будем поступать каждый раз, когда захотим увидеть на
экране ту или иную форму.
После того как пройдет эйфория по поводу созданного окна, станет со-
вершенно очевидно, что в окнах подобного рода пользы нет никакой.
Нам нужны добротные и функциональные оконные формы. Чтобы на-
учиться их создавать, предстоит серьезно расширить наши горизонты
в области основ языка C#. Поэтому с высот базовых принципов ООП
опускаемся к более насущным задачам. О них пойдет речь в следующей
главе.
Объектные переменные и наследование 81
ПРИМЕЧАНИЕ Выше мы сокрушались по поводу того, что в окне нет управляющих
элементов — ни тебе кнопок, ни списков, вообще ничего. Так вот, добавить все эти детали в окно достаточно просто. Намного сложнее
научить элементы управления правильному поведению. Вообще, са-
мый сложный этап в программировании приложений с графическим
интерфейсом связан с обработкой событий. Именно благодаря обра-
ботке событий компоненты оживают, становятся функциональными.
По сравнению с этим весь этот оконный декор является сплошной
забавой.
Вместе с тем закрыты еще не все вопросы, касающиеся классов и объектов.
Частично мы их будем закрывать по ходу книги, а несколько важных во-
просов рассмотрим прямо сейчас.
Объектные переменные
и наследование
Я унаследовал всех врагов своего отца
и лишь половину его друзей.
Дж. Буш-младший
Мы уже знаем, что объектная переменная — это переменная, которая ссы-
лается на объект. Значением объектной переменной является некий адрес
(который сам по себе нам ни о чем не говорит), и, когда мы обращаемся
к объектной переменной, она автоматически передает наше обращение объ-
екту, адрес которого она хранит. При объявлении объектной переменной
мы в качестве ее типа указывали имя класса, на объекты которого в прин-
ципе может ссылаться переменная. Все вроде бы понятно. Возникает во-
прос: при чем тут наследование? Ответ такой: переменная базового класса
может ссылаться на объект производного класса. Другими словами, если
класс B наследует класс A, то мы можем объявить объектную переменную
класса A, а в качестве значения присвоить ей ссылку на объект класса B.
Правда, здесь есть одно серьезное ограничение: через объектную перемен-
ную базового класса в объекте производного класса можно ссылаться толь-
ко на те члены, которые описаны в базовом классе. Так, если переменная
класса A ссылается на объект класса B, то доступ будет только к тем членам
класса B, которые унаследованы им из класса A.
В листинге 2.7 представлен пример, в котором есть и объектные перемен-
ные, и производные классы.
82
Глава 2. Классы и объекты
Листинг 2.7. Объектные переменные и наследование
using System;
// Базовый класс A:
class A{
// Открытое текстовое поле:
public string nameA;
// Открытый метод для отображения значения поля:
public void showA(){
Console.WriteLine("Метод класса А: "+nameA);
}
}
// Производный класс B от базового класса A:
class B:A{
// Еще одно открытое текстовое поле:
public string nameB;
// Открытый метод для отображения
// значения двух полей:
public void showB(){
Console.WriteLine("Метод класса B: "+nameA+" и "+nameB);
}
}
// Производный класс C от базового класса B:
class C:B{
// Новое открытое текстовое поле:
public string nameC;
// Открытый метод для отображения
// значения трех полей:
public void showC(){
Console.WriteLine("Метод класса C: "+nameA+",
"+nameB+" и "+nameC);
}
}
// Класс с главным методом программы:
class ABCDemo{
// Главный метод программы:
public static void Main(){
// Объектная переменная класса A:
A objA;
// Объектная переменная класса B:
B objB;
// Объектная переменная и объект класса C:
C objC=new C();
// Объектной переменной класса A
// в качестве значения
// присваивается ссылка на объект класса C:
objA=objC;
Объектные переменные и наследование 83
// Объектной переменной класса B
// в качестве значения
// присваивается ссылка на объект класса C:
objB=objC;
// Доступ к объекту класса C
// через переменную класса B.
// Поле nameC и метод showC() недоступны:
objB.nameA="красный";
objB.nameB="желтый";
objB.showA();
objB.showB();
// Доступ к объекту класса C через
// переменную класса C.
// Доступно все:
objC.nameC="зеленый";
objC.showC();
// Доступ к объекту класса C через
// переменную класса A.
// Доступны поле nameA и метод showA():
objA.nameA="белый";
objA.showA();
// Ожидание нажатия клавиши (любой):
Console.ReadKey();
}
}
Идея такая: класс А содержит текстовое поле и метод для отображения зна-
чения этого поля. На основе класса А путем наследования создается класс
В, который получает поле и метод класса А и, кроме них, добавляет в свой
арсенал еще одно текстовое поле и еще один метод, который отображает
значение обоих текстовых полей. На основе класса В, опять же путем на-
следования, создается класс С. Класс С получает в наследство два тексто-
вых поля и два метода из класса В, и в нем описано еще одно текстовое поле
и метод, который позволяет отобразить в консоли значения всех трех полей
класса. Таким образом, получаем цепочку наследования: класс А является
базовым для класса В, а класс В является базовым для класса С. Это пример
многоуровневого наследования, которое, в отличие от многократного (или
множественного) наследования, в С# разрешено и широко используется
на практике.
Многократное наследование — это наследование, при котором один
класс создается сразу на основе нескольких базовых классов. Так де-
лать в C# нельзя. Многоуровневое наследование — это наследование, при котором производный класс сам является базовым для другого
класса. Так в C# делать можно. Этим мы и воспользовались выше.
84
Глава 2. Классы и объекты
В главном методе программы мы объявляем три объектные переменные: переменная objA класса A, переменная objB класса B и объектная перемен-
ная objC класса C. Причем последней в качестве значения присваивается
ссылка на новосозданный объект класса C. И пока все банально. Неба-
нально становится, когда мы командами objA=objC и objB=objC ссылку на
объект класса C присваиваем объектным переменным objA и objB. После
этого все три переменные (objA, objB и objC) ссылаются на один и тот же
объект.
О том, что переменная базового класса может ссылаться на объект
производного класса, мы уже намекали ранее. В этом смысле присваи-
вание переменной класса B ссылки на объект класса С не является
неожиданностью. Но, поскольку класс B является производным от
класса A, то на объект класса C может ссылаться и переменная класса
A. Имеет место своеобразная транзитивность. При этом ограничение
остается прежним: доступ через объектную переменную есть только
к тем членам, которые прописаны в классе, к которому относится
объектная переменная.
Однако полномочия у переменных objA, objB и objC разные. Переменная
objC имеет доступ ко всем трем полям и методам. Переменная objB имеет
доступ к двум полям и двум методам: тем, что описаны в классе B и унасле-
дованы в классе B из класса A. Через переменную objA доступны только те
поля и методы, которые описаны непосредственно в классе A.
Для разнообразия мы вместо метода Console.ReadLine() в главном
методе программы использовали метод Consile.ReadKey(). Метод
Console.ReadLine() считывает текст ввода в консоли, а признаком
окончания ввода является нажатие клавиши Enter. Метод Consile.
ReadKey() считывает нажатую клавишу. Поэтому в рассматриваемом
примере консольное окно не закроется, пока мы не нажмем какую-
нибудь клавишу. Если бы мы использовали метод Console.ReadLine(), пришлось бы нажимать именно клавишу Enter.
Командами objB.nameA="красный" и objB.nameB="желтый" через перемен-
ную objB заполняем поля объекта objC. Третье поле, nameC, через пере-
менную objB недоступно. Поэтому, чтобы присвоить полю значение, ис-
пользуем команду objC.nameC="зеленый". Но перед этим командами objB.
showA() и objB.showB() проверяем поля, у которых есть значения. К тре-
тьему, незаполненному полю эти методы не обращаются. После того как
заполнено и третье поле, проверяем результат присваивания значения
Замещение членов класса и переопределение методов 85
полям с помощью команды objC.showC(). Если же мы хотим получить до-
ступ к объекту класса C через переменную класса A, то доступными будут
лишь поле nameA и метод showA() объекта класса C. Эту ситуацию иллю-
стрируют команды objA.nameA="белый" и objA.showA(). Результат выполне-
ния программы пред ставлен на рис. 2.5.
Рис. 2.5. Объектные переменные и наследование: результат выполнения программы
Как мы увидим далее в книге, не только объектные переменные базового
класса имеют честь ссылаться на объекты производных классов. В C# есть
интерфейсы, которые могут быть реализованы в классе. Переменные ин-
терфейсного типа могут ссылаться на объекты классов, в которых реали-
зуется соответствующий интерфейс. Ситуация во многом схожа с объект-
ными переменными базовых типов. Вместе с тем имеются и существенные
различия, но их обсуждать сейчас не время.
Замещение членов класса
и переопределение методов
— Так что же, выходит, у вас два мужа?
— Выходит, два.
— И оба Бунши?
— Оба!
Из к/ф «Иван Васильевич меняет профессию»
С наследованием связано еще два выдающихся феномена — замещение
членов и переопределение виртуальных методов. В некотором смысле они
идеологически близки, поскольку в обоих случаях речь идет о том, что
имеет место конфликт (в хорошем смысле этого слова) между унаследо-
ванным из базового класса членом и аналогичным членом, описываемым
в производном классе. Начнем с замещения. Суть его состоит в том, что
при наследовании в производном классе описывается член с абсолютно
такими же параметрами, как и в базовом классе. Это может быть как поле, так и метод.
86
Глава 2. Классы и объекты
ПРИМЕЧАНИЕ Строго говоря, полями и методами члены класса не ограничиваются.
Членами класса могут быть, например, свойства или индексаторы —
этот факт уже отмечался нами ранее. Но пока мы знакомы с полями
и методами, на их примере и рассматриваем вопрос о замещении
членов при наследовании.
С формальной точки зрения ситуация достаточно простая. В производном
классе описывается, например, поле с таким же именем и типом, как поле
в базовом классе. Также это может быть метод с такими же атрибутами, включая имя и список аргументов. В этом случае класс получает два члена
с одинаковыми атрибутами. И это не является ошибкой. Единственное, что
нам следует указать, сознательно или нет мы допускаем такую ситуацию.
Если в производном классе мы специально описываем новый старый член, перед этим членом указывается ключевое слово new. Единственное назна-
чение идентификатора new в такой ситуации — показать, что мы в курсе
того, что у класса два одинаковых члена. Не больше.
Если инструкцию new возле члена-клона в производном классе не
указать, программный код будет откомпилирован, но с предупре-
ждением. Поэтому в известном смысле использование инструкции
new — это скорее правила хорошего тона, чем острая необходи-
мость.
Итак, допустим, что у нас есть класс, который создан путем наследования
на основе базового класса. Для производного класса описан такой же член, как и в базовом классе. Неприятность в том, что член базового класса на-
следуется. Получается, что в производном классе как бы два члена, и оба
они как бы один член. Возникает два вопроса: как все это понимать, и что
в такой ситуации делать?
Ответы достаточно простые и во многом возвращают кризисную ситуацию
в обычное русло. Во-первых, технически существует два члена. Во-вторых, по умолчанию, если выполняется обращение к такому двойному члену, обращение это выполняется на самом деле к тому, который явно описан
в производном классе. Этот член как бы заслоняет или замещает собой
член, наследуемый из базового класса. Вместе с тем второй (замещенный) член никуда не девается, просто доступ к нему скрыт. В программном коде
производного класса к замещенному члену из базового класса можно вы-
полнить обращение с помощью инструкции base, указав после нее через
точку имя соответствующего поля или заголовок метода. В качестве иллю-
страции рассмотрим пример из листинга 2.8.
Замещение членов класса и переопределение методов 87
Листинг 2.8. Замещение членов класса при наследовании
using System;
// Базовый класс с полем и методом:
class A{
// Открытое текстовое поле:
public string name;
// Конструктор класса с одним
// текстовым аргументом:
public A(string txtA){
name=txtA;
}
// Открытый метод для отображения значения поля:
public void show(){
Console.WriteLine("Класс А: "+name);
}
}
// Производный класс от класса A:
class B:A{
// Замещение текстового поля
// в производном классе:
new public string name;
// Конструктор производного класса
// с двумя аргументами:
public B(string txtA,string txtB):base(txtA){
name=txtB;
}
// Замещение метода в производном классе:
new public void show(){
Console.WriteLine("Класс B: "+name);
}
// Метод содержит ссылки на замещенные
// члены класса:
public void showAll(){
Console.WriteLine("Небольшая справка по объекту класса B.");
// Ссылка на поле name из базового класса:
Console.WriteLine("Поле name из класса A: "+base.name);
// Ссылка на поле name из производного класса:
Console.WriteLine("Поле name из класса B: "+name); Console.WriteLine("Вызов метода show() из класса A:");
// Вызов метода show() из базового класса:
base.show();
Console.WriteLine("Вызов метода show() из класса B:");
// Вызов метода show() из производного класса:
show();
продолжение
88
Глава 2. Классы и объекты
Листинг 2.8 (продолжение)
// Переход к новой строке:
Console.WriteLine();
}
}
// Класс с главным методом программы:
class ABDemo{
// Главный метод программы:
public static void Main(){
// Объект производного класса:
B objB=new B("поле класса А","поле класса В");
// Вызов метода, в котором есть
// ссылки на замещенные члены:
objB.showAll();
// Объектная переменная базового класса:
A objA;
// Объектная переменная базового класса
// ссылается на объект производного класса:
objA=objB;
// Вызываем метод show() через объектную
// переменную производного класса:
objB.show();
// Вызываем метод show() через объектную
// переменную базового класса:
objA.show();
// Ожидание нажатия какой-нибудь клавиши:
Console.ReadKey();
}
}
У класса A есть текстовое поле name и show() для отображения значения
этого поля. Кроме значения поля name, методом show() также выводится
тестовое сообщение, которое позволяет однозначно определить, что метод
описан именно в классе A. Также у класса имеется конструктор с одним
аргументом, который определяет значение текстового поля name создавае-
мого объекта.
Во многом класс B дублирует класс A. Класс B создается наследованием
класса A. В классе B описывается поле name — такое же, как и то, что на-
следуется классом B из класса A. Поэтому в классе B при описании поля
name мы указали атрибут new. Еще в классе B описывается метод show().
Метод с таким же именем и атрибутами наследуется из класса A. Для мето-
да show() в классе B также указан атрибут new. Метод show() в классе B тоже
отображает значение текстового поля name, и это как раз то поле, которое
описано в классе B. Также метод выводит сообщение с информацией о том,
Замещение членов класса и переопределение методов 89
что метод описан именно в классе B. Благодаря этому мы легко сможем
определить, метод какого класса вызывается.
Конструктор класса B принимает два текстовых аргумента (обозначены
как txtA и txtB). Первый аргумент конструктора txtA передается аргумен-
том конструктору базового класса (инструкция base(txtA) в заголовке
конструктора). Текстовое значение txtA будет присвоено тому полю name, которое наследуется из базового класса A. Здесь еще раз хочется отметить, что замещение поля не означает его отсутствия. Аргумент txtB присваива-
ется в качестве значения полю name, описанному в классе B.
Еще у класса B есть оригинальный метод showAll(), который позволяет со-
ставить достаточно полное впечатление о том, что есть у класса B, а чего
у него нет. Особенность метода в том, что в нем выполняется обращение
как к замещенным членам, так и к замещаемым. Например, инструкции
name и show() означают обращение, соответственно, к полю и методу, опи-
санным в классе B. Инструкции base.name и base.show() означают обраще-
ние к полю и методу, описанным в классе A.
В главном методе программы командой B objB=new B("поле класса А","поле
класса В") мы создаем объект objB класса B со значениями полей name, равными "поле класса А" (для поля из класса A) и "поле класса В" (для
поля из класса B). После этого командой objB.showAll() вызываем метод
showAll(), который позволяет проверить корректность работы программ-
ного кода. Результат представлен на рис. 2.6.
Рис. 2.6. Замещение членов класса при наследовании: результат выполнения программы
Сообщения в консольном окне говорят сами за себя. Но это еще не все.
В главном методе мы выполнили еще несколько незначительных на пер-
вый взгляд команд, последствием выполнения которых являются две по-
следние строки в консольном окне на рис. 2.6. А именно, мы объявили объ-
ектную переменную objA класса A и затем командой objA=objB в качестве
значения присвоили ей ссылку на объект objB. Затем мы вызываем метод
show() двумя разными способами: командой objB.show() через объектную
90
Глава 2. Классы и объекты
переменную objB класса B и командой objA.show() через объектную пере-
менную objA класса A. Что здесь интересного? Интересно вот что: мы уже
знаем, что для объекта класса B обращение show() означает вызов метода, описанного в этом классе. С другой стороны, через переменную класса A мы
имеем доступ только к тем членам и методам, которые определены в классе
A. Что же победит — опыт или молодость? Здесь, в отличие от классиче-
ского сюжета, побеждает опыт. В результате выполнения команды objB.
show() вызывается метод show() из класса B, а в результате выполнения
команды objA.show() вызывается метод show() из класса A. Аналогичная
ситуация имела бы место, если бы мы попробовали обратиться к полю name через объектные переменные obA и objB. Таким образом, при замещении
членов класса вопрос о том, какой вариант метода вызывается или какой
экземпляр поля запрашивается, решается на основе типа объектной пере-
менной. Это не очень хорошая новость. С точки зрения парадигмы ООП
такое положение дел в отношении методов, будь оно единственно возмож-
ным, поставило бы крест на многих полезных начинаниях. Естественно, из
ситуации имеется выход. Связан он с использованием виртуальных мето-
дов, допускающих переопределение в производных классах.
Уделим внимание изучению методики переопределения методов при на-
следовании. Сначала кратко изложим суть дела. Она такова: можно не
только замещать методы в производном классе, но и добиваться того, что
при вызове метода через объектную переменную базового класса вызыва-
лась не старая, базовая версия метода, а новая, переопределенная. Для это-
го нужно сделать две вещи:
В базовом классе объявить метод, который мы планируем (или раз-
решаем — как посмотреть) переопределять в производных классах, как
виртуальный. Для этого в заголовок метода достаточно включить клю-
чевое слово virtual.
В производном классе, в случае необходимости, переопределить вир-
туальный метод — то есть описать его код в производном классе. При
переопределении метода в его заголовок добавляется ключевое слово
override.
Теперь посмотрим, как все это выглядит на практике. Обратимся к про-
граммному коду, который представлен в листинге 2.9.
Листинг 2.9. Переопределение виртуальных методов
using System;
// Базовый класс с полем и методом:
class A{
// Открытое текстовое поле:
public string name;
// Конструктор класса:
public A(string txt){
Замещение членов класса и переопределение методов 91
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. Классы и объекты
Листинг 2.9 (продолжение)
// Вызов метода show() объекта класса B
// через объектную переменную класса A:
obj.show();
// Переменная класса A ссылается на
// объект класса C:
obj=new C("поле класса C");
// Вызов метода show() объекта класса C
// через объектную переменную класса A:
obj.show();
// Переменная класса A ссылается на
// объект класса D:
obj=new D("поле класса D");
// Вызов метода show() объекта класса D
// через объектную переменную класса A:
obj.show();
// Ожидание нажатия какой-нибудь клавиши:
Console.ReadKey();
}
}
В программе описывается четыре класса с именами A, B, C и D. Они по це-
почке наследуют друг друга: класс B создается на основе класса A, класс C
создается на основе класса B, а класс D создается на основе класса C. В клас-
се A описано открытое текстовое поле name, которое наследуется всеми
классами в цепочке наследования, а также виртуальный метод show(), ко-
торый переопределяется в производных классах. Точнее, он переопреде-
ляется в классе B, в классе C наследуется из класса B без переопределения, а в классе D снова переопределяется. Там, где метод переопределяется, он
описан так, что кроме значения поля name выводит сообщение о том, какого
класса этот метод. Также у каждого из классов есть конструктор с одним
аргументом, который присваивается в качестве значения полю name.
В главном методе программы создается объектная переменная obj класса A, после чего она последовательно «получает в подарок» ссылки на объекты
разных классов. И каждый раз из объектной переменной obj вызывается
метод show(). На рис. 2.7 представлен результат выполнения программы.
Рис. 2.7. Переопределение виртуальных методов: результат выполнения программы
Статические члены класса 93
Несмотря на то, что для объектов каждого из четырех классов метод show() вызывается через объектную переменную класса A (который находится
в вершине нашей импровизированной иерархии наследования), для каж-
дого из объектов вызывается правильный метод — тот метод, который опи-
сан в классе объекта, а не в классе объектной переменной. Таким образом, для виртуальных переопределенных методов вопрос о том, какую версию
метода вызывать (старую, унаследованную из базового класса, или новую, переопределенную в производном классе) решается на основе типа объ-
екта, на который ссылается объектная переменная, а не на основе типа объ-
ектной переменной.
Независимо от того, переопределяется или замещается метод, его
старая версия из базового класса доступна через base-ссылку.
Из этого примера также видно, что свойство виртуальности наследуется.
Так, в классе C мы явно не переопределяли метод show(). Поэтому у клас-
са C версия метода show() такая же, как и у класса B. А вот в классе D мы
метод снова переопределили так, как если бы он был объявлен в классе C
как виртуальный. Другими словами, виртуальность метода декларируется
единожды.
Статические члены класса
Не копируйте человека, если вы
неспособны ему подражать.
Й. Берра
У классов могут быть статические члены. Признаком статического члена
является ключевое слово static. Такой атрибут мы встречаем постоянно —
каждый раз главный метод программы описывается с таким атрибутом.
Настало время разобраться в том, что же такое статические члены клас-
са, и в чем их особенности. Здесь мы остановимся только на самых общих
и наиболее важных с прикладной точки зрения моментах, связанных с ис-
пользованием статических членов.
Статический член от обычного, нестатического члена класса, отличается
в первую очередь тем, что он один для всех экземпляров класса. Более того, статический член класса можно использовать даже в том случае, если ни
один объект в классе не создан. Как мы уже знаем, описание статического
члена класса выполняется с ключевым словом static. Вызов статического
94
Глава 2. Классы и объекты
члена класса выполняется в формате имя_класса.статически_член, то есть
вызывается статический член класса так же, как и нестатический, но вме-
сто имени объекта указывается имя класса. Это логично, поскольку ста-
тический член существует вне контекста какого бы то ни было объекта.
Вместе с тем к статическому члену можно выполнить обращение и через
объект — конечно, если такой существует. Но даже если мы прибегаем при
работе со статическими членами к помощи объектов, важно понимать, что
любые изменения статических членов (полей) автоматически отражаются
на всех объектах, поскольку статический член один для всех объектов —
и тех, что уже существуют, и тех, что только будут созданы. В этом смысле
статический член класса — член общего пользования, со всеми плюсами
и минусами этого подхода. Некоторые методы работы со статическими
членами рассмотрим на простом примере. Исследуем программный код, представленный в листинге 2.10.
ПРИМЕЧАНИЕ Здесь мы имеем дело с Windows-проектом. В среде Visual C# Express создается проект соответствующего типа.
Листинг 2.10. Статические члены класса
using System;
using System.Windows.Forms;
// Класс со статическими членами:
class MyForms{
// Закрытое статическое поле для
// подсчета открытых окон:
private static int count=0; // Нулевое начальное значение
// Статический метод для отображения
// окна с двумя кнопками:
public static void ShowForm(){
// Текстовые переменные:
string txt="Перед Вами окно № "; // Текст в окне
// Заголовок окна:
string cpt="Статические члены класса";
// Значение статического поля-счетчика
// увеличивается на единицу:
count++;
// Переменная для запоминания выбора
// пользователя при щелчке на одной
// из кнопок окна:
DialogResult res;
// Отображение окна и запоминание
// выбора пользователя:
res=MessageBox.Show(txt+count,cpt,MessageBoxButtons.OKCancel);
Статические члены класса 95
// Проверяем, каков был выбор пользователя.
// Если щелкнули кнопку ОК:
if(res==DialogResult.OK) ShowForm(); // Рекурсивный вызов
// метода
}
}
// Класс с главным методом программы:
class StaticDemo {
// Главный метод программы:
public static void Main(){
// Вызываем статический метод:
MyForms.ShowForm();
}
}
Идея, положенная в основу программы, достаточно простая. В начале про-
граммы отображается окно с тестовым сообщением в центральной области
окна и двумя кнопками: ОК и Отмена. Текстовое сообщение содержит инфор-
мацию о номере окна. В начале выполнения программы открывается окно
с первым номером. Если пользователь щелкает на кнопке Отмена, окно за-
крывается и на этом работа программы прекращается. Если пользователь
щелкает на кнопке ОК, окно закрывается, но вместо него открывается новое, практически такое же, но с несколько иным текстом — увеличивается номер
окна. Если в этом новом окне щелкнуть на кнопке Отмена, работа программы
прекратится. Если щелкнуть на кнопке ОК, появится новое окно с новым
номером (который на единицу больше номера предыдущего окна), и т. д.
Чтобы добиться желаемого результата, мы описываем специальный класс
с названием MyForms. У этого класса есть целочисленное статическое поле
count, значение которого еще при объявлении указано как нулевое. Это не
обязательно, поскольку по умолчанию числовые поля классов получают
начальные нулевые значения. Но явно указывать значение лучше хотя бы
потому, что так легче читается код. Поле count объявлено не только как
статическое, но еще и как закрытое. Назначение этого поля — запоминать
количество открытых окон. Поэтому оно статическое. Поле должно быть
таким, что единственный способ изменить его — открыть новое окно. Поэ-
тому поле закрытое.
Еще у класса есть статический метод ShowForm() для отображения окна
с двумя кнопками. Метод статический, поэтому для его вызова нам не
надо будет создавать объект класса. В методе объявляются и инициали-
зируются вспомогательные текстовые переменные. Также, поскольку вы-
зов метода означает, что будет открыто окно, командой count++ на единицу
увеличивается значение статического поля-счетчика. Кроме этого, коман-
дой DialogResult res объявляется переменная, с помощью которой мы за-
помним, на какой кнопке щелкнул пользователь в диалоговом окне. Это
96
Глава 2. Классы и объекты
переменная типа перечисления. И здесь нужны некоторые пояснения. Дело
в том, что метод MessageBox.Show(), который мы уже несколько раз исполь-
зовали и будем использовать в методе ShowForm(), возвращает результат.
Этот результат позволяет определить, на какой кнопке в окне щелкнул
пользователь. Нас результат метода MessageBox.Show() ранее не интересо-
вал по прозаичной причине — те диалоговые окна, с которыми мы имели
дело, содержали лишь одну кнопку, поэтому там особых вариантов не было.
В нашем случае окно будет содержать две кнопки. Поэтому мы будем запо-
минать результат вызова метода MessageBox.Show(). Результат метода — это
значение типа DialogResult. В C# есть такое понятие, как перечисление —
набор числовых констант со специальными именами. Переменная, объяв-
ленная как относящаяся к перечислению, может иметь значением одну из
этих констант. Константы из перечисления указываются вместе с именем
перечисления и отделяются от него точкой. Забегая вперед отметим, что
щелчок на кнопке ОК означает, что метод MessageBox.Show() в качестве ре-
зультата вернет значение DialogResult.OK.
Командой res=MessageBox.Show(txt+count,cpt,MessageBoxButtons.OKCancel) отображается окно с двумя кнопками. Текст в окне содержит текущее зна-
чение счетчика count, а константа MessageBoxButtons.OKCancel в качестве
третьего аргумента метода MessageBox.Show()означает, что у окна должно
быть две кнопки (названия кнопок определяются по умолчанию как для
системных кнопок подтверждения и отмены). После того как окно будет
закрыто щелчком на кнопке ОК, кнопке Отмена или системной пиктограм-
ме (все равно что кнопка Отмена), в переменную res будет записан резуль-
тат. Этот результат мы проверяем в условном операторе. Если условие
res==DialogResult.OK выполнено (значение переменной res равно Dialog
Result.OK), снова вызывается метод ShowForm(), в результате чего открыва-
ется еще одно окно, и т. д.
Обратите внимание на то, что мы в методе ShowForm() вызываем (при
определенных условиях) метод ShowForm(), то есть метод вызывается
в самом себе. Такая ситуация называется рекурсией или рекурсивным
вызовом. Это разрешено, но очень опасно.
Главный метод программы в классе StaticDemo состоит всего из одной
команды MyForms.ShowForm(), которой вызывается статический метод
ShowForm() из класса MyForms. В результате отображается окно, представ-
ленное на рис. 2.8.
Дальнейшие события определяются поведением пользователя. Если щел-
кнуть на кнопке Отмена, все сразу прекратится. Если несколько раз щел-
кнуть на кнопке ОК, можно увидеть, например, окно, как на рис. 2.9.
Статические члены класса 97
Рис. 2.8. Так выглядит окно при запуске программы
Рис. 2.9. Так может выглядеть окно после нескольких щелчков
на кнопке ОК: номер окна изменился
Принципиальное его отличие от своих предшественников — номер, кото-
рый красуется в текстовом сообщении в области окна.
На этом мы закончим обсуждение статических членов. Мы еще будем
с ними встречаться, но особо большого внимания уделять им не будем. Тем
не менее в C# в плане работы со статичными членами есть уникальные
и экзотические моменты — например, статические конструкторы, которые
описываются с ключевым словом static и вызываются при загрузке про-
граммного кода класса в память. Но эта тема — для другой книги.
Основы синтаксиса
языка C#
Как полон я любви, как чуден милой лик,
Как много я б сказал и как мой нем язык!
О. Хайям
Не только классы представляют интерес в языке программирования C#.
В нем много других интересных и полезных вещей — и мы сейчас о них
узнаем.
Базовые типы данных
и основные операторы
— А почему он роет на дороге?
— Да потому, что в других местах все уже
перерыто и пересеяно.
Из к/ф «31 июня»
Чтобы понять, что в принципе можно делать с данными в программе, же-
лательно сначала выяснить, какими эти данные могут быть. И в этом деле
не обойтись без рассмотрения базовых типов данных. Благо, с некоторыми
из них мы уже знакомы: это, например, символьный тип char, целочислен-
Базовые типы данных и основные операторы 99
ный тип int или числовой тип с плавающей точкой double. Более полное
представление о базовых типах языка C# дает табл. 3.1.
ПРИМЕЧАНИЕ Для каждого базового (или примитивного) типа данных в C# есть
класс-оболочка. Через такие классы реализуются данные соответ-
ствующих типов, но уже как объекты. Хотя наличие классов-оболочек
на первый взгляд может показаться излишним, на практике это до-
статочно удобно, поскольку через такие классы реализуются многие
полезные методы для работы с данными. В табл. 3.1, кроме прочего, приведены и классы-оболочки для базовых типов данных.
Таблица 3.1. Базовые типы C#
Тип
Класс
Биты
Значения
Описание
byte
Byte
8
от 0 до 255
Целые неотрицательные
числа
sbyte
SByte
8
от –128 до 127
Целые числа
short
Int16
16
от –32768 до 32767
Целые числа
ushort
UInt16
16
от 0 до 65535
Целые неотрицательные
числа
int
Int32
32
от –2147483648 до 2147483647 Целые числа
uint
UInt32
32
от 0 до 4294967295
Целые неотрицательные
числа
long
Int64
64
от –9223372036854775808 до
Целые числа
9223372036854775807
ulong
UInt64
64
от 0 до 18446744073709551615 Целые неотрицательные
числа
float
Single
32
от 1.5E-45 до 3.4E+38
Действительные числа
double
Double
64
от 5E-324 до 1.7E+308
Действительные числа
decimal Decimal
128
от 1E-28 до 7.9E+28
Действительные чис-
ла — специальный тип
для выполнения особо
точных (финансовых)
вычислений
char
Char
16
от 0 до 65535
Символьный тип
bool
Boolean
8
значения true и false
Логический тип
100
Глава 3. Основы синтаксиса языка C#
Основную массу базовых (примитивных) типов составляют числовые
типы. Только непосредственно целочисленных типов восемь, плюс три
для действительных чисел. Нечисловыми являются лишь логический тип
bool и символьный тип char — да и тот представляет собой специальный
числовой тип.
Целочисленные типы различаются между собой диапазоном значений. Тем
не менее тип int имеет некоторое идеологическое преимущество, которое
зиждется в первую очередь на правилах автоматического преобразования
типов, о которых мы поговорим несколько позже. Среди двух типов (float и double), предназначенных для работы с действительными числами, прио-
ритет остается за типом double: во-первых, диапазон допустимых значений
у этого типа шире, а во-вторых, по умолчанию числа с плавающей точкой
интерпретируются как double-значения.
ПРИМЕЧАНИЕ Есть еще тип decimal, под который отводится аж 128 бит. В известном
смысле это экзотика. Тип предназначен для выполнения расчетов, в которых критичны ошибки округления. Обычно это финансовые
расчеты.
Данные типа char — это буквы (или управляющие символы). Другими
словами, значением переменной типа char может быть буква. В отличие от
текста (объект класса string), который заключается в двойные кавычки, отдельный символ заключается в одинарные кавычки.
Если отдельный символ заключить в двойные кавычки, это уже будет
текст, состоящий из одного символа. Например, 'A' — это символьное
значение (тип char), а «A» — текстовое значение (тип string).
Кроме непосредственно букв, есть еще управляющие символы (или
последовательности символов). С двумя мы уже знакомы: это ин-
струкция перехода к новой строке \n и табуляция \t. Каждая из
этих инструкций считается одним символом — во всяком случае, соответствующее значение можно записать в переменную типа char.
Есть и другие интересные инструкции. Например, инструкция \a по-
зволяет сгенерировать «бип» — программный писк. Или, скажем, символ одинарных или двойных кавычек — поскольку и те и дру-
гие используются для выделения литералов (значений символьного
и текстового типов соответственно), то кавычки как символ вводятся
с помощью косой черты: \' для одинарной и \" для двойной. Инструк-
ция \\ позволяет определить символ косой черты. Очень полезна
инструкция \b, с помощью которой курсор вывода переводится на
одну позицию назад.
Базовые типы данных и основные операторы 101
Переменные логического типа (тип bool) могут принимать всего два зна-
чения: true (истина) и false (ложь). Обычно значения логического типа
используются в условных операторах для проверки условий.
Специфика логического типа в C# такова, что там, где должно быть
логическое значение, следует указывать именно логическое значение.
У новичков в программировании, скорее всего, желание поместить
в условном операторе нечто неположенное вряд ли появится. А вот
те, кто знаком с языком программирования C++, могут поддаться
соблазну. Ведь в С++ в качестве логического значения можно ис-
пользовать числа. В C# такой номер не пройдет.
Что касается основных операторов языка C#, то их традиционно делят на
четыре группы:
арифметические операторы, используемые в основном для выполнения
операций с числовыми данными;
операторы сравнения, которые позволяют сравнивать значения пере-
менных;
логические операторы, предназначенные, как ни странно, для выполне-
ния логических операций;
побитовые, или поразрядные, операторы — группа операторов, которые
позволяют выполнять преобразования на уровне побитового представ-
ления чисел.
Кроме этого, имеются такие уникальные и достаточно специфические опе-
раторы, как оператор присваивания и тернарный оператор (такая себе ком-
пактная версия условного оператора). Причем если без тернарного опера-
тора еще как-то можно обойтись, то без оператора присваивания процесс
программирования просто теряет свой сакраментальный смысл.
Арифметические операторы представлены в табл. 3.2.
Таблица 3.2. Арифметические операторы C#
Оператор
Описание
+
Сложение: бинарный оператор. В результате вычисления выражения вида
A+B в качестве результата возвращается сумма значений числовых пере-
менных A и B. Если переменные текстовые, результатом является строка, полученная объединением текстовых значений переменных
Вычитание: бинарный оператор. В результате вычисления выражения вида
A-B в качестве результата возвращается разность значений числовых пере-
менных A и B. Оператор может также использоваться как унарный (перед
переменной, например -A) для противоположного (умноженного на -1) числа, по отношению к тому, что записано в переменную
продолжение
102
Глава 3. Основы синтаксиса языка C#
Таблица 3.2 (продолжение)
Оператор
Описание
*
Умножение: бинарный оператор. В результате вычисления выражения вида
A*B в качестве результата возвращается произведение значений числовых
переменных A и B
/
Деление: бинарный оператор. В результате вычисления выражения вида
A/B в качестве результата возвращается частное значений числовых пере-
менных A и B. Если операнды (переменные A и B) целочисленные, деление
выполняется нацело. Для вычисления результата на множестве действи-
тельных чисел (при целочисленных операндах) можно использовать коман-
ду вида (double)A/B
%
Остаток от деления: бинарный оператор. Оператор применим не только
к целочисленным операндам, но и к действительным числам. В результате
вычисления выражения A%B возвращается остаток от целочисленного
деления значения переменной A на значение переменной B
++
Инкремент: унарный оператор. В результате вычисления выражения
++A (префиксная форма оператора инкремента) или А++ (постфиксная
форма оператора инкремента) значение переменной A увеличивается
на единицу. Оператор возвращает результат. Префиксная форма опера-
тора инкремента возвращает новое (увеличенное на единицу) значение
переменной. Постфиксная форма оператора инкремента возвращает
старое значение переменной (значение переменной до увеличения на
единицу)
Декремент: унарный оператор. В результате вычисления выражения
--A (префиксная форма оператора декремента) или А-- (постфиксная
форма оператора декремента) значение переменной A уменьшается на
единицу. Оператор возвращает результат. Префиксная форма операто-
ра декремента возвращает новое (уменьшенное на единицу) значение
переменной. Постфиксная форма оператора декремента возвращает
старое значение переменной (значение переменной до уменьшения
на единицу)
На практике достаточно часто используются так называемые состав-
ные (или сокращенные) операторы присваивания, в которые, кроме
прочего, могут входить и представленные выше бинарные операторы.
Например, команда вида A+=B означает команду A=A+B. Аналогично, команда A*=B интерпретируется как A=A*B, и т. д. Это замечание
относится и к бинарным побитовым операторам.
Операторы сравнения достаточно просты, а принцип их выполнения ин-
туитивно понятен. Тем не менее эти операторы тоже заслужили свое место
в табл. 3.3.
Базовые типы данных и основные операторы 103
Таблица 3.3. Операторы сравнения C#
Оператор
Описание
==
Оператор «равно»: результатом выражения A==B является логическое
значение true, если значения переменных A и B одинаковы, и false в про-
тивном случае
!=
Оператор «не равно»: результатом выражения A!=B является логическое
значение true, если значения переменных A и B разные, и false в про-
тивном случае
>
Оператор «больше»: результатом выражения A>B является логическое зна-
чение true, если значение переменной A больше, чем значение переменной
B, и false в противном случае
<
Оператор «меньше»: результатом выражения A
чение true, если значение переменной A меньше, чем значение переменной
B, и false в противном случае
>=
Оператор «больше или равно»: результатом выражения A>=B является
логическое значение true, если значение переменной A не меньше, чем
значение переменной B, и false в противном случае
<=
Оператор «меньше или равно»: результатом выражения A<=B является
логическое значение true, если значение переменной A не больше, чем
значение переменной B, и false в противном случае
Результатом выражения с оператором сравнения является логическое зна-
чение (true или false). Такие выражения могут сами входить, как состав-
ная часть, в логические выражения. Операндами логических выражений
являются значения логического типа. Логические же операторы перечис-
лены в табл. 3.4.
Таблица 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#
Таблица 3.4 (продолжение)
Оператор
Описание
&&
Сокращенная форма логического оператора «и». От обычной формы
логического оператора «и» отличие && состоит в том, что при вычислении
выражения 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 Идеологически близки к логическим операторам побитовые (поразряд-
ные) операторы. Необходимо лишь сделать две поправки: во-первых, опе-
рации выполняются с парами (для бинарных операторов) битов в побито-
вом представлении операндов и, во-вторых, вместо логического значения
true следует читать 1, а вместо логического значения false следует читать
0. Правда, в этом правиле исключением являются операторы сдвига. По-
битовые операторы перечислены в табл. 3.5.
Таблица 3.5. Побитовые операторы C#
Оператор
Описание
&
Оператор поразрядного «и». Сопоставляются соответствующие биты двух
чисел. Если оба сопоставляемых бита равны единице, на выходе получаем
единичный бит. Если хотя бы один из двух сопоставляемых битов равен
нулю, на выходе получаем нуль
|
Оператор поразрядного «или». Сопоставляются соответствующие биты
двух чисел. Если хотя бы один из сопоставляемых битов равен единице, на выходе получаем единичный бит. Если оба бита равны нулю, на выходе
получаем нуль
^
Оператор поразрядного «исключающего или». Сопоставляются соответ-
ствующие биты двух чисел. Если сопоставляемые биты разные, на выходе
получаем единицу. Если сопоставляемые биты одинаковы, на выходе по-
лучаем нуль
Базовые типы данных и основные операторы 105
Оператор
Описание
>>
Оператор сдвига вправо. Бинарный оператор для выполнения сдвига
вправо битов в побитовом представлении числа. Результат получается
смещением битов в значении переменной, указанной слева от оператора, на количество битов, указанное справа от оператора. При этом старший
знаковый бит сохраняется
<<
Оператор сдвига влево. Бинарный оператор для выполнения сдвига влево
битов в побитовом представлении числа. Результат получается смещением
битов в значении переменной, указанной слева от оператора, на количе-
ство битов, указанное справа от оператора. Младшие биты заполняются
нулями
~
Оператор «дополнение до единицы». В двоичном представлении числа
нули заменяются единицами, а единицы — нулями
Хотя побитовые операторы могут показаться на первый взгляд экзотикой, умелое их использование значительно упрощает жизнь программисту.
ПРИМЕЧАНИЕ Для эффективной работы с побитовыми операторами необходимо
хотя бы примерно представлять, как в двоичном коде представляются
числовые значения и как эти значения обрабатываются. Здесь мы
приводим краткую справку по этому поводу.
В повседневной жизни мы используем десятичную систему счисления, поэтому для записи чисел нам в принципе нужно десять цифр: от 0 до
9 включительно. Благодаря мудрым арабским мужам и позиционной
записи мы можем легко записать любое, даже самое мало вообрази-
мое число. Причина кроется в достаточно универсальном алгоритме
записи чисел. Причем этот алгоритм касается не только десятичной
системы счисления.
Для обозначения нескольких начальных чисел (начиная с нуля, то есть
0, 1, 2 и так далее) вводятся специальные обозначения — цифры.
Количество цифр определяет систему счисления. В десятичной си-
стеме счисления используют десять цифр, в восьмеричной системе
счисления используют восемь цифр, в двоичной системе счисления
используют две цифры, а в шестнадцатеричной системе — шестнад-
цать (десять цифр и еще шесть букв, которые играют роль «недо-
стающих» цифр). Числа, для которых нет специальных цифр, запи-
сываются с помощью позиционного представления — то есть в виде
последовательности цифр. А именно, любое (неотрицательное) число
записывается в виде последовательности цифр a a 1...
n n
1
a a
-
0 , где че-
рез ak ( k = 0,1,..., n ) обозначены цифры, используемые в системе
счисления. Если речь идет о десятичной системе счисления, то пара-
метры ak могут принимать значения от 0 до 9 включительно. Значение
106
Глава 3. Основы синтаксиса языка C#
числа при этом находится по формуле
n
k
anan 1...
-
1
a a 0 = å a 10 .
k=0 k
Для двоичной системы (которая нас в данном случае интересует боль-
ше всего) параметры ak могут принимать всего два значения: 0 и 1.
n
Значение числа вычисляется по формуле
k
anan 1...
-
1
a a 0 = å a 2 .
k=0 k
Если бы речь шла о шестнадцатеричной системе счисления, то пара-
метры ak принимали бы значения от 0 до 9 и еще шесть букв A, B, C, D, E и F (обозначают числа от 10 до 15 соответственно). Значение
n
числа вычисляется по формуле
k
anan 1...
-
1
a a 0 = å a 16 .
k=0 k
Как отмечалось выше, побитовые операторы оперируют на уровне
двоичного кода числа. В этом представлении число является по-
следовательностью нулей и единиц. Сколько этих нулей и единиц
(в совокупности) определяется количеством бит, отводимых для
записи числа. Например, значения типа int запоминаются в виде
последовательности из 32 нулей и единиц. Скажем, число 5 в двоич-
ном представлении типа int имеет вид 00...0101 (всего 32 цифры).
Старшие нулевые биты, как правило, не упоминают, поэтому про
число 5 обычно говорят, что в двоичном представлении это 101
(поскольку
0
1
2
1 × 2 + 0 × 2 + 1 × 2 = 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
для получения отрицательного числа –число, берем положительное
число, инвертируем его бинарный код (заменяем нули на единицы
и наоборот) и к полученному коду добавляем единицу.
Чтобы узнать, какое отрицательное число представлено двоичным
кодом, поступаем следующим образом. Инвертируем бинарный код
(получим код положительного числа) и вычисляем десятичное зна-
чение. К этому десятичному значению прибавляем единицу и затем
дописываем знак «минус». Это и есть результат.
Но самое важное практическое следствие из всего сказанного состоит, пожалуй, в том, что у отрицательных чисел старший бит всегда единич-
ный, а у положительных чисел старший бит всегда нулевой. Поэтому
старший бит и называется знаковым битом или битом знака.
Те операторы, что рассматривались выше, были либо бинарными, либо уна-
рными — по количеству операндов (один и два соответственно). Но есть один
оператор, у которого аж три операнда. Поэтому оператор так и называют —
тернарный. Вместе с тем это целая конструкция, которая представляет со-
бой условный оператор, результат которого зависит от некоторого условия.
Синтаксис вызова оператора следующий: условие?выражение1:выражение2.
Результат проверяется так: вычисляется значение условия. Это логиче-
ское значение. Если оно true, в качестве значения тернарного оператора
возвращается значение выражения после вопросительного знака (вы ра же-
ние1). Если оно false, возвращается значение выражения после двоеточия
(выражение2). В принципе, компактно и удобно.
Несколько слов скажем еще об операторе присваивания. Мы уже знаем, что в качестве такового используется знак равенства =. Оператор бинар-
ный. Переменной, указанной слева от оператора присваивания, присваива-
ется значение выражения, указанного справа от оператора присваивания.
В этом нет ничего необычного. Удивить может то, что оператор присваи-
вания возвращает результат. Это означает, что в одном выражении может
быть несколько операторов присваивания: блок с присваиванием перемен-
ной значения может, в свою очередь, входить как операнд в более сложное
выражение. В этом смысле вполне законной, например, является такая по-
следовательность команд:
int x,y,z;
x=(y=20)+(z=10);
В результате переменная x получает значение 30, переменная y получает
значение 20, а переменная z получает значение 10. Вместе с тем подобно-
го рода конструкции следует использовать крайне осторожно, и в случае, когда исход дела не совсем ясен, лучше разбивать сложные выражения на
несколько простых.
108
Глава 3. Основы синтаксиса языка C#
ПРИМЕЧАНИЕ В C# есть такая удивительная штука, как перегрузка операторов.
Благодаря перегрузке операторов действие операторов (не всех, но
многих) «доопределяется» для случая, если операндами являются
объекты пользовательских классов. И хотя для базовых типов и би-
блиотечных классов действие операторов переопределить нельзя, механизм перегрузки операторов настолько эффектен, что нередко
служит одним из решающих аргументов для выбора языка C# как
средства программирования. Справедливости ради следует отметить, что в эффектных механизмах в C# недостатка нет.
Основные управляющие инструкции
Мы никогда ничего не запрещаем.
Мы только советуем.
Из к/ф «Забытая мелодия для флейты»
К управляющим инструкциям мы относим всевозможные условные опера-
торы и операторы цикла.
ПРИМЕЧАНИЕ Для знатоков языков программирования C++ и Java сразу отметим, что различие управляющих инструкций в языке C# по сравнению
с означенными языками минимально. Хотя некоторые различия все
же есть.
Начнем с условного оператора if(). Этот оператор позволяет создавать
точки ветвления: в зависимости от того, истинно или нет некоторое усло-
вие, выполняется один из двух блоков команд. У оператора следующий
синтаксис:
if(условие){
// команды — если условие истинно
}
else{
// команды — если условие ложно
}
Выполнение оператора начинается с проверки условия — выражения, ко-
торое в качестве результата возвращает логическое значение (то есть зна-
чение true или значение false). Условие указывается в круглых скобках
после ключевого слова if. Если условие истинно, выполняются команды
Основные управляющие инструкции 109
в фигурных скобках после if-конструкции. На случай, если условие лож-
но, предназначен else-блок. Схема работы условного оператора проиллю-
стрирована структурной диаграммой на рис. 3.1.
Рис. 3.1. Схема работы условного оператора
После завершения выполнения условного оператора управление передает-
ся следующей после тела оператора команде. Вообще, условный оператор
в C# достаточно демократичный. Например, можно использовать систему
вложенных условных операторов — когда в теле условного оператора вы-
зывается еще один условный оператор, и т. д. Существует также упрощен-
ная форма условного оператора, в которой нет else-блока. Общий синтак-
сис упрощенной формы условного оператора следующий:
if(условие){
// команды — если условие истинно
}
Общая схема выполнения упрощенной формы условного оператора про-
иллюстрирована в структурной диаграмме на рис. 3.2.
Как и в полной версии условного оператора, все начинается с проверки
условия, указанного в скобках после ключевого слова if. Если значение
условия равно true, выполняется блок команд в фигурных скобках. Если
значение условия равно false — ничего не выполняется. Управление сразу
передается команде, следующей после условного оператора.
110
Глава 3. Основы синтаксиса языка C#
Рис. 3.2. Схема работы упрощенной формы (без else-блока) условного оператора
ПРИМЕЧАНИЕ Если блок команд в условном операторе состоит всего из одной
инструкции, в фигурные скобки такой блок можно не заключать.
Вместе с тем наличие фигурных скобок повышает читабельность
кода и снижает риск ошибки. Поэтому правила хорошего тона под-
разумевают наличие фигурных скобок везде, где это уместно, а не
только там, где это необходимо.
Еще один оператор, который нередко относят к группе условных операто-
ров, — оператор switch(). Основное рабочее название этого оператора —
оператор выбора. Ниже приведен синтаксис вызова этого оператора: switch(выражение){
case значение_1:
// команды — если выражение равно значению_1
break;
case значение_2:
// команды — если выражение равно значению_2
break;
...
case значение_N:
// команды — если выражение равно значению_N
break;
default:
// команды — если совпадение не найдено
break;
}
Основные управляющие инструкции 111
Так все выглядит в кодах. На рис. 3.3 показана схема выполнения операто-
ра выбора в картинках.
Рис. 3.3. Схема выполнения оператора
выбора с default-блоком
В словах последовательность выполнения оператора выбора может быть
описана следующим образом. После ключевого слова switch в круглых
скобках указывается выражение, которое возвращает целочисленный, символьный или текстовый результат. Затем следует группа case-блоков, в каждом из которых указано значение для сравнения с результатом выра-
жения. Если совпадение найдено, выполняются команды соответствующе-
го case-блока. Последний default-блок является блоком по умолчанию —
команды этого блока выполняются в случае, если ни в одном case-блоке
совпадение не найдено. Блок по умолчанию не является обязательным.
Как выполняется оператор выбора без default-блока, иллюстрирует диа-
грамма на рис. 3.4.
Если в операторе выбора блока по умолчанию нет, то при отсутствии со-
впадений управление передается следующему после оператора выбора
оператору.
112
Глава 3. Основы синтаксиса языка C#
Рис. 3.4. Схема выполнения оператора выбора
без default-блока
Каждый блок оператора выбора, в том числе и блок по умолчанию, обычно заканчивается инструкцией break. Эта инструкция имеет до-
статочно универсальное назначение и останавливает выполнение
операторов цикла и, в частности, оператора выбора.
В некотором отношении оператор выбора объединяет в себе свойства как
условного оператора, так и оператора цикла. В C# есть несколько опера-
торов цикла и еще одна замечательная инструкция goto, которая в извест-
ном смысле стоит целого оператора. И сейчас как раз наступило время для
того, чтобы познакомиться с этими замечательными управляющими ин-
струкциями.
Достаточно простой, с точки зрения синтаксиса и логики выполнения, опе-
ратор цикла while(). В круглых скобках после ключевого слова while ука-
зывается выражение, возвращающее значение логического типа. По нашей
доброй традиции такие выражения мы просто и лаконично называем усло-
виями. Так вот, если выражение (условие) возвращает значение true, вы-
полняется блок команд в фигурных скобах сразу после while-инструкции.
После этого снова проверяется условие. Если получаем значение true, блок команд выполняется снова. Так продолжается до тех пор, пока зна-
чение условия не станет равным false. Если это произошло, работа опера-
тора цикла while() заканчивается и управление передается следующему
Основные управляющие инструкции 113
оператору после оператора цикла. Последовательность действий проил-
люстрирована диаграммой на рис. 3.5.
Рис. 3.5. Схема выполнения оператора цикла while() Синтаксис вызова оператора цикла while() такой:
while(условие){
// команды — если условие истинно
}
У оператора while() есть брат-близнец. Это оператор dowhile(). Обра-
тимся к синтаксису этого оператора:
do{
// команды — если условие истинно
}while(условие);
От оператора while() оператор dowhile() отличается тем, что сначала
выполняется блок команд в теле оператора (в фигурных скобках между
ключевыми словами do и while) и только после этого проверяется условие.
Если условие истинно, снова выполняются команды в теле оператора цик-
ла, и так до достижения значения false в условии.
ПРИМЕЧАНИЕ Таким образом, если мы используем оператор цикла do-while(), ко-
манды тела цикла будут выполнены по крайней мере один раз, чего
нельзя сказать об операторе while().
Последовательность выполнения оператора цикла dowhile() отмечена
в структурной диаграмме на рис. 3.6.
114
Глава 3. Основы синтаксиса языка C#
Рис. 3.6. Схема выполнения оператора цикла do-while() Но на этом операторы цикла не заканчиваются. На сцену выходит един-
ственный и неповторимый в своей непредсказуемости оператор цикла
for(). По сравнению со своими предшественниками, у этого оператора до-
статочно запутанный, хотя и стильный синтаксис:
for(инициализация;условие;изменение){
// команды — если условие истинно
}
Хотя все основное действо и разворачивается обычно в теле оператора цик-
ла, принципиальное значение имеет структура (или «начинка») for-блока.
Вот некоторые правила, о которых следует помнить при работе с операто-
ром цикла for().
В круглых скобках после ключевого слова for размещается три блока
команд. Каждый блок разделяется точкой с запятой.
Блоки могут быть пустыми. Если блок состоит из нескольких команд, такие команды внутри блока разделяются запятыми.
Непосредственно команды тела оператора цикла размещаются в фигур-
ных скобках после for-блока (имеется в виду ключевое слово for и три
блока команд в круглых скобках). Если тело цикла состоит из одной
команды, фигурные скобки можно не использовать.
В начале выполнения оператора цикла выполняются команды первого
блока. Этот блок обычно называется блоком инициализации и выпол-
няется только один раз.
После выполнения первого блока (блока инициализации) проверя-
ется условие во втором блоке. Этот блок называют блоком условия.
Основные управляющие инструкции 115
Условие — выражение логического типа. Если условие равно true, выполняются команды из тела оператора цикла (команды в фигурных
скобках). Если условие равно false, работа оператора цикла завершает-
ся. Если второй блок пуст, по умолчанию условие считается истинным
(значение true).
После выполнения команд тела оператора цикла выполняются команды
в третьем блоке for-инструкции. Третий блок обычно называют блоком
изменения (или инкремента/декремента), поскольку обычно в этом блоке
размещают команды для изменения значения индексной переменной.
После выполнения команд третьего блока проверяется условие. Если
условие истинно (значение true), выполняются команды тела оператора
цикла. Если условие ложно (значение false), работа оператора цикла
завершается.
Схема выполнения оператора цикла for() проиллюстрирована на рис. 3.7.
Для удобства и разрешения неоднозначных ситуаций линии, определяю-
щие последовательность выполнения блоков, экипированы стрелками.
Рис. 3.7. Схема выполнения оператора цикла for()
Даже из изложенного выше становится совершенно очевидно, что опера-
тор цикла for() допускает огромное количество способов вызова. Причем
многие из них имеют не только смысл, но и оправданы с практической
точки зрения. Некоторые из таких вариантов мы рассмотрим чуть позже.
А сейчас несколько слов хочется посвятить многострадальной инструкции
безусловного перехода goto.
Инструкция goto позволяет передавать управление определенному месту
в программе. Это место определяется с помощью метки. Синтаксис вызова
116
Глава 3. Основы синтаксиса языка C#
инструкции такой: goto метка. Здесь метка является идентификатором, с помощью которого помечается программный код. Меткой может быть
любой допустимый синтаксисом C# (незарезервированный) идентифика-
тор. Метку не нужно как-то описывать, она просто размещается в коде. По-
сле метки ставится двоеточие. В принципе это все, что касается инструкции
goto и меток. Почему мы рассматриваем эту инструкцию здесь? Потому
что с помощью простой метки и не менее простой инструкции goto можно, кроме прочего, организовать оператор цикла. Правда, придется привлечь
еще и условный оператор, но это уже мелочи.
На этом краткий теоретический обзор управляющих инструкций языка
программирования C# можно заканчивать. Памятуя о том, что практика
есть лучший критерий истины, реализуем наши познания в программных
кодах.
В C# есть еще один оператор цикла foreach(), который в основном ис-
пользуется с массивами (да и то не со всеми). Поэтому до конца завесу
приоткрывать не будем. Подождем, пока на сцене появятся массивы.
Кроме того, не следует сбрасывать со счетов систему обработки
исключительных ситуаций (блок try-catch), с которой мы неожи-
данно столкнулись в первой главе. Хотя напрямую к управляющим
инструкциям эта алхимия не относится, умело используя систему от-
слеживания (и генерирования!) ошибок можно добиваться воистину
удивительных эффектов — куда там управляющим инструкциям!
Как иллюстрацию в использовании наших новых знакомых (имеются
в виду управляющие инструкции) рассмотрим пример, приведенный в ли-
стинге 3.1. Программа достаточно простая:
Листинг 3.1. Знакомство с управляющими инструкциями
using System;
// Класс с методами для вычисления
// суммы натуральных чисел:
class Summator{
// Поле определяет количество слагаемых:
int n;
// Конструктор класса (с одним аргументом):
public Summator(int n){
// Проверка выхода аргумента за
// пределы диапазона от 1 до 100:
if(n>100){ // Проверка условия
// Если аргумент больше 100:
Console.WriteLine("Слишком большое число! Изменено на 100.");
Основные управляющие инструкции 117
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 // Изменение индексной переменной // (для подсчета циклов): i++; // Изменение переменной для подсчета суммы: s+=i; } // Результат метода: return s; } // Вычисление суммы с помощью оператора do-while: int useDoWhile(){ // Сообщение о том, какой оператор используется: Console.Write("Используем оператор do-while. "); // Индексная переменная и переменная // для вычисления суммы: int i=0,s=0; // Оператор цикла do-while: do{ продолжение 118 Глава 3. Основы синтаксиса языка C# Листинг 3.1 (продолжение) // Изменение индексной переменной // (для подсчета циклов): i++; // Изменение переменной для подсчета суммы: s+=i; }while(i // Результат метода: return s; } // Вычисление суммы с помощью оператора for: int useFor1(){ // Сообщение о том, какой оператор используется: Console.Write("Используем оператор for (первый вариант). "); // Индексная переменная и переменная // для вычисления суммы: int i,s=0; // Оператор цикла: for(i=1;i<=n;i++){ // Изменение переменной для подсчета суммы: s+=i; } // Результат метода: return s; } // Вычисление суммы с помощью оператора for: int useFor2(){ // Сообщение о том, какой оператор используется: Console.Write("Используем оператор for (второй вариант). "); // Индексная переменная и переменная // для вычисления суммы: int i=0,s=0; // Оператор цикла for с двумя пустыми блоками: for(;i // Изменение индексной переменной: i++; // Изменение переменной для подсчета суммы: s+=i; } // Результат метода: return s; } // Вычисление суммы с помощью оператора for: int useFor3(){ // Сообщение о том, какой оператор используется: Console.Write("Используем оператор for (третий вариант). "); Основные управляющие инструкции 119 // Индексная переменная и переменная // для вычисления суммы: int i,s; // Оператор цикла for с пустым телом: for(i=1,s=0;i<=n;s+=i++); // Результат метода: return s; } // Вычисление суммы с помощью оператора for: int useFor4(){ // Сообщение о том, какой оператор используется: Console.Write("Используем оператор for (четвертый вариант). "); // Индексная переменная и переменная // для вычисления суммы: int i=1,s=0; // Оператор цикла for с пустыми блоками: for(;;){ // Изменение индексной переменной // и переменной для вычисления суммы: s+=i++; // Условный оператор для проверки условия // выхода из цикла: if(i>n) break; } // Результат метода: return s; } // Вычисление суммы с помощью инструкции goto: int useGoto(){ // Сообщение о том, какой оператор используется: Console.Write("Используем инструкцию goto. "); // Индексная переменная и переменная // для вычисления суммы: int i=1,s=0; // Метка: start: // Изменение переменной для вычисления суммы: s+=i; // Изменение индексной переменной: i++; // Условный оператор для перехода к метке: if(i<=n) goto start; // Результат метода: return s; } продолжение 120 Глава 3. Основы синтаксиса языка C# Листинг 3.1 (продолжение) // Метод для отображения результата // вычислений выбранным методом: public void show(char choice){ // Отображение символьного аргумента метода: Console.Write(choice+") "); // Переменная для вычисления суммы: int res; // Оператор выбора: switch(choice){ case 'A': res=useWhile(); break; case 'B': res=useDoWhile(); break; case 'C': res=useFor1(); break; case 'D': res=useFor2(); break; case 'E': res=useFor3(); break; case 'F': res=useFor4(); break; default: res=useGoto(); break; } // Отображаем результат: Console.WriteLine("Результат: "+res); } } // Класс с главным методом программы: class SummatorDemo{ // Главный метод программы: public static void Main(){ // Объектная переменная: Summator obj; // Оператор цикла с целочисленной // индексной переменной: for(int i=-25;i<160;i+=50){ // Создание нового объекта: Основные управляющие инструкции 121 obj=new Summator(i); // Оператор цикла с символьной // индексной переменной: for(char s='A';s<'H';s++){ // Отображение результата // вычислений выбранным методом: obj.show(s); } // Переход к новой строке: Console.WriteLine(); } // Ожидание нажатия клавиши Enter: Console.ReadLine(); } } Основу нашей программы составляет класс Summator, предназначенный для решения исключительно банальной задачи — вычисления суммы натураль- ных чисел. У класса есть закрытое целочисленное поле n, значение которо-