Лекция 1.2 Типы и классы. Переменные и объекты

Типы и классы. Переменные и объекты
Общий взгляд
Знакомство с новым языком программирования разумно начинать с изучения системы типов этого языка. Как в нем устроена система типов данных? Какие есть простые типы, как создаются сложные, структурные типы, как определяются собственные типы, динамические типы. Для объектно-ориентированных языков программирования важно понимать, как связаны между собой такие близкие по духу понятия, как понятие типа и понятие класса.
В первых языках программирования понятие класса отсутствовало – рассматривались только типы данных. При определении типа явно задавалось только множество возможных значений, которые могут принимать переменные этого типа. Например, утверждалось, что тип integer задает целые числа в некотором диапазоне. Неявно с типом всегда связывался и набор разрешенных операций.
В строго типизированных языках, к которым относится большинство языков программирования, каждая переменная в момент ее объявления связывалась с некоторым типом. Связывание переменной x с типом Т означало, что переменная x может принимать только значения из множества, заданного типом T, и к ней применимы операции, разрешенные этим типом. Таким образом, тип определял, говоря современным языком, свойства и поведение переменных. Значение переменной задавало ее свойства, а операции над ней — ее поведение, — то, что можно делать с этой переменной.
Классы и объекты впервые появились в программировании в языке Симула 67. Произошло это спустя 10 лет после появления первого алгоритмического языка Фортран. Определение класса наряду с описанием данных уже тогда содержало четкое определение операций или методов, применимых к данным. Классы стали естественным обобщением понятия типа, а объекты – экземпляры класса — стали естественным обобщением понятия переменной. Сегодня определение класса в C# и других объектных языках содержит:
поля, синтаксически представляющие объявления переменных и задающие свойства объектов класса;
методы, определяющие поведение объектов класса;
события, которые могут происходить с объектами класса.
Так есть ли различие между основополагающими понятиями – типом и классом, переменной и объектом? Такое различие существует. Программистам нужны все эти понятия. Но определить это различие не так-то просто. Мне до сих пор не удается точно описать все ситуации, в которых следует использовать только понятие тип, и ситуации, в которых приемлемо только использование понятия класса. Во многих ситуациях эти понятия становятся синонимичными. Если, например, есть объявление <T x;>, то можно говорить, что объявлена переменная типа T, но с тем же успехом справедливо утверждение, что данное объявление задает объект x класса T. На первых порах можно считать, что класс – это хорошо определенный тип данных, объект – хорошо определенная переменная.
Есть традиционные предпочтения. Базисные встроенные типы, такие как int или string, предпочитают называть по-прежнему типами, а их экземпляры – переменными. Когда же речь идет о создании собственных типов, моделирующих, например, такие абстракции данных как множество автомобилей или множество служащих, то естественнее говорить о классах Car и Employee, а экземпляры этих классов называть объектами.
Объектно-ориентированное программирование, доминирующее сегодня, построено на классах и объектах. Тем не менее, понятия типа и переменной все еще остаются центральными при описании языков программирования, что характерно и для языка C#. Заметьте, что в справочной системе Framework .Net предпочитают говорить о системе типов, хотя все типы библиотеки FCL являются классами.
Типы данных принято разделять на простые и сложные в зависимости от того, как устроены их данные. У простых (скалярных) типов возможные значения данных едины и неделимы. Сложные типы характеризуются способом структуризации данных — одно значение сложного типа состоит из множества значений данных, организующих сложный тип.
Есть и другие критерии классификации типов. Так, типы разделяются на встроенные типы и типы, определенные программистом (пользователем). Встроенные типы изначально принадлежат языку программирования и составляют его базис. В основе системы типов любого языка программирования всегда лежит базисная система типов, встроенных в язык. На их основе программист может строить собственные, им самим определенные типы данных. Но способы (правила) создания таких типов являются базисными, встроенными в язык. Принято считать, что встроенными в язык C# являются арифметические типы, булевский и строковый тип, что в язык встроен механизм построения массивов из переменных одного типа. Эти встроенные типы будем называть базисными. Базисные встроенные типы должны быть реализованы любым компилятором, отвечающим стандарту языка C#.
Язык C#, рассматриваемый в данном курсе изначально предполагает реализацию, согласованную с Framework .Net. Это означает, что все базисные встроенные типы проецируются на соответствующие типы библиотеки FCL. Библиотека FCL реализует базис языка C#. Но помимо этого она предоставляет в распоряжение программиста множество других полезных типов данных. Так что для нашей реализации языка C# встроенных типов огромное число – вся библиотека FCL. Знать все типы из этой библиотеки практически невозможно, но умение ориентироваться в ней необходимо.
Типы данных разделяются также на статические и динамические. Для переменных статического типа память под данные отводится в момент объявления, требуемый размер данных известен при их объявлении. Для динамического типа размер данных в момент объявления обычно не известен и память им выделяется динамически в процессе выполнения программы. Многие динамические типы, доступные разработчикам проектов на C#, реализованы как встроенные типы в библиотеке FCL. Например, в пространстве имен этой библиотеки System.Collections находятся классы Stack, Queue, ListArray и другие классы, описывающие широко распространенные динамические типы данных – стек, очередь, список, построенный на массиве.
Возможно, наиболее важным для C# программистов является деление типов на значимые и ссылочные. Для значимых типов, называемых также развернутыми, значение переменной (объекта) является неотъемлемой собственностью переменной (точнее, собственностью является память, отводимая значению, само значение может изменяться). Значимый тип принято называть развернутым, подчеркивая тем самым, что значение объекта развернуто непосредственно в памяти, отводимой объекту.
Для ссылочных типов значением служит ссылка на некоторый объект в памяти, расположенный обычно в динамической памяти – «куче». Объект, на который указывает ссылка, может быть разделяемым. Это означает, что несколько ссылочных переменных могут указывать на один и тот же объект и разделять его значения. О ссылочных и значимых типах еще предстоит обстоятельный разговор.
Для большинства процедурных языков, реально используемых программистами, – Паскаль, C++, Java, Visual Basic, C#, — базисная система встроенных типов более или менее одинакова. Всегда в языке присутствуют арифметический, логический (булев), символьный типы. Арифметический тип всегда разбивается на подтипы. Всегда допускается организация данных в виде массивов и записей (структур). Внутри арифметического типа всегда допускаются преобразования, всегда есть функции, преобразующие строку в число и обратно. Так что знание, по крайней мере, одного из процедурных языков позволяет построить общую картину базисной системы типов и для языка C#. Язык C# многое взял от языка C++, системы базисных типов этих двух языков близки и совпадают вплоть до названия типов и областей их определения. Но отличия, в том числе принципиального характера, есть и здесь.
Система типов
Давайте рассмотрим, как устроена система типов в языке C#. Во многом это устройство заимствовано из языка С++. Стандарт языка C++ включает следующий набор фундаментальных типов.
Логический тип (bool).
Символьный тип (char).
Целые типы. Целые типы могут быть одного из трех размеров – short, int, long, сопровождаемые описателем signed или unsigned, который указывает, как интерпретируется значение, – со знаком или без оного.
Типы с плавающей точкой. Эти типы также могут быть одного из трех размеров – float, double, long double.
Кроме того, в языке есть
Тип void, используемый для указания на отсутствие информации.
Язык позволяет конструировать типы
Указатели (например, int* – типизированный указатель на переменную типа int).
Ссылки (например, double& – типизированная ссылка на переменную типа double).
Массивы (например, char[] – массив элементов типа char).
Язык позволяет конструировать пользовательские типы
Перечислимые типы (enum) для представления значений из конкретного множества.
Структуры (struct)
Классы (class).
Первые три вида типов называются интегральными или счетными. Значения их перечислимы и упорядочены. Целые типы и типы с плавающей точкой относятся к арифметическому типу. Типы подразделяются также на встроенные и типы, определенные пользователем.
Эта схема типов сохранена и в языке C# и ее следует знать. Однако здесь на верхнем уровне используется и другая классификация, носящая для C# принципиальный характер. Согласно этой классификации все типы можно разделить на четыре категории:
типы-значения (value), или значимые типы;
ссылочные (reference);
указатели (pointer);
тип void.
Эта классификация основана на том, где и как хранятся значения типов. Переменные, или что тоже в данном контексте – объекты, хранят свои значения в памяти компьютера, которую будем называть памятью типа «Стек» (Stack). Другой вид памяти, также связанный с хранением значений переменной будем называть памятью типа «Куча» (Heap). Об особенностях этих двух видов памяти поговорим позже. Сейчас для нас важно понимать следующее. Для значимого типа значение переменной хранится непосредственно в стеке. Поскольку значение может быть сложным и состоять, например, из множества скалярных значений, то говорят, что значение разворачивается в стеке. По этой причине значимый тип называют также развернутым типом. Для ссылочного типа значение в стеке задает ссылку на область памяти в «куче», где хранятся собственно данные, задающие значение. Данные, хранящиеся в куче, в этом случае называют объектом, а значение, хранящееся в стеке, ссылкой на объект. Самое важное в этой модели хранения значений это то, что разные ссылки в стеке могут указывать на один и тот же объект из кучи. И тогда у этого объекта есть много разных имен (псевдонимов), каждое из которых позволяет получить доступ к полям объекта и изменять хранящиеся там значения.
В отдельную категорию выделены указатели, что подчеркивает их особую роль в языке. Указатели и ссылки в языке C# хотя и возможны, но в большинстве проектов используются редко. Отказ от этих средств делает программы более простыми, а самое главное более надежными. В нашем курсе эти средства практически появляться не будут. Ситуация похожа на ситуацию с оператором Goto, — оператор доступен в языке, но не рекомендован к использованию. Если программист C# действительно хочет применять эти средства, то ему придется предпринять для этого определенные усилия, поскольку указатели имеют ограниченную область действия и могут использоваться только в небезопасных блоках, помеченных как unsafe.
Особый статус имеет и тип void, указывающий на отсутствие какого-либо значения.
В языке C# жестко определено, какие типы относятся к ссылочным, а какие к значимым. Типы — логический, арифметический, структуры, перечисление — относятся к значимым типам. Массивы, строки и классы относятся к ссылочным типам. На первый взгляд, такая классификация может вызывать некоторое недоумение, почему это структуры относятся к значимым типам, а массивы и строки — к ссылочным. Однако ничего удивительного здесь нет. В C# массивы рассматриваются как динамические, их размер может определяться на этапе вычислений, а не в момент трансляции. Поэтому естественно хранить массивы в динамической памяти — куче, а не в статической памяти, каковой является стек, где размеры хранимых данных не меняются в процессе выполнения. Строки в C# также рассматриваются как динамические переменные, длина которых может изменяться. Поэтому строки и массивы относятся к ссылочным типам, требующим распределения памяти в куче.
Со структурами дело сложнее. Структуры C# представляют частный случай класса. Два объявления типа данных могут отличаться лишь одним ключевым словом, начинающим это объявление – class или struct. В зависимости от того, какое ключевое слово использовано, данное объявление будет задавать класс (ссылочный тип) или структуру (значимый тип). Определив тип как структуру, программист получает возможность отнести класс к значимым типам, что иногда бывает крайне полезно. Замечу, что в хорошем объектном языке Eiffel программист может любой класс объявить развернутым (expanded), что эквивалентно отнесению к значимому типу. У программиста C# только благодаря структурам появляется возможность управлять отнесением класса к значимым или ссылочным типам. Правда, это не совсем полноценное средство, поскольку на структуры накладываются дополнительные и довольно жесткие ограничения по сравнению с обычными классами. В частности, для структур разрешено только наследование интерфейсов и структура не может иметь в качестве родителя класс или структуру. Все развернутые типы языка C# — int, double и прочие реализованы как структуры.
Все базисные встроенные типы C# однозначно отображаются, а фактически совпадают с системными типами каркаса Net Framework, размещенными в пространстве имен System. Поэтому всюду, где можно использовать имя типа, например, – int, с тем же успехом можно использовать и имя – System.Int32.
Замечание: Следует понимать тесную связь и идентичность базисных встроенных типов языка C# и типов каркаса. Какими именами типов следует пользоваться в программных текстах – это спорный вопрос. Джеффри Рихтер в своей известной книге «Программирование на платформе Framework .Net» рекомендует использовать системные имена. Другие авторы считают, что следует пользоваться именами типов, принятыми в языке. Возможно, в модулях, предназначенных для межъязыкового взаимодействия, разумны системные имена, а в остальных случаях – имена конкретного языка программирования.
В заключение этого раздела приведу таблицу, содержащую описание базисных встроенных типов языка C# и их основные характеристики.

Таблица 2_1. Базисные встроенные типы языка C#

Логический тип
Имя типа Системный тип Значения Размер
bool System.Boolean true, false 8 бит
Арифметические целочисленные типы
Имя типа Системный тип Диапазон Размер
sbyte System.SByte [-128, 127] Знаковое, 8-бит
byte System.Byte [0, 255] Беззнаковое, 8-бит
short System.Short [-32768, 32767] Знаковое, 16-бит
ushort System.UShort [0, 65535] Беззнаковое, 16-бит
int System.Int32 [-231, 231] Знаковое, 32-бит
uint System.UInt32 [0, 232] Беззнаковое, 32-бит
long System.Int64 [-263 , 263] Знаковое, 64-бит
ulong System.UInt64 ≈(0, 264) Беззнаковое, 64-бит
Арифметический тип с плавающей точкой
Имя типа Системный тип Диапазон (по модулю) Точность
float System.Single [10-45, 1038] 7 цифр
double System.Double [10-324, 10308] 15-16 цифр
Арифметический тип с фиксированной точкой
Имя типа Системный тип Диапазон (по модулю) Точность
decimal System.Decimal [10-28, 1028] 28-29 значащих цифр
Символьные типы
Имя типа Системный тип Диапазон Точность
char System.Char [U+0000, U+ffff] 16-бит Unicode символ
string System.String Строка из символов Unicode
Объектный тип
Имя типа Системный тип Примечание
object System.Object Прародитель всех встроенных и пользовательских типов

Система базисных встроенных типов языка C# не только содержит практически все встроенные типы (за исключением long double) стандарта языка C++, но и перекрывает его разумным образом. В частности тип string является встроенным в язык, что вполне естественно. В области совпадения сохранены имена типов, принятые в C++, что облегчает жизнь тем, кто привык работать на C++, но уже перешел на язык C#.
Переменные, объекты и сущности
Уже говорилось о том, что одна из главных ролей, которую играют классы в ОО-языках, это роль типа данных. Класс, рассматриваемый как тип данных, задает описание свойств, поведения и событий некоторого множества элементов, называемых экземплярами класса, а чаще переменными или объектами. Заметьте, класс – это описание, это текст – статическая конструкция. Чтобы программа могла выполняться, в ней должны быть определены переменные или, что тоже, объекты класса, создаваемые динамически в ходе выполнения программы. Напомню, в первой главе объяснялось, что в начальный момент работы программы – в момент «большого взрыва» создается первый объект, который становится текущим объектом и начинает работать метод Main – точка входа в программу. Все дальнейшее зависит от содержания метода Main, — какие новые объекты создаются, какие методы и свойства вызываются этими объектами.
В строго типизированных языках всякая переменная до ее использования должна быть явно объявлена в программе. В момент объявления должно указываться имя переменной и тип, которому принадлежит эта переменная. Тип, задаваемый в момент объявления, называется базовым типом этой переменной.
В этом тексте вместо переменных можно говорить об объектах, вместо типов — о классах. Мало того, что у нас уже есть два слабо различимых понятия – объект и переменная, я введу еще одно близкое понятие – понятие сущности, понимая под сущностью то имя, которое появляется непосредственно в тексте программы. У сущности есть базовый тип, — тот тип, который указан для сущности в момент ее объявления.
В отличие от сущности, заданной именем, объекты и переменные – это уже динамически созданные реалии, которым отведена память. В процессе работы программы сущность связывается с физически создаваемым в стеке или в куче объектом (переменной). Объект, созданный в памяти компьютера и связанный с сущностью, может иметь тип согласованный, но не обязательно совпадающий с базовым типом сущности.
Синтаксис объявления
Неформально уже отмечалось, что в момент объявления переменной указывается ее тип, имя и, возможно, значение. Давайте попробуем выразить это более формально. Синтаксис объявления сущностей в C# может быть задан следующей синтаксической формулой:
[<атрибуты>] [<модификаторы>] <тип> <список объявителей>;
В синтаксических формулах используется широко распространенный метаязык, известный еще со времен описания синтаксиса языка Алгол. В угловые скобки заключаются синтаксические понятия языка. В квадратные скобки заключаются необязательные элементы формулы.
О необязательных элементах этой формулы — атрибутах и модификаторах будем говорить по мере необходимости, пока же будем считать, что эти элементы при объявлении сущности опущены. Список объявителей позволяет в одном объявлении задать несколько переменных одного типа. Терминальный символ «запятая» служит разделителем элементов списка. Элемент списка объявитель задается следующей синтаксической формулой:
<имя> | <имя> = <инициализатор>
Когда задается просто имя переменной, то такое объявление называется объявлением без инициализации. Во втором случае переменная не только объявляется, но и инициализируется ее начальное значение.
О типах языка C# мы уже кое-что знаем, так что готовы понять, как строятся объявления сущностей в простых случаях.
Объявления простых переменных
Если тип в синтаксической формуле задавать именем типа из таблицы 2_1, то это означает, что объявляются простые скалярные переменные. Объявления переменных сложных типов – массивов, перечислений, структур и других типов, определяемых программистом, будут изучаться в последующих лекциях.
Как строится инициализатор для простых переменных? Инициализатор, как чаще всего и бывает, задается некоторым выражением — обычно константой, но может быть достаточно сложным выражением, допускающим вычисление на этапе компиляции программы и зависящим от ранее инициализированных переменных. Инициализатор может быть задан и в объектном стиле. В этом случае он представляет вызов конструктора объектов соответствующего типа. Синтаксическая формула для инициализатора в этом случае имеет вид:
new <имя конструктора>([<список аргументов>])
Ключевое слово new в данном контексте говорит, что создается новый объект, и для его создания вызывается конструктор объектов соответствующего класса. Такая конструкция инициализатора, применимая и для скалярных переменных базисных типов, подчеркивает, что в C# все переменные как значимых, так и ссылочных типов, простые и сложные, являются настоящими объектами соответствующих классов.
Хороший стиль программирования требует, чтобы все простые переменные объявлялись с инициализацией. Дело в том, что при объявлении без инициализации значение переменной остается неопределенным, и всякая попытка использовать такую переменную в вычислениях, где требуется значение переменной, является ошибкой, которую компилятор обнаруживает еще на этапе компиляции. Конечно, можно объявить переменную без инициализации, а потом в процессе вычислений присвоить ей значение, тем самым инициализируя ее.
Заметьте, компилятор строго следит за тем, чтобы в вычислениях не появлялись переменные, не имеющие значения. Если присвоение значения переменной происходит внутри одной из ветвей оператора if или в теле оператора цикла, то компилятор предпочитает сигнализировать об ошибке в подобных ситуациях. Он ориентируется на худший случай, поскольку не может на этапе компиляции разобраться, будет ли выполняться ветвь с инициализацией, будет ли выполняться тело цикла или не будет.
Типы, допускающие неопределенные значения
Инициализация переменных позволяет в момент объявления каждой переменной дать значение, принадлежащее множеству возможных значений данного типа. Тем самым исключается возможность работы с неопределенными значениями переменных. Однако для ссылочных типов возможным значением объектов является значение null, задающее неопределенную ссылку. Конечно, попытка вызвать метод или свойство объекта со значением null не приведет к успеху и возникнет ошибка периода выполнения. Тем не менее, полезно для ссылочных типов иметь null в качестве возможного значения.
Для значимых типов значение null не входит в множество возможных значений. Но в ряде ситуаций полезно, когда переменная значимого типа имела бы неопределенное значение. Язык C# позволяет из любого значимого типа данных построить новый тип, отличающийся лишь тем, что множество возможных значений дополнено специальным значением null. Так построенные типы данных называются типами, допускающими неопределенное значение (Nullable Types). Если построен тип T, то тип, допускающий неопределенные значения, определяется следующим образом:
System.Nullable<T>
Чаще используется эквивалентная, но более простая форма записи –
T?
Переменные таких типов могут получать значение null либо в результате присваивания, либо в процессе вычислений. Понятно, что если при вычислении выражения один из операндов будет иметь значение null, то и результат вычисления выражения будет иметь то же значение null. Над переменными этого типа определена специальная операция склеивания:
A ?? B
Результатом вычисления этого выражения будет операнд А, если значение А не равно null, и В, если первый операнд равен null.
Рассмотрим выполнение преобразований между типами Т? и Т. Очевидно, что преобразование из типа Т в тип Т? – безопасное преобразование, и потому может выполняться неявно. В другую сторону преобразование является опасным и должно выполняться явно, например, путем кастинга – приведения к типу. Рассмотрим некоторые примеры работы с переменными подобных типов.
static void TestNullable()
{
int x = 3, y = 7;
int? x1 = null, y1, z1;
y1 = x + y;
z1 = x1 ?? y1;
Console.WriteLine(«x1 = {0}, y1 = {1}, z1 = {2}»,
x1, y1, z1);
В этом фрагменте вводятся переменные типа int? и int. Демонстрируется безопасное преобразование из типа int в тип int? и выполнение операции ?? – операции склеивания. Рассмотрим следующий фрагмент тестового метода:
//x = (int)x1;
y = (int)y1;
Console.WriteLine(«x = {0}, y = {1}»,
x, y);
z1 = x1 * y ?? y1;
y1 = z1 — y1;
Console.WriteLine(«x1 = {0}, y1 = {1}, z1 = {2}»,
x1, y1, z1);
Первая строка фрагмента закомментирована, поскольку попытка явного приведения типа переменной со значением null приведет к ошибке периода выполнения. В следующей строчке такое приведение успешно выполняется, поскольку переменная y1 имеет значение, допустимое для типа int. Заметьте, что операция ?? имеет более низкий приоритет, чем операция умножения, поэтому первый операнд этой операции будет иметь значение null и z1 получит значение y1. В следующем фрагменте демонстрируются оба эквивалентных способа задания типа double, допускающего неопределенные значения:
System.Nullable<double> u = x + x1;
double? v = y + y1, w;
w = u ?? v + y1;
Console.WriteLine(«u = {0}, v = {1}, w = {2}»,
u, v, w);
В заключение взгляните на результаты работы этого тестового примера:
Рис. 1_2.1 Типы, допускающие значение null
Заметьте, при выводе на печать значение null не имеет специального обозначения и выводится как пробел.
Null, NaN и Infinity
Значение null не является единственным особым значением, входящим в множество возможных значений значимого типа. У вещественного типа данных (double и float) есть и другие особые значения, не являющиеся обыкновенными числами. Таких значений три – Infinity, NegativeInfinity и NaN. Первые два хорошо известны из математики – это бесконечность и отрицательная бесконечность. Третье значение NaN (Not a Number) появляется тогда, когда результат не является вещественным числом или значением null и Infinity. Рассмотрим правила, приводящие к появлению особых значений.
Если при выполнении операций умножения или деления результат по модулю превосходит максимально допустимое число, то значением является бесконечность или отрицательная бесконечность в зависимости от знака результата. У типов double и float есть константы, задающие эти значения. При выполнении операций сложения, вычитания и умножения бесконечности с обычными вещественными числами результат имеет значение бесконечность, возможно с противоположным знаком. При делении вещественного числа на бесконечность результат равен нулю.
Если один из операндов вычисляемого выражения есть null, а остальные – обычными вещественными числами или бесконечностью, то результат выражения будет иметь значение null – не определен.
Если бесконечность делится на бесконечность, или ноль умножается на бесконечность, то результат будет NaN. Этот же результат будет появляться, когда результат выполнения некоторой операции не будет вещественным числом, например, при извлечении квадратного корня из отрицательного числа. Если NaN участвует в операциях, то результатом будет NaN. Это верно и тогда, когда другие операнды имеют значение null или бесконечность.
Рассмотрим примеры:
static void NullAndNan()
{
double? u = null, v = 0, w = 1.5;
Console.WriteLine(«u = {0}, v = {1}, w = {2}»,
u, v, w);
Пока что введены три переменные типа double?, одна из которых получила значение null. Введем еще несколько переменных этого же типа, которые получат в результате вычислений особые значения:
double? x, y, z;
x = u + v; y = w / v; z = x + y;
Console.WriteLine(«x = u + v = {0}, y = w / v = {1}, » +
» z = x + y = {2}», x, y, z);
При вычислении значения переменной x в выражении участвует null, поэтому и x получит значение null. При вычислении значения переменной y выполняется деление на ноль, поэтому y получит значение бесконечность. При вычислении значения переменной z в выражении участвует null и бесконечность, поэтому z получит значение null. Рассмотрим еще один фрагмент кода:
x = -y; y = v * y; z = x + y;
Console.WriteLine(«x = -y = {0}, y = v * y = {1}, » +
» z = x + y = {2}», x, y, z);
При вычислении значения переменной x происходит смена знака и x получает значение отрицательной бесконечности. При вычислении значения переменной y бесконечность умножается на ноль, результат не определен и будет иметь значение NaN. При сложении бесконечности со значением NaN результат будет NaN. Ну и еще один заключительный фрагмент:
double p = -(double)w, q = double.NegativeInfinity;
Console.WriteLine(«p = {0}, q = {1}, 1 / q = {2}»,
Math.Sqrt(p), q, 1 / q);
p = 1e160;
Console.WriteLine(«p = {0}, p * p = {1}», p, p * p);
float p1 = 1e20f;
Console.WriteLine(«p1 = {0}, p1 * p1 = {1}», p1, p1 * p1);
Здесь вводятся переменные типа double и float. Показано, как при умножении появляются значения бесконечность, а также появляется одна из соответствующих констант, задающих бесконечность. В заключение взгляните на результаты работы этого тестового примера:
Рис. 1_2.2 Значения null, NaN, Infinity

Программный проект SimpleVariables
Продолжу приводить примеры. И сейчас подробнее остановлюсь на том, как строилось Решение (Solution) с именем Ch2, в котором размещены проекты этой главы. Наряду с консольным проектом ConsoleNullable, примеры из которого приводились в предыдущем разделе, создадим Windows-проект с именем SimpleVariables. В этом проекте почти не будет никаких вычислений. Его главная задача — продемонстрировать различные способы объявления простых переменных встроенных базисных типов, простейшие присваивания и вывод значений переменных.
Побочная цель состоит в том, чтобы показать работу проекта, включающего несколько форм – интерфейсных классов. Проект продемонстрирует часто встречающуюся на практике ситуацию, когда в нем есть главная кнопочная форма с множеством командных кнопок, обработчики событий которых открывают формы, решающие специальные задачи.
Поскольку в первой главе подробно рассказывалось о том, как создаются Windows-проекты и первые шаги работы с ними, то останавливаться на этом уже не буду. На рис. 2_1 показана главная форма на начальном этапе проектирования.
Рис. 1_2.3 Главная форма на начальных шагах процесса ее проектирования
Что же было сделано при проектировании главной формы? Прежде всего, она была переименована, получив содержательное имя. Переименован был и файл, содержащий описание интерфейсного класса, задающего форму. На форме размещена поясняющая надпись «Главная форма» и несколько командных кнопок.
Для задания надписей, поясняющих назначение формы, можно использовать элемент управления Label или текстовое окно – TextBox. В нашем примере использовалась метка.
Главная кнопочная форма
Проектируемая главная форма является примером главной кнопочной формы. В каких ситуациях имеет смысл проектировать главную форму как главную кнопочную форму? Так поступают достаточно часто. Представьте себе, что создаваемый проект предоставляет конечному пользователю несколько различных сервисов, и пользователь, начиная работу с проектом, выбирает нужный ему сервис. Главная форма может иметь меню, команды которого и позволяют пользователю выбирать нужный ему сервис. Если каждый сервис достаточно сложен и требует собственного интерфейса, то в таких ситуациях вместо стандартного меню удобнее использовать главную кнопочную форму. Роль команд меню в ней играют расположенные в форме командные кнопки. Выбор командной кнопки на форме соответствует выбору команды меню.
Подведем итоги. Если в проекте предполагается n различных сервисов, каждый из которых требует собственного интерфейса, то в проект наряду с главной формой, создаваемой по умолчанию в момент создания проекта, добавляются n интерфейсных классов – наследников класса Form, каждый из которых имеет собственную форму. Каждая такая форма заселяется элементами управления, задавая интерфейс соответствующего сервиса. На главной форме располагаются n командных кнопок, а в код интерфейсного класса, задающего главную форму, добавляются n полей, каждое из которых содержит объявление объекта соответствующего интерфейсного класса. Когда пользователь выбирает в главной форме командную кнопку, то обработчик события Click этой кнопки, вызывает конструктор интерфейсного класса и создает реальный объект этого интерфейсного класса и реальную форму, связанную с объектом. Затем в обработчике вызывается метод Show этого объекта, соответствующая форма открывается, показывается на экране дисплея, и пользователь начинает работать с интерфейсом формы. Такая схема работы встречается на практике достаточно часто, так что проекты с главной кнопочной формой будут встречаться неоднократно.
В нашем проекте для демонстрации различных аспектов применения скалярных типов и объявления переменных этих типов роль сервисов, предоставляемых пользователю, будут играть три различных примера – три теста. Каждый тест имеет свою цель и свой интерфейс, позволяющий конечному пользователю проводить исследования возможностей и особенностей объявления скалярных переменных. Для каждого теста естественно будет создан свой интерфейсный класс и спроектирована форма, связанная с этим классом. Каждая командная кнопка главной формы будет запускать свой тест. Обработчик события Click каждой из командных кнопок будет открывать форму, спроектированную для работы с выбранным тестом нашего проекта.
Тест «Types» — ввод и вывод переменных различных типов
Какова идея этого теста? Давайте дадим конечному пользователю возможность в текстовых окнах формы задать имя скалярного типа и значение, соответствующее этому типу. После этого пользователь может нажать командную кнопку «Ввод», спроектированную в интерфейсе формы. Обработчик события Click командной кнопки «Ввод» должен построить переменную заданного типа и присвоить ей значение, заданное пользователем. Если корректно указано имя типа и значение, то операция пройдет успешно, о чем пользователю и будет выдано соответствующее сообщение. В качестве допустимых типов разрешается указывать любой допустимый в C# скалярный тип, заданный таблицей 2_1. В качестве допустимого значения разрешается указывать любое значение из диапазона, соответствующего выбранному типу, и представленному в таблице 2_1. В случае некорректной работы пользователя – неправильно указано имя типа, или, если значение не соответствует заданному типу, должно выдаваться сообщение об ошибке.
Спроектируем в интерфейсе пользователя и командную кнопку «Вывод», по нажатию которой будет выдаваться значение, хранимое в созданной переменной.
Тест должен позволить пользователю изучить все скалярные типы языка C#, понять, какие значения допустимы для каждого типа, увидеть, как можно вводить и выводить значения переменных скалярного типа.
Еще одна важная роль этого теста состоит в демонстрации правильной организации ввода данных с использованием программной конструкции try – catch блоков. Ввод данных, задаваемых пользователем, всегда должен контролироваться, поскольку человеку свойственно ошибаться. Тест показывает, как можно обнаруживать ошибки ввода.
В соответствии с задачами теста спроектируем интерфейс пользователя. Прежде всего, в проект нужно добавить новый интерфейсный класс, сопровождаемый формой. Такие классы являются наследниками класса Form из библиотеки FCL. Воспользуемся пунктом меню Project | AddWindowsForm для добавления в проект интерфейсного класса и соответственно новой формы. Отслеживая наши действия, инструментальное средство Designer Form добавит в наш проект необходимые классы. Следуя правилам стиля, произведем переименование, заменив стандартные имена содержательными.
Теперь можно заняться непосредственным проектированием пользовательского интерфейса, размещая в форме нужные элементы управления. Не буду в деталях описывать весь процесс проектирования, поскольку интерфейс достаточно прост. Он отвечает заданной функциональности, и включает известные элементы управления – метки, текстовые окна, командные кнопки и контейнеры GroupBox. Контейнеры в формах позволяют придать пользовательскому интерфейсу нужную структуру, объединяя в группу элементы управления, решающие общую задачу — например, ввода исходных данных или вывода результатов.
Проектирование интерфейса пользователя важная и самостоятельная задача. Она требует определенных дизайнерских качеств, поскольку интерфейс не только должен соответствовать требуемой функциональности, но должен быть интуитивно понятен и элегантен. К сожалению, дизайнер из меня неважный, так что «элегантного» интерфейса в примерах увидеть не удастся.
На рис. 1_2.4 показана спроектированная форма в процессе работы с ней.
Рис. 1_2.4 Форма TestTypes в процессе работы
Визуальное, событийно-управляемое программирование
Прежде чем продолжить рассмотрение теста, давайте поговорим об основных принципах, лежащих в основе современного визуального стиля программирования. На начальных этапах программирования работой программы полностью управлял ее текст. Так в программах на языке Алгол выполнение программы начиналось с оператора begin (начало) и заканчивалось оператором end (конец). Развитие программирования потребовало диалога с пользователем в ходе выполнения программы. Вначале это был простой диалог, характерный для уже рассмотренных нами консольных приложений, когда выполнение программы приостанавливается, и она ждет ответа пользователя, вводимого с консоли. Дальнейшее развитие программирования привело к визуальному стилю, когда программные объекты стали иметь визуальные образы, когда появились графические объекты. Графические образы более информативны, чем текстовые. Визуальный стиль изменил и стиль диалога с пользователем, позволив строить интерфейс пользователя, основанный на объектах, имеющих визуальный образ. Простейшим и классическим примером является объект класса TextBox, графическим образом которого является текстовое окно, которое может служить как для вывода текстов в окно, так и для ввода текста пользователем. Этот объект обладает мощной функциональностью текстового редактора, позволяя пользователю редактировать вводимый им текст, удаляя, заменяя и вставляя символы в произвольное место текста.
В библиотеке FCL имеется большое число классов, предназначенных для организации пользовательского интерфейса. Со многими из них будем знакомиться по ходу изучения нашего курса. Эти интерфейсные классы и соответствующие объекты получили название элементов управления (control). Класс Control из этой библиотеки обеспечивает базовую функциональность всех элементов управления и является родительским классом для всех интерфейсных классов. Класс Form, который неоднократно упоминался, также является непрямым наследником класса Control и прямым наследником класса ContainerControl. Последний класс определяет базовую функциональность тех элементов управления, которые, как и формы, являются контейнерами, допускающими внутри графического образа размещение образов других элементов управления. Кроме формы уже известным нам примером контейнера является класс GroupBox.
Уже говорилось, что проектирование пользовательского интерфейса, как правило, ведется не программным путем, хотя и такое возможно, а «руками» . В Visual Studio 2008 имеется специальная инструментальная панель с элементами управления, которые можно перетаскивать на форму в процессе проектирования, располагать эти элементы в нужном месте формы, менять их размеры, устанавливать их свойства, открываемые в окне свойств. Дизайнер формы транслирует все эти действия в соответствующий программный код.
Процедура Main, задающая точку входа в Windows-проект, открывает главную форму, и пользователь попадает в спроектированный для этой формы мир графических объектов. Теперь пользователь становится у руля управления ходом выполнения проекта. Пользователь является «возмутителем спокойствия» в этом мире объектов. Он начинает вводить тексты в текстовые окна, выбирать нужные ему элементы из открывающихся списков, нажимать командные кнопки и выполнять другие действия над элементами управления. Каждое такое действие пользователя приводит к возникновению соответствующего события у программного объекта. Так, когда пользователь изменяет текст в текстовом окне, у соответствующего объекта класса TextBox возникает событие TextChanged, при нажатии командной кнопки у объекта возникает событие Click, двойной щелчок по командной кнопке приводит к событию DblClick. В ответ на возникновение события объект посылает сообщение операционной системе. Обрабатывая очередь сообщений, операционная система отыскивает обработчик события, если объект предусмотрел таковой и передает ему управление.
Подключить к объекту обработчик события можно визуально в процессе проектирования элемента управления. В окне свойств элемента управления можно перейти к списку событий этого элемента и в этом списке выбрать (включить) нужное событие. На рис. 1_2.5 показан момент проектирования формы, когда для выбранного элемента управления buttonInput в окне его свойств отображается список возможных событий этого элемента.
Рис. 1_2.5 Список событий элемента управления buttonInput
Можно видеть, что для этого элемента включено событие Click. Заметьте, имя обработчика события строится из имен элемента управления и события, разделенных знаком подчеркивания. У каждого элемента управления есть одно основное событие, которое можно включить простым щелчком по элементу управления, например, для командных кнопок таким событием является событие Click.
При включении события интерфейсный класс автоматически дополняется специальным методом, содержащим заготовку обработчика события — заголовок метода с пустым его телом. Дополнить обработчик события содержательным кодом — задача программиста. В обработчике события программист волен предусмотреть самые разные действия: может изменять свойства других объектов, вызывать методы других объектов, создавать объекты, добавлять или удалять интерфейсные объекты, изменяя мир объектов пользовательского интерфейса. Большинство компьютерных игр – всяческие «стрелялки» — яркий пример такого стиля программирования.
С точки зрения объектного программирования обработчики событий – это специальные методы интерфейсных классов (события), особенность которых состоит в том, что они вызываются в нужный момент операционной системой, в ответ на возникновение события у соответствующего объекта, инициированного работой пользователя. Механизм делегатов, на котором основана работа с событиями в языке C#, будет подробно рассматриваться в соответствующем разделе нашего курса.
Стиль программирования, основанный на проектировании визуального пользовательского интерфейса, называется визуальным программированием. Стиль программирования, основанный на событиях и системных вызовах обработчиков событий, называется событийно — управляемым программированием (event — driven programming). Современный стиль является визуальным, событийно – управляемым стилем программирования.
Построение обработчиков событий
Перейдем к решению главной программистской задачи – созданию обработчиков события Click для кнопок «Ввод» и «Вывод». Наиболее интересным является обработчик события для кнопки «Ввод». Пользователь в тестовом окне задает имя скалярного типа. В ответ на его действия обработчик события должен создать объект этого типа. Возможное имя типа — это элемент конечного множества, перечисленного в таблице 2_1. Обработчик должен уметь выполнять работу, называемую в программировании «разбором случаев». Ему нужно понять, какой конкретный тип задал пользователь в текстовом окне и, следовательно, переменную какого типа ему нужно создать. В C# для разбора случаев есть специальный оператор switch, каждая case-ветвь которого задает один из возможных вариантов. В этой ветви создается объект типа, заданного пользователем, и этому объекту присваивается значение, введенное пользователем в текстовое окно значений.
Поскольку пользователь вводит значение как текст (тип string), то возникает необходимость преобразования значения – от типа string к типу, заданному пользователем. При выполнении этого преобразования возможны ошибки по самым разным причинам. Например, пользователь мог задать значение, не принадлежащее множеству возможных значений данного типа. Другая причина – пользователь может не знать, как представляются значения данного типа, использовать точку, а не запятую для данных типа float или double. Возможны просто банальные ошибки – опечатки, неверный символ и так далее. Пользователь человек, человеку свойственны ошибки, человек имеет право на ошибку. Задача обработчика события выявить ошибку, если она возникла, сообщить о ней, дать возможность пользователю исправить ошибку и продолжить нормальную работу. В языке C# для этих целей предусмотрен специальный механизм охраняемых блоков, который и будет продемонстрирован в данном примере.
Давайте рассмотрим построенный код той части интерфейсного класса, в которой находятся обработчики событий, использующие упомянутые механизмы. Краткое описание этих механизмов будет дано в этой главе. Начнем с описания полей и конструктора интерфейсного класса:
namespace SimpleVariables
{
public partial class FormTestTypes : Form
{
//fields
string strType = «»;
string strValue = «»;
string strResult = «»;

const string OK_MESSAGE =
«Операция ввода прошла успешно!»;
const string ERR_MESSAGE =
«Значение, заданное при вводе, не принадлежит типу «;
const string ERR_Type_MESSAGE =
«Неверно задан скалярный тип!»;
public FormTestTypes()
{
InitializeComponent();
textBoxType.Select();
}
Как правило, текстовым полям в интерфейсе класса ставятся в соответствие поля в интерфейсном классе, что облегчает обмен информацией между интерфейсными объектами и объектом, представляющим форму. Константы, являющиеся статическими полями класса, используются при выводе информационных сообщений. Приведу теперь код обработчика события Click командной кнопки buttonInput:
private void buttonInput_Click(object sender, EventArgs e)
{
strType = textBoxType.Text;
strValue = textBoxValue.Text;

//разбор вариантов
switch (strType)
{
case «byte»:
{
byte x;
try
{
x = Convert.ToByte(strValue);
textBoxResult.Text = OK_MESSAGE;
strResult = x.ToString();
}
catch (Exception)
{
textBoxResult.Text = ERR_MESSAGE + «byte!»;

}
break;
}
case «bool»:
{
bool x;
try
{
x = Convert.ToBoolean(strValue);
textBoxResult.Text = OK_MESSAGE;
strResult = x.ToString();
}
catch (Exception)
{
textBoxResult.Text = ERR_MESSAGE + «bool!»;
}
break;
}
case «decimal»:
{
decimal x;
try
{
x = Convert.ToDecimal(strValue);
textBoxResult.Text = OK_MESSAGE;
strResult = x.ToString();
}
catch (Exception)
{
textBoxResult.Text = ERR_MESSAGE + «decimal!»;

}
break;
}
case «object»:
{
object x;
try
{
x = strValue;
textBoxResult.Text = OK_MESSAGE;
strResult = x.ToString();
}
catch (Exception)
{
textBoxResult.Text = ERR_MESSAGE + «object!»;

}
break;
}
default :
{
textBoxResult.Text = ERR_Type_MESSAGE;
break;
}
}
Поскольку все case ветви оператора switch устроены одинаковым образом, то в данном тексте большинство ветвей опущено. Если имя типа, заданное пользователем в текстовом окне textBoxType совпадает с именем, указанным в соответствующей case ветви, то именно эта ветвь и начинает выполняться. По ее завершении оператором break завершает работу и оператор разбора случае switch. Если же пользователь задал «ошибочное» имя, то ни одна из case ветвей не сработает, и тогда управление передается последней default ветви этого оператора. Она устроена не так, как остальные ветви, — ее задача выдать сообщение о данной ошибке в текстовое окно, представляющее результаты выполнения операции.
Давайте на примере первой case ветви рассмотрим более подробно ее устройство. Еще до выполнения оператора switch обработчик события в поля класса strType и strValue читает информацию, записанную пользователем в соответствующие текстовые поля. Первая case ветвь сравнивает значение поля strType с возможным вариантом – byte. Если значения совпадают, то пользователь задал тип «byte», поэтому в этой ветви и объявляется переменная этого типа. Рассмотрим три оператора этой ветви:
x = Convert.ToByte(strValue);
textBoxResult.Text = OK_MESSAGE;
strResult = x.ToString();
Первый из этих операторов присваивает переменной x значение strValue, введенное пользователем в соответствующее текстовое окно. В момент присваивания значение из строкового типа преобразуется к типу byte, заданному пользователем. Это преобразование типа выполняется методом ToByte класса Convert. Следующий оператор выдает сообщение об успехе операции в текстовое окно, информирующее пользователя о результате выполнения операции ввода. Последний оператор тройки формирует значение поля strResult, преобразуя значение типа byte в значение строкового типа.
Два последних оператора этой тройки безопасны, при их выполнении никогда не может произойти ошибки, обусловленной программными причинами (конечно, всегда возможен аппаратный сбой). Но вот первый оператор нормально завершит свою работу только тогда, когда пользователь задаст значение из достаточно узкого диапазона – допустимое значение для типа byte должно быть целым числом от 0 до 255. Во всех остальных случаях преобразование строки к типу byte приведет к ошибке, и возникнет так называемая «исключительная ситуация», когда программа не может продолжать нормально выполняться. Как бы хорошо не была написана программа, избежать возникновения в ней исключительных ситуаций не удается. В данном случае причиной может быть действия пользователя, задавшего некорректное значение. Избежать ситуации нельзя, но можно ее предвидеть и корректно обработать, позволяя продолжить нормальный ход выполнения программы.
В нашем примере это удается сделать за счет того, что оператор, при выполнении которого возможно возникновение исключительной ситуации, помещен в охраняемый try блок. Следом за охраняемым блоком располагается catch блок, которому будет передано управление в случае возникновения исключительной ситуации. Если же try- блок нормально завершит свою работу, то catch — блок выполняться не будет.
Задача catch — блока проста – выдать сообщение об ошибке выполнения операции, указать ее причину и продолжить выполнение проекта, дав пользователю возможность исправить свою ошибку. В данном случае совершенно ясна причина, по которой могла возникнуть исключительная ситуация – неверно задано значение типа byte. Аппаратные сбои, весьма редкие в наше время, можно игнорировать.
Перейдем теперь к рассмотрению обработчика события Click командной кнопки buttonOutput. Он устроен совсем просто. После того как получила значение переменная x, объявленная в case ветви, это значение предусмотрительно было преобразовано в значение строкового типа и сохранено в переменной strResult. Поэтому обработчику события достаточно передать значение этой переменной в текстовое окно. Если при вводе переменной была допущена ошибка, то результатом вывода является пустая строка. Вот код этого обработчика:
private void buttonOutput_Click(object sender, EventArgs e)
{
if (textBoxResult.Text == OK_MESSAGE)
textBoxOutputValue.Text = strResult;
else textBoxOutputValue.Text = «»;
}
Исключения и охраняемые блоки. Первое знакомство
В этом примере мы впервые встретились с исключениями и охраняемыми try-блоками. Исключениям и способам их обработки посвящена отдельная лекция, но не стоит откладывать надолго знакомство со столь важным механизмом.
Начну с определений. Любая последовательность операторов программы, заключенная в фигурные скобки, образует блок. Блок, которому предшествует ключевое слово try, называется охраняемым блоком или try- блоком. Блок, которому предшествует конструкция catch(<catch параметр>), называется блоком перехватчиком исключения или catch- блоком.
Блоки играют важную роль в структуре программы. Синтаксически блок воспринимается как один оператор программы – составной оператор. Там, где по синтаксису должен быть один оператор, а содержательно необходима последовательность операторов, эта последовательность заключается в скобки, образуя блок. И синтаксис удовлетворен, и содержательная сторона не страдает. Поскольку каждый оператор внутри блока может быть в свою очередь блоком, то для блоков характерна вложенность.
Еще одна важная роль, которую играют блоки, состоит в том, что они ограничивают область действия объявления локальных переменных. В блоке может быть объявлена переменная и ее область действия распространяется от точки объявления до конца блока.
Ситуация, при которой выполнение программы прерывается из-за того что по каким-либо причинам она не может далее нормально выполняться, называется исключительной ситуацией. В языке C# предусмотрен специальный механизм обработки исключительных ситуаций, основанный на исключениях. В момент возникновения исключительной ситуации создается специальный объект, называемый исключением, характеризующий возникшую ситуацию.
В состав библиотеки FCL входит класс Exception, задающий базовые свойства и методы исключений, рассматриваемых как объекты. У класса Exception большое число потомков, каждый из которых описывает определенный тип исключения. При проектировании собственного класса зачастую следует создать и собственный класс исключений, описывающий исключения, которые могут возникать при работе с объектами собственного класса. Все классы исключений, в том числе и создаваемые программистом, должны быть потомками базового класса Exception.
Как показывает практика программирования, любая программа не гарантирует, что в процессе ее работы не возникнут какие-либо неполадки, в результате которых она не сможет выполнить свою задачу. Исключения являются нормальным способом уведомления об ошибках в работе программы. Возникновение ошибки в работе программы должно приводить к выбрасыванию исключения соответствующего типа, следствием чего является прерывание нормального хода выполнения программы и передача управления обработчику исключения – стандартному или предусмотренному самой программой.
Стандартный обработчик исключений предусмотрен операционной системой. Он завершает выполнение программы, выдавая соответствующую информацию о возникновении исключения. Стандартное описание возникшего исключения может быть непонятно конечному пользователю, не говоря уже о том, что завершение программы до получения нужного результата весьма нежелательно. В хорошо построенной программе она сама должна обрабатывать возникшие ошибки.
Если в некотором модуле предполагается возможность появления исключений, то разумно предусмотреть и их обработку. В этом случае в модуле создается охраняемый try-блок. Вслед за этим блоком следуют один или несколько блоков, перехватывающих исключения, – catch-блоков. Каждый catch-блок имеет формальный параметр класса Exception или одного из его потомков. Если в try-блоке возникает исключение типа T, то catch-блоки начинают конкурировать в борьбе за перехват исключения. Первый по порядку catch-блок, тип формального аргумента которого согласован с типом T – совпадает с ним или является его потомком – захватывает исключение и начинает выполняться; поэтому порядок написания catch-блоков небезразличен. Вначале должны идти специализированные обработчики. Универсальным обработчиком является catch-блок с формальным параметром родового класса Exception, согласованным с исключением любого типа T. Универсальный обработчик, если он есть, стоит последним, поскольку захватывает исключение любого типа. По сути, последовательность catch блоков соответствует схеме разбора случаев, применяемой в операторе switch.
Конечно, плохо, когда в процессе работы программы возникает исключение. Однако его появление еще не означает, что программа не сможет выполнить свой контракт. Исключение может быть нужным образом обработано, после чего продолжится нормальный ход вычислений приложения. Гораздо хуже, когда возникают ошибки в работе, не приводящие к исключениям. Тогда работа продолжается с неверными данными без исправления ситуации и даже без уведомления о возникновении ошибки.
Наш пример продемонстрировал первое применение охраняемых блоков и обработку возникающих исключений. В примере для всех охраняемых блоков использовался универсальный перехватчик исключений, поскольку причина возникновения исключения в охраняемом блоке однозначно определялась, так что в разборе случаев не было необходимости.
Контролируемый ввод данных
Пользователь, работающий с программой, имеет право на ошибку. Программист, создающий программу, такого права не имеет. Если из-за ошибки пользователя программа перестает работать, то в этом вина программиста, поскольку его программа не смогла во время обнаружить ошибку и дать возможность пользователю исправить ее. Ошибки пользователя чаще всего возникают, когда пользователь вводит исходные данные, необходимые программе. Поэтому любой пользовательский ввод должен контролироваться. Операторы, управляющие вводом, должны, как правило, размещаться в охраняемых блоках. Обнаружение ошибки ввода должно приводить к возникновению исключения, а обработчик этого исключения должен проинформировать об ошибке и попытаться исправить ситуацию.
В рассмотренном выше примере пользовательский ввод контролируется. Поэтому стоит еще раз проанализировать этот пример с позиций того, как организован контроль ввода данных, вводимых пользователем.
Тест 1. Объявления с инициализацией и без нее
Этот тест демонстрирует объявление переменных, часть из которых инициализируется в момент объявления. Пользователю в текстовом окне показывается некоторый фрагмент программы, содержащий объявления переменных и простейшие присваивания. Задача пользователя в текстовые окна, спроектированные для всех переменных фрагмента, ввести их значения, которые они получат в результате выполнения фрагмента. После чего он может нажать командную кнопку и сравнить введенные им результаты с теми, что дает реальное вычисление.
На рис. 1_2.6 показана спроектированная форма в процессе работы.
Рис. 1_2.6 Форма теста 1 в процессе работы
Давайте поговорим подробнее о демонстрируемом фрагменте кода:
int x, y;
y = 1;
double u = y + 0.5, v = u*u +2, w = u+v;
uint k = new uint(), l = k + 1;
int n = y + 1;
//int m = x + 1;
В первой строке объявляются две сущности с именами x и y типа int без инициализации. В результате этого объявления в стеке будут созданы две переменные (два объекта), им будет выделена нужного размера память под значения, но сами значения останутся неопределенными. Во второй строке кода в результате присваивания переменная y получит значение. В третьей строке кода объявляются с инициализацией три сущности с именами u, v, w типа double. В стеке будут созданы три переменные, им будет отведена память под значения. Поскольку на этапе компиляции инициализирующие выражения могут быть вычислены, то значения этих переменных будут определены.
В следующей строке кода объявляются с инициализацией две сущности с именами k и l типа uint. Для сущности k используется объектный стиль инициализации с вызовом конструктора объектов класса uint. Конструктор без параметров этого класса не только создает в стеке соответствующий объект, но и инициализирует его значением по умолчанию, равным нулю для этого типа. Переменная l в результате инициализации также получит значение.
Аналогичным образом в результате объявления в стеке будет создана и переменная с именем n, которая также получит значение. А вот попытка создать и инициализировать переменную m закончится ошибкой еще на этапе компиляции, поскольку выражение инициализатора ссылается на переменную x, значение которой все еще не определено. Поэтому соответствующий оператор кода закомментирован.
Вот еще один тест на эту же тему:
void SimpleVars()
{
//Объявления локальных переменных
int x, s; //без инициализации
int y =0, u = 77; //обычный способ инициализации
//допустимая инициализация
float w1=0f, w2 = 5.5f, w3 =w1+ w2 + 125.25f;
//допустимая инициализация в объектном стиле
int z= new int();
// Недопустимая инициализация.
//Конструктор с параметрами не определен
//int v = new int(77);

x=u+y; //теперь x инициализирована
if(x> 5) s = 4;
for (x=1; x<5; x++)s=5;
//Инициализация в if и for не рассматривается,
//поэтому s считается неинициализированной переменной
//Ошибка компиляции:
//использование неинициализированной переменной
//x = s;
}// SimpleVars
В первой строке объявляются переменные x, s с отложенной инициализацией. Последующие объявления переменных эквивалентны по сути, но демонстрируют два стиля инициализации – обычный и объектный. Обычная форма инициализации предпочтительнее не только в силу своей естественности, но она и более эффективна, поскольку в этом случае инициализирующее выражение может быть достаточно сложным, с переменными и функциями. На практике объектный стиль для скалярных переменных используется редко. Вместе с тем полезно понимать, что объявление с инициализацией int y =0 можно рассматривать как создание нового объекта (new) и вызова для него конструктора по умолчанию. При инициализации в объектной форме может быть вызван только конструктор по умолчанию, другие конструкторы с параметрами для базисных встроенных типов не определены. В примере закомментировано объявление переменной v с инициализацией в объектном стиле, приводящее к ошибке, где делается попытка дать переменной значение, передавая его конструктору в качестве параметра.
Откладывать инициализацию не стоит, как показывает пример с переменной s, объявленной с отложенной инициализацией. В вычислениях она дважды получает значение: один раз в операторе if, другой – в операторе цикла for. Тем не менее, при компиляции возникнет ошибка, утверждающая, что в присваивании x = s делается попытка использовать неинициализированную переменную s. Связано это с тем, что для операторов if и for на этапе компиляции не вычисляются условия, зависящие от переменных. Поэтому компилятор предполагает худшее – условия ложны, инициализация s в этих операторах не происходит. А за инициализацией наш компилятор следит строго, ты так и знай!
Переменные. Область видимости и время жизни
Давайте рассмотрим, где могут появляться объявления переменных, какую роль они играют в зависимости от уровня, на котором они объявлены. Рассмотрим такие важные характеристики переменных, как время их жизни и область видимости. Зададимся вопросом, как долго живут объявленные переменные и в какой области программы видимы их имена? Ответ зависит от того, где и как, в каком контексте объявлены переменные. В языке C# не так уж много возможностей для объявления переменных, пожалуй, меньше, чем в любом другом языке. Открою «страшную» тайну, – здесь вообще нет настоящих глобальных переменных. Их отсутствие не следует считать некоторым недостатком C#, это достоинство языка. Но обо всем по порядку.
Поля класса
Первая важнейшая роль переменных, – задавать свойства классов. В языке C#, как и в других ОО-языках, такие переменные называются полями (fields) класса. О классах и полях предстоит еще обстоятельный разговор, а сейчас сообщу лишь некоторые минимальные сведения, связанные с рассматриваемой темой.
Все переменные, объявленные на уровне класса при его описании, являются полями этого класса.
Поскольку класс, как уже многократно говорилось, задает описание типа данных, то поля класса задают представление этих данных. Необходимо крайне внимательно относиться к проектированию полей класса, — всякие «лишние» объявления на этом уровне крайне нежелательны.
Когда конструктор класса создает очередной объект – экземпляр класса, то он в памяти создает набор полей, определяемых классом, и записывает в них значения, характеризующие свойства данного конкретного экземпляра. Так что //Объявления локальных переменныхкаждый объект в памяти можно рассматривать как набор соответствующих полей класса со своими значениями. Заметьте, для классов, представленных структурой, объект создается аналогичным способом, но разворачивается в стеке.
Объекты в динамической памяти, с которыми не связана ни одна ссылочная переменная, становятся недоступными. Реально они оканчивают свое существование, когда сборщик мусора (garbage collector) выполнит чистку «кучи». Для значимых типов, к которым принадлежат экземпляры структур, жизнь оканчивается при завершении блока, в котором они объявлены.
Есть одно важное исключение. Некоторые поля могут жить дольше. Если при объявлении класса поле объявлено с модификатором static, то такое поле является частью модуля, связанного с классом и не входит в состав его экземпляров. Поэтому static-поля живут так же долго, как и сам класс. Более подробно эти вопросы будут обсуждаться при рассмотрении классов, структур, интерфейсов.
Наследование и поля
Наследование классов – это одно из важнейших отношений, существующих между классами одного проекта. О нем мы будем говорить подробно, а сейчас рассмотрим только одну сторону наследования – что происходит с полями классов в процессе наследования. Пусть класс В является наследником класса А. Тогда класс В наследует все поля класса А. Наследник не может ни удалить поле родительского класса, ни изменить его тип. Наследник может лишь добавить собственные поля к уже имеющимся полям родителя. Таким образом, объекты класса наследника обладают всеми свойствами родителя и возможно дополнительным набором свойств.
Как создаются объекты класса наследника? Конструктор этого класса первым делом вызывает конструктор родителя и тот создает объект родителя – коробочку с набором родительских полей. Только после этого конструктор наследника добавляет, если они есть, собственные поля к уже созданному объекту.
Все сказанное относится лишь к «настоящим» классам, представляющим ссылочный тип. Для развернутых классов, заданных структурой, отношение наследования не определено. Структуры могут иметь в качестве родительских классов лишь интерфейсы.
Область видимости полей класса
Поля класса являются глобальными переменными класса. Они видимы во всех методах этого класса. Каждый метод класса может читать и изменять значение любого поля класса независимо от того, какие атрибуты доступа установлены для полей и методов класса.
Если в теле метода объявлена локальная переменная, имя которой совпадает с именем поля класса, то такая ситуация не приводит к ошибке, поскольку конфликт имен разрешим. К полю классу можно добраться, используя уточненное имя поля с префиксом this, задающим имя текущего объекта.
Поля класса видимы не только в пределах самого класса. Если в некотором классе В объявлен и создан объект x класса А, то класс В является клиентом класса А. В клиентском классе у объекта x видны лишь те поля класса, для которых в момент объявлении был задан атрибут доступа public, что делает эти поля общедоступными. Для классов наследников у объекта видны поля с атрибутами public или protected, но недоступны поля с атрибутом private.
Имеет место следующая иерархия доступности полей объекта в зависимости от значения атрибута доступа – public, protected, private. В самом классе доступны все поля. У наследников не доступны закрытые поля с атрибутом private, у клиентов не доступны закрытые поля и защищенные поля – поля с атрибутами private и protected.
Глобальные переменные уровня модуля. Существуют ли они в C#?
Где еще могут объявляться переменные? Во многих языках программирования переменные могут объявляться на уровне модуля. Такие переменные называются глобальными. Их область действия распространяется, по крайней мере, на весь модуль. Глобальные переменные играют важную роль, поскольку они обеспечивают весьма эффективный способ обмена информацией между различными частями модуля. Обратная сторона эффективности аппарата глобальных переменных, – их опасность. Если какая-либо процедура, в которой доступна глобальная переменная, некорректно изменит ее значение, то ошибка может проявиться в другой процедуре, использующей эту переменную. Найти причину ошибки бывает чрезвычайно трудно. В таких ситуациях приходится проверять работу многих компонентов модуля.
В языке C# роль модуля играют классы, пространства имен, проекты, решения. Поля классов, о которых шла речь выше, могут рассматриваться как глобальные переменные класса. Но здесь у них особая роль. Данные, хранимые в полях класса, являются тем центром, вокруг которого вращается мир класса. Методы класса в этом мире, можно сказать, играют второстепенную роль – они обрабатывают данные. Заметьте, каждый экземпляр класса – это отдельный мир. Поля экземпляра (открытые, защищенные и закрытые) – это глобальная информация, которая доступна всем методам класса.
Статические поля класса хранят информацию, общую для всех экземпляров класса. Они представляют определенную опасность, поскольку каждый экземпляр способен менять их значения.
В других видах модуля – пространствах имен, проектах, решениях — нельзя объявлять переменные. В пространствах имен в языке C# разрешено только объявление классов и их частных случаев: структур, интерфейсов, делегатов, перечислений. Поэтому глобальных переменных уровня модуля, в привычном для других языков программирования смысле, в языке C# нет. Классы не могут обмениваться информацией, используя глобальные переменные. Все взаимодействие между ними обеспечивается способами, стандартными для объектного подхода. Между классами могут существовать два типа отношений – клиентские и наследования, а основной способ инициации вычислений – это вызов метода для объекта-цели или вызов обработчика события. Поля класса и аргументы метода позволяют передавать и получать нужную информацию. Устранение глобальных переменных на уровнях более высоких, чем класс, существенно повышает надежность создаваемых на языке C# программных продуктов, поскольку устраняется источник опасных, трудно находимых ошибок.
Локальные переменные
Перейдем теперь к рассмотрению локальных переменных. Во всех языках программирования, в том числе и в C#, основной контекст, в котором появляются переменные, – это процедуры и функции – методы класса. Тело метода, заключенное в фигурные скобки, будем называть процедурным блоком. Переменные, объявленные в процедурном блоке, называются локальными, – они локализованы в методе.
В некоторых языках, например в Паскале, локальные переменные должны быть объявлены в вершине процедурного блока. Иногда это правило заменяется менее жестким, но, по сути, аналогичным правилом, – где бы внутри процедурного блока ни была объявлена переменная, она считается объявленной в вершине блока и ее область видимости распространяется на весь процедурный блок. В C# принята другая стратегия. Переменную можно объявлять в любой точке процедурного блока. Область ее видимости распространяется от точки объявления до конца процедурного блока.
На самом деле, ситуация с процедурным блоком в C# не так проста. Процедурный блок имеет сложную структуру; в него могут быть вложены другие блоки, связанные с операторами выбора, цикла и так далее. В каждом таком блоке, в свою очередь, допустимы вложения блоков. В каждом внутреннем блоке допустимы объявления переменных. Переменные, объявленные во внутренних блоках, локализованы именно в этих блоках, их область видимости и время жизни определяются этими блоками. Локальные переменные, объявленные в любом внутреннем блоке, существуют от точки объявления до конца соответствующего блока.
Рассмотрим ситуацию с возможными конфликтами имен, появляющихся в различных блоках. Уже говорилось, что имя локальной переменной может совпадать с именем поля класса. Этот конфликт разрешен, поскольку для поля класса можно использовать уточненное имя. Чтобы избежать других конфликтов не разрешается во внутреннем блоке метода объявлять локальную переменную, имя которой совпадает с именем формального параметра метода или совпадает с именем локальной переменной, объявленной в охватывающем блоке.
Класс TestingLocals
Добавим в наш проект новый класс TestingLocals. Зададим в этом классе два поля и один метод. Поля будут играть роль глобальных переменных для метода класса, а во внутренних блоках метода появятся объявления локальных переменных. Это поможет нам обсудить на примере области действия и существования объявленных переменных. Вот код этого класса:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace SimpleVariables
{
class TestingLocals
{
//fields
string s;
int n;

const string POINT = «Point_1″;

//Constructor
public TestingLocals(string s, int n)
{
this.s = s;
this.n = n;
}

//Method
public int Test(int x)
{
int result = 0;
int n = 9;
if (s == POINT)
{
//static int sc =7;
const int cc = 6;
int u = 7, v = u + 2;
x += u + v — cc;
for (int i = 0; i < x; i++)
{
result += i * i;
}
//x += i;
}
else
{
//int n = 5;
//int x = 10;
int u = 7, v = u + 2;
x += u + v — n + this.n;
for (int i = 0; i < x; i++)
{
result += i * i;
}
}
Return result;
}
}
}
Тест 2. Локальные и глобальные переменные класса
Создадим интерфейс пользователя для работы с классом TestingLocals. С этой целью добавим в проект интерфейсный класс FormLocals – наследник класса Form. На рис. 1_2.7 показано, как выглядит спроектированный интерфейс в процессе работы с формой.
Рис.1_ 2.7 Форма FormLocals — интерфейс пользователя
Назначение формы поясняется в специальном текстовом окне. В разделе «исходные данные» два текстовых окна позволяют задать данные, необходимые для формирования объекта класса TestingLocals. Две командные кнопки позволяют создать объект этого класса и вызвать метод Test, тело которого представляет систему вложенных внутренних блоков, содержащих объявление локальных переменных. Большой раздел в интерфейсе формы занимают советы, подсказывающие разработчику, что он может делать при объявлении локальных переменных, и что является недопустимым.
Напомню, что код интерфейсного класса, создаваемый по умолчанию, состоит из двух частей. Одна часть предназначена для Дизайнера форм, и код в ней появляется автоматически, отражая проектирование дизайна формы, выполняемое руками. Другая часть ориентирована на разработчика интерфейса. В эту часть класса добавляются поля, необходимые для обмена информацией с элементами управления, расположенными на форме. Здесь же находится поле, в котором объявлен объект класса Testing. Заметьте, если создается интерфейсный класс, обеспечивающий поддержку работы с одним или несколькими содержательными классами, то в интерфейсном классе должны быть поля с объектами этих классов. Так интерфейсный класс становится клиентом содержательного класса. В нашем случае интерфейсный класс FormLocals становится клиентом класса TestingLocals и эти два класса связываются отношением «клиент – поставщик».
Приведу код той части интерфейсного класса, которая создается разработчиком:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;

namespace SimpleVariables
{
public partial class FormLocals : Form
{
//поля класса — константы и переменные
const string YOU_CAN_1 =
«Объявить локальную переменную с именем, » +
«совпадающим с именем поля класса!»;
const string YOU_CAN_2 =
«В непересекающихся блоках объявлять » +
«локальные переменные с совпадающими именами!»;
const string YOU_CAN_3 =
«Объявить локальную переменную в любой точке блока!»;

const string YOU_CAN_4 =
«Объявлять константы в блоках!»;
const string YOU_CANNOT_1 =
«Объявлять глобальные переменные » +
«уровня Решения, Проекта, Пространства Имен!»;
const string YOU_CANNOT_2 =
«Объявить локальную переменную метода с именем,» +
«совпадающим с именем формального параметра!»;
const string YOU_CANNOT_3 =
«Объявить локальную переменную с атрибутом static!»;
const string YOU_CANNOT_4 =
«Объявить переменную во внутреннем блоке, » +
«если в охватывающем блоке уже объявлена » +
«локальная переменная с тем же именем!»;

TestingLocals testing;
string s;
int n;
public FormLocals()
{
InitializeComponent();
}
}
}
Помимо упомянутых полей класса в нем определены строковые константы для вывода советов по использованию локальных переменных.
В этой части класса появляются и обработчики событий элементов управления формы. Вот как выглядит код обработчика события Click командной кнопки, создающей объект класса TestingLocals:
private void buttonCreateObject_Click(object sender, EventArgs e)
{
s = textBoxS.Text;
n = Convert.ToInt32(textBoxN.Text);
testing = new TestingLocals(s, n);
textBoxS.Text = «Point»;
textBoxN.Text = «0″;
}
Устроен он достаточно просто, — вначале из текстовых полей формы читается информация, необходимая конструктору класса TestingLocals для создания объекта, затем этот объект создается, а текстовые окна формы получают новое значение, которое может быть изменено конечным пользователем в процессе работы с формой.
Чуть более сложно устроен обработчик события Click командной кнопки, вызывающей метод Test класса TestingLocals:
private void buttonTest_Click(object sender, EventArgs e)
{
int x = Convert.ToInt32(textBoxX.Text);
textBoxResult.Text = testing.Test(x).ToString();

textBoxCan1.Text = YOU_CAN_1;
textBoxCan2.Text = YOU_CAN_2;
textBoxCan3.Text = YOU_CAN_3;
textBoxCan4.Text = YOU_CAN_4;

textBoxCannot1.Text = YOU_CANNOT_1;
textBoxCannot2.Text = YOU_CANNOT_2;
textBoxCannot3.Text = YOU_CANNOT_3;
textBoxCannot4.Text = YOU_CANNOT_4;
}
И здесь из текстового окна формы читается значение аргумента, заданное конечным пользователем, затем созданный объект testing вызывает открытый (public) метод класса Test, и результат работы метода выводится в соответствующее текстовое окно, поддерживая необходимую связь с конечным пользователем. В качестве побочного эффекта в текстовые поля формы выводятся советы по использованию локальных переменных.
Помимо советов, анализируя текст метода Test, следует обратить внимание при использовании локальных переменных на следующие моменты:
Аргументы метода (его формальные параметры) считаются объявленными в начале блока, задающего тело метода. Таким образом область их действия распространяется на весь этот блок и ни в одном из внутренних блоков нельзя объявлять локальную переменную с именем, совпадающим с именем аргумента.
Параметр цикла считается объявленным в блоке, задающим тело цикла. Поэтому область его действия распространяется на весь этот блок и во внутренних блоках тела цикла нельзя объявлять локальную переменную с именем, совпадающим с именем параметра цикла. Заметьте, после окончания цикла параметр цикла перестает существовать и не может быть использован. В нашем примере оператор, в котором делается попытка использовать параметр цикла после завершения цикла, закомментирован.
В параллельных блоках (в нашем примере две ветви оператора if) разрешается объявлять локальные переменные с одинаковыми именами, поскольку области существования этих переменных не пересекаются.
Поскольку объявлять локальную переменную можно в любой точке блока, то хорошим стилем считается объявление локальной переменной как можно ближе к точке ее непосредственного использования. Нет смысла объявлять локальную переменную в начале блока, если она будет использована где-то в конце блока.

Глобальные переменные уровня процедуры. Существуют ли?
Поскольку процедурный блок – блок тела метода — имеет сложную структуру с вложенными внутренними блоками, то и здесь возникает тема глобальных переменных. Переменная, объявленная во внешнем блоке, рассматривается как глобальная по отношению к внутренним блокам. В большинстве известных языках программирования во внутренних блоках разрешается объявлять переменные с именем, совпадающим с именем глобальной переменной. Конфликт имен снимается за счет того, что локальное внутреннее определение сильнее внешнего. Поэтому область видимости внешней глобальной переменной сужается и не распространяется на те внутренние блоки, где объявлена переменная с подобным именем. Внутри блока действует локальное объявление этого блока, при выходе восстанавливается область действия внешнего имени. В языке C# этот гордиев узел конфликтующих имен разрублен, – во внутренних блоках запрещено использование имени, совпадающего с именем, уже использованном во внешнем блоке. В нашем примере незаконная попытка объявить во внутреннем блоке уже объявленное имя закомментирована.
Обратите внимание, что подобные решения, принятые создателями языка C#, не только упрощают жизнь разработчикам транслятора. Они способствуют повышению эффективности программ, а самое главное, повышают надежность программирования на C#.
Отвечая на вопрос, вынесенный в заголовок, следует сказать, что глобальные переменные на уровне процедуры в языке C#, конечно же, есть, но нет конфликта имен между глобальными и локальными переменными на этом уровне. Область видимости глобальных переменных процедурного блока распространяется на весь блок, в котором они объявлены, начиная от точки объявления, и не зависит от существования внутренних блоков. Когда говорят, что в C# нет глобальных переменных, то, прежде всего, имеют в виду их отсутствие на уровне модуля. Уже во вторую очередь речь идет об отсутствии конфликтов имен на процедурном уровне.
Константы
Константы C# могут появляться, как обычно, в виде литералов и именованных констант. Вот пример константы, заданной литералом и стоящей в правой части оператора присваивания:
y = 7.7f;
Значение константы “7.7f” является одновременно ее именем, оно же позволяет однозначно определить тип константы. Заметьте, иногда, как в данном случае, приходится добавлять к значению специальные символы для точного указания типа. Я не буду останавливаться на этих подробностях. Если возникает необходимость уточнить, как записываются литералы, то достаточно получить справку по этой теме.
Всюду, где можно объявить переменную, можно объявить и именованную константу. Синтаксис объявления схож. В объявление добавляется модификатор const, инициализация констант обязательна и не может быть отложена. Инициализирующее выражение может быть сложным, важно, чтобы оно было вычислимым в момент его определения. Вот пример объявления констант:
/// <summary>
/// Константы
/// </summary>
public void Constants()
{
const int SMALL_SIZE = 38, LARGE_SIZE =58;
const int MIDDLE_SIZE = (SMALL_SIZE + LARGE_SIZE)/2;
const double PI = 3.141593;
// LARGE_SIZE = 60; //Значение константы нельзя изменить.
}//Constants
Два важных правила стиля связаны с константами. Правило именования констант требует, чтобы имена констант задавались заглавными буквами, и знак подчеркивания использовался бы в качестве разделителя слов для многословных имен. Правило стиля «Нет литеральным константам» требует, чтобы литеральные константы использовались только в момент объявления именованных констант, а во всех остальных местах кода использовались бы только именованные константы. Это позволяет давать константам содержательные имена, что улучшает понимание смысла программы. Кроме того, если по какой либо причине значение константы нужно изменить, то такое изменение будет сделано только в одном месте – в точке объявления константы, не затрагивая основного кода программы. Следуя этим правилам стиля легко обеспечить многоязычный интерфейс программы.
В примерах я буду стараться выдерживать правила хорошего стиля программирования. Правило стиля допускает разумные исключения и такие константы как 0, 1, константа, задающая пустую строку, будут появляться в различных операторах программного кода.
Типы и классы
Язык C# является языком объектного программирования. Все типы – встроенные и пользовательские – определены как классы, связанные отношением наследования. Родительским, базовым классом является класс object. Все остальные типы или, точнее, классы являются его потомками, наследуя методы этого класса. У класса object есть четыре наследуемых метода:
bool Equals(object obj) – проверяет эквивалентность текущего объекта и объекта, переданного в качестве аргумента;
System.Type GetType() – возвращает системный тип текущего объекта;
string ToString() – возвращает строку, связанную с объектом. Для арифметических типов возвращается значение, преобразованное в строку;
int GetHashCode() – служит как хэш-функция в соответствующих алгоритмах поиска по ключу при хранении данных в хэш-таблицах.
Естественно, что все встроенные типы нужным образом переопределяют методы родителя и добавляют собственные методы и свойства. Учитывая, что и классы, создаваемые пользователем, также являются потомками класса object, то, как правило, в них необходимо переопределить методы родителя, поскольку реализация родителя, предоставляемая по умолчанию, не будет обеспечивать нужный эффект.
Рассмотрим вполне корректный в языке C# пример объявления переменных и присваивания им значений:
int x = 1;
int v = new Int32();
v = 007;
string s1 = «Agent»;
s1 = s1 + v.ToString() +x.ToString();
В этом примере переменная x объявляется как обычная переменная типа int. В то же время для объявления переменной v того же типа int используется стиль, принятый для объектов. В объявлении применяется конструкция new и вызов конструктора класса. В операторе присваивания, записанном в последней строке фрагмента, для обеих переменных вызывается метод ToString, как это делается при работе с объектами. Этот метод, наследуемый от родительского класса object, переопределенный в классе int, возвращает строку с записью целого. Сообщу еще, что класс int не только наследует методы родителя – класса object, – но и дополнительно определяет метод CompareTo, выполняющий сравнение целых, и метод GetTypeCode, возвращающий системный код типа. Для класса int определены также статические методы и поля, о которых расскажу чуть позже.
Так что же такое после этого int, спросите Вы? Ведь ранее говорилось, что int относится к значимым – value-типам, следовательно, он хранит в стеке значения своих переменных, в то время как объекты должны задаваться ссылками. С другой стороны, создание экземпляра с помощью конструктора, вызов методов, наконец, существование родительского класса object, – все это указывает на то, что int – это настоящий класс. В зависимости от контекста x может восприниматься как переменная типа int или как объект класса int. Это же верно и для всех остальных value-типов. Замечу еще, что все значимые типы фактически реализованы как структуры, представляющие частный случай класса.
Остается понять, для чего в языке C# введена такая двойственность. Для int и других значимых типов сохранена концепция типа не только из-за ностальгических воспоминаний о типах. Дело в том, что значимые типы эффективнее в реализации, им проще отводить память, так что именно соображения эффективности реализации заставили авторов языка сохранить значимые типы. Более важно, что зачастую необходимо оперировать значениями, а не ссылками на них, хотя бы из-за различий в семантике присваивания для переменных ссылочных и значимых типов.
С другой стороны, в определенном контексте крайне полезно рассматривать переменные типа int как настоящие ссылочные объекты и обращаться с ними как с объектами. В частности, полезно иметь возможность создавать и работать со списками, чьи элементы являются разнородными объектами, в том числе принадлежащими к значимым типам.
Проекты, содержащие несколько форм
Вернемся к вопросам организации нашего проекта. Проект SimpleVariables, созданный нами, содержит четыре интерфейсных класса, наследуемых от класса Form, и соответственно четыре формы. Каждая форма представляет контейнер, который в процессе проектирования заполняется элементами управления для создания необходимого пользовательского интерфейса.
Архитектура проекта является примером типовой архитектуры проекта с главной кнопочной формой. Главная форма, задающая точку входа в проект, представляет форму с множеством командных кнопок. Всякий раз, когда в проект добавляется новый интерфейсный класс и соответствующая ему форма, в класс главной формы добавляется объект нового класса, а на главную форму добавляется новая командная кнопка, обработчик события Click которой будет создавать объект этого класса и вызывать метод Show для показа формы соответствующего интерфейсного класса.
Обработчик события Click, создающий объект интерфейсного класса FormLocals и вызывающий связанную с ним форму, имеет вид:
private void button1_Click(object sender, EventArgs e)
{
testLocalsForm = new FormLocals();
testLocalsForm.Show();
}
Модальные и немодальные формы. Методы Show, ShowDialog, Hide,Close
В проекте с множеством форм одновременно на экране может быть открыто несколько форм. Может ли пользователь переключаться на работу с той или иной формой? Все зависит от того, как открыта форма. Каждую форму можно открыть как модальную или как не модальную. Хотя термин «модальная форма» широко используется удобнее говорить, что форма может быть открыта как «диалоговое окно» (модальная форма) или как обычное окно (немодальная форма). Если форма, как в наших примерах, открывается методом Show, то она задает обычное окно, если форму открывать методом ShowDialog, то она открывается как диалоговое окно. В чем разница? Из диалогового окна нельзя выйти, не закончив диалог и не закрыв форму. Открыв диалоговое окно нельзя переключиться на работу с другой формой, не закончив диалог. Закрыть диалоговое окно можно разными способами. Можно щелкнуть по крестику, расположенному в правом верхнем углу формы, закрывая ее «грубым» способом. В диалоговом окне часто размещают командные кнопки с предопределенной семантикой – “OK”, “Cancel” и другие. Щелчки по этим кнопкам также приводят к закрытию диалогового окна. У метода ShowDialog есть еще одна особенность, — в отличие от метода Show он реализован как функция, возвращающая значение типа System.DialogResult. Благодаря этому можно узнать, какая кнопка была нажата при завершении диалога (например OK или Cancel).
Если форма открыта методом Show, как не диалоговое окно, то, не закончив работу с открытой формой, можно перейти в главную форму или другую не модальную форму, поработать там, нажав какие-нибудь командные кнопки, получив нужную информацию, а затем снова вернуться к исходной форме.
Для показа формы можно применять два метода – Show и ShowDialog, для скрытия формы можно также применять два метода – Hide и Close. Первый из этих методов скрывает форму, второй – закрывает. Для диалоговых окон можно применять как метод Hide, так и метод Close, – эффект будет одинаков – диалоговое окно будет закрыто. Затем его можно открыть и показать методом Show. Метод Hide можно применять и для немодальных форм, открытых методом Show. Окно, открытое не для диалога, можно временно скрыть, вызвав метод Hide, а затем показать, вызвав метод Show. Но вот на что следует обратить особое внимание. После закрытия не диалогового окна либо при вызове метода Close, либо «грубым» способом нажатия на крестик в окне формы, показать затем форму, вызвав метод Show, уже не удастся. Причина в том, что при закрытии формы, сам объект, задающий форму, продолжает существовать, но ресурсы освобождаются, и графическое окно уже не связано с программным объектом. Поскольку конечный пользователь всегда может применить грубую силу для закрытия формы, то, обратите внимание, в соответствующих обработчиках события создание объекта, задающего форму, предшествует вызову метода Show. Для диалоговых окон объект можно было бы создать только один раз, например в конструкторе главной кнопочной формы.
Я добавил в проект диалоговую форму, в которой разместил командные кнопки OK и Cancel с предустановленной семантикой, ввел еще некоторые изменения, чтобы проиграть все возможные варианты показа, скрытия и закрытия форм, открываемых как модальные (диалоговые окна), так и немодальные. Не буду приводить подробного описания этих экспериментов, но надеюсь, что при желании читатели их смогут провести самостоятельно.
Еще одно замечание, связанное с закрытием форм. Когда закрывается главная форма, открываемая в точке входа – процедуре Main, то закрываются все открытые к этому времени формы и приложение заканчивает свою работу. Когда же закрывается любая другая форма, то закрывается только эта форма, остальные формы остаются открытыми.
Задачи
Построить консольное приложение. Дать ему имя, разместить в выбранной директории. Проанализировать созданный программный текст. Познакомиться со средой разработки Visual Studio .Net. Выполнить приложение в пошаговом отладочном режиме.
Построить Windows-приложение. Дать ему имя, разместить в выбранной директории. Проанализировать созданный программный текст. Познакомиться со средой разработки Visual Studio .Net. Выполнить приложение в пошаговом отладочном режиме.
Построить консольное приложение «Здравствуй, Мир!», выводящее на консоль строку приветствия.
Построить Windows-приложение «Здравствуй, Мир!» с командной кнопкой и текстовым окном. Приложение выводит в текстовое окно строку приветствия при нажатии командной кнопки.
Построить консольное приложение «Здравствуй, человек!». Приложение вводит с консоли имя и выводит на консоль строку приветствия, используя введенное имя. Если вводится пустая строка, то выводится текст «Здравствуй, человек!».
Построить Windows-приложение «Здравствуй, человек!» с командной кнопкой и двумя текстовыми окнами. Пользователь вводит имя в первое текстовое окно и при нажатии командной кнопки получает во втором текстовом окне строку приветствия, использующую введенное имя. Если вводится пустая строка, то выводится текст «Здравствуй, человек!».
Построить консольное приложение «Здравствуйте, люди!». Приложение вводит с консоли имя и выводит на консоль строку приветствия, используя введенное имя. Вводу имени предшествует приглашение к вводу «Введите имя».
Построить Windows-приложение «Здравствуйте, люди!» с командной кнопкой и двумя текстовыми окнами. Пользователь вводит имя в первое текстовое окно и при нажатии командной кнопки получает во втором текстовом окне строку приветствия, использующую введенное имя. С каждым текстовым окном связывается окно метки, в котором дается описание назначения текстового окна.
Построить циклическое консольное приложение «Здравствуй, человек!». Приложение вводит с консоли имя и выводит на консоль строку приветствия, используя введенное имя. Вводу имени предшествует приглашение к вводу «Введите имя». После приветствия на консоль выводится запрос на продолжение работы «Продолжить работу? (да, нет)». В зависимости от введенного ответа повторяется приветствие или приложение заканчивает работу.
Указание к задаче 1.9. Представьте выполняемый блок процедуры Main оператором цикла следующего вида:
do
{

}while((answer==»Да») || (answer==»да» ));
Здесь answer переменная, содержащая ответ пользователя на предложение продолжения работы.
Встроенные типы данных. Ввод-вывод данных.
Назначение задач этого раздела состоит в знакомстве с основными скалярными встроенными типами, их классификацией, диапазоном возможных значений. Решение задач требует умения объявлять, вводить и выводить значения переменных этих типов.
Построить циклическое консольное приложение «Целочисленные типы». Приложение поочередно вводит с консоли значения целочисленных типов: sbyte, byte, short, ushort, int, uint, long, ulong. Вводу значения предшествует приглашение к вводу. После завершения ввода приложения выводит все введенные значения с указанием их типа. Проанализировать, что происходит при вводе значений, не соответствующих требуемому типу или выходящих за пределы интервала возможных значений типа.
Построить Windows-приложение «Целочисленные типы» с 16-ю помеченными текстовыми окнами и двумя командными кнопками Пользователь вводит значения целочисленных типов: sbyte, byte, short, ushort, int, uint, long, ulong в первые 8 окон. По нажатию командной кнопки «Ввод значений» данные из текстовых окно становятся значениями переменных соответствующих типов. По
нажатию командной кнопки «Вывод значений» значения переменных соответствующих типов передаются в текстовые окна, предназначенные для вывода значений. Проанализировать, что происходит при вводе значений, не соответствующих требуемому типу или выходящих за пределы интервала возможных значений типа.
Построить циклическое консольное приложение «Вещественные типы». Приложение поочередно вводит с консоли значения вещественных типов: float, double. Вводу значения предшествует приглашение к вводу. После завершения ввода приложения выводит все введенные значения с указанием их типа. Проанализировать, что происходит при вводе значений, не соответствующих требуемому типу или выходящих за пределы интервала возможных значений типа.
Построить Windows-приложение «Вещественные типы» с 4-мя помеченными текстовыми окнами и двумя командными кнопками Пользователь вводит значения вещественных типов: float, double в первые 2 окна. По нажатию командной кнопки «Ввод значений» данные из текстовых окно становятся значениями переменных соответствующих типов. По нажатию командной кнопки «Вывод значений» значения переменных соответствующих типов передаются в текстовые окна, предназначенные для вывода значений. Проанализировать, что происходит при вводе значений, не соответствующих требуемому типу или выходящих за пределы интервала возможных значений типа.
Построить циклическое консольное приложение «Decimal тип». Приложение поочередно вводит с консоли значения типа Decimal c целой и дробной частью. Вводу значения предшествует приглашение к вводу. После завершения ввода приложение выводит введенное значение с указанием типа. Проанализировать, что происходит при вводе значений, не соответствующих требуемому типу или выходящих за пределы интервала возможных значений типа.
Построить Windows-приложение «Decimal тип» с 2-мя помеченными текстовыми окнами и двумя командными кнопками Пользователь вводит значения типа Decimal c целой и дробной частью в текстовое окно. По нажатию командной кнопки «Ввод значений» данные из текстового окна становятся значением переменной соответствующего типа. По нажатию командной кнопки «Вывод значений» значение переменной передается в текстовое окно, предназначенное для вывода значений. Проанализировать, что происходит при вводе значений, не соответствующих требуемому типу или выходящих за пределы интервала возможных значений типа.
Построить циклическое консольное приложение «Строковый тип». Приложение поочередно вводит с консоли символы и строки – значения типов char и string. Вводу значения предшествует приглашение к вводу. После завершения ввода приложение выводит введенное значение с указанием типа. Проанализировать, что происходит при вводе значений, не соответствующих требуемому типу.
Построить Windows-приложение «Строковый тип». Пользователь вводит значения в текстовое окно. По нажатию командной кнопки «Ввод значений» данные из текстового окна становятся значением переменной соответствующего типа. По нажатию командной кнопки «Вывод значений» значение переменной передается в текстовое окно, предназначенное для вывода значений. Проанализировать, что происходит при вводе значений, не соответствующих требуемому типу.
Построить циклическое консольное меню-приложение «Выбор». Приложение выводит на консоль нумерованные пункты меню, предлагая затем пользователю задать номер выбранного им пункта меню. При вводе пользователем соответствующего номера начинает выполняться выбранный пункт меню. В данной задаче команды меню определяют тип значений, вводимых и выводимых на консоль (смотри задачи 1.10 – 1.18).
Построить Windows-приложение «Выбор». В одно из текстовых окон пользователь вводит тип переменной, в другое – значение. По нажатию командной кнопки «Ввод значений» данные из текстового окна становятся значением переменной, тип которой задан в первом текстовом окне. По нажатию командной кнопки «Вывод значений» значение переменной передается в текстовое окно, предназначенное для вывода значений.
Контролируемый ввод данных
В эадачах этого раздела необходимо организовать контроль данных, вводимых пользователем.
Постройте консольное приложение, в котором вводится имя пользователя. Имя должно отвечать правилам, принятым в русском языке: составлено из букв кириллицы. Первый символ должен быть заглавной буквой, остальные – строчными буквами.
Постройте Windows приложение, в котором вводится имя пользователя. Имя должно отвечать правилам, принятым в русском языке: составлено из букв кириллицы. Первый символ должен быть заглавной буквой, остальные – строчными буквами.
Постройте консольное приложение, в котором вводится возраст пользователя. Возраст должно отвечать заданным правилам: быть целым числом из фиксированного диапазона.
Постройте Windows приложение, в котором вводится возраст пользователя. Возраст должно отвечать заданным правилам: быть целым числом из фиксированного диапазона.
Постройте консольное приложение, в котором вводится выбранная пользователем специальность, принадлежащая фиксированному списку специальностей.
Постройте Windows приложение, в котором вводится выбранная пользователем специальность, принадлежащая фиксированному списку специальностей.
Постройте консольное приложение, в котором вводятся номера телефонов пользователя (домашний, мобильный). Номера должны быть заданы в соответствии с фиксированным шаблоном. Один или оба номера могут отсутствовать.
Постройте Windows приложение, в котором вводятся номера телефонов пользователя (домашний, мобильный). Номера должны быть заданы в соответствии с фиксированным шаблоном. Один или оба номера могут отсутствовать.
Постройте консольное приложение, в котором вводятся анкетные данные пользователя, являющегося студентом (ФИО, возраст, телефон, факультет, номер группы). Значения каждого поля должны отвечать заданным правилам (смотри предыдущие задачи этого раздела).
Постройте Windows приложение, в котором вводятся анкетные данные пользователя, являющегося студентом (ФИО, возраст, телефон, факультет, номер группы). Значения каждого поля должны отвечать заданным правилам (смотри предыдущие задачи этого раздела).
Постройте Windows приложение, в котором вводятся данные о владельце машины (ФИО, марка машины, ее номер и цвет). Значения каждого поля должны отвечать заданным правилам.

Скачать лекцию 1.2