Проект к данной лекции Вы можете скачать здесь.
Знакомство с новым языком программирования разумно начинать с изучения системы типов этого языка. Как в нем устроена система типов данных? Какие имеются простые типы, как создаются сложные, структурные типы, как определяются собственные типы,
В первых языках программирования понятие класса отсутствовало - рассматривались только типы данных. При определении типа явно задавалось только множество возможных значений, которые могут принимать переменные этого типа. Например, утверждалось, что тип 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. Знать все типы из этой библиотеки практически невозможно, но умение ориентироваться в ней необходимо.
Типы данных разделяются также на статические и динамические. Для переменных 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.Кроме того, в языке есть
Язык позволяет конструировать типы.
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# однозначно отображаются, а фактически совпадают с системными типами каркаса Net Framework, размещенными в пространстве имен System. Поэтому всюду, где можно использовать имя типа, например, int, с тем же успехом можно использовать и имя System.Int32.
В заключение этого раздела приведу таблицу, содержащую описание базисных встроенных типов языка C# и их основные характеристики.
| Логический тип | |||
|---|---|---|---|
| Имя типа | Системный тип | Значения | Размер |
| bool | System.Boolean | true, false | 8 бит |
| Арифметические целочисленные типы | |||
| Имя типа | Системный тип | Диапазон | Размер |
| sbyte | System.SByte | [-128, 127] | Знаковое, 8-бит |
| byte | System.Byte | [0, 255] | Беззнаковое, 8-бит |
| short | System.Int16 | [-32768, 32767] | Знаковое, 16-бит |
| ushort | System.UInt16 | [0, 65535] | Беззнаковое, 16-бит |
| int | System.Int32 | [-231, 231] | Знаковое, 32-бит |
| uint | System.UInt32 | [0, 232] | Беззнаковое, 32-бит |
| long | System.Int64 | [-263 , 263] | Знаковое, 64-бит |
| ulong | System.UInt64 | $$\approx(0, 2^{64})$$ | Беззнаковое, 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 <имя конструктора>([<список аргументов>])
Такая конструкция инициализатора, применимая и для скалярных переменных
Хороший стиль программирования требует, чтобы все простые переменные объявлялись с инициализацией. Дело в том, что при объявлении без инициализации значение переменной остается неопределенным, и всякая попытка использовать такую переменную в вычислениях, где требуется значение переменной, является ошибкой, которую компилятор обнаруживает еще на этапе компиляции. Конечно, можно объявить переменную без инициализации, а потом в процессе вычислений присвоить ей значение, тем самым инициализируя ее.
Заметьте, компилятор строго следит за тем, чтобы в вычислениях не появлялись переменные, не имеющие значения. Если присвоение значения переменной происходит внутри одной из ветвей оператора 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);
В заключение взгляните на результаты работы этого тестового примера.
(рис 2.1) Типы, допускающие значение nullЗаметьте, при выводе на печать значение null не имеет специального обозначения и выводится как пробел.
Значение 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. Показано, как при умножении появляются значения бесконечности, а также появляется одна из соответствующих констант, задающих бесконечность. В заключение взгляните на результаты работы этого тестового примера.
(рис 2.2) Значения null, NaN, Infinity
Продолжу приводить примеры и сейчас подробнее остановлюсь на том, как строилось Решение (Solution) с именем Ch2, в котором размещены проекты этой лекции. Наряду с консольным проектом ConsoleNullable, примеры из которого приводились в предыдущем разделе, создадим Windows-проект с именем SimpleVariables. В этом проекте почти не будет никаких вычислений. Его главная задача - продемонстрировать различные способы объявления простых переменных встроенных
Побочная цель состоит в том, чтобы показать работу проекта, включающего несколько форм - интерфейсных классов. Проект продемонстрирует часто встречающуюся на практике ситуацию, когда в нем есть
Поскольку в первой лекции подробно рассказывалось о том, как создаются Windows-проекты и первые шаги работы с ними, останавливаться на этом уже не буду. На рис 2.3 показана главная форма на начальном этапе проектирования.
(рис 2.3) Главная форма на начальных шагах процесса ее проектированияЧто же было сделано при проектировании главной формы? Прежде всего, она была переименована и получила содержательное имя. Переименован был и файл, содержащий описание интерфейсного класса, задающего форму. На форме размещена поясняющая надпись "Главная форма" и три командные кнопки.
Для задания надписей, поясняющих назначение формы, можно использовать элемент управления Label или текстовое окно - TextBox. В нашем примере использовалась метка.
Проектируемая главная форма является примером
Подведем итоги. Если в проекте предполагается n различных сервисов, каждый из которых требует собственного интерфейса, то в проект наряду с главной формой, создаваемой по умолчанию в момент создания проекта, добавляются n интерфейсных классов - наследников класса Form, каждый из которых имеет собственную форму. Каждая такая форма заселяется элементами управления, задавая интерфейс соответствующего сервиса. На главной форме располагаются n командных кнопок, а в код интерфейсного класса, задающего главную форму, добавляются n полей, каждое из которых содержит объявление объекта соответствующего интерфейсного класса. Когда пользователь выбирает в главной форме командную кнопку, то обработчик события Click этой кнопки вызывает конструктор интерфейсного класса и создает реальный объект этого интерфейсного класса и реальную форму, связанную с объектом.
Затем в обработчике вызывается метод Show этого объекта, соответствующая форма открывается, показывается на экране дисплея, и пользователь начинает работать с интерфейсом формы. Такая схема работы встречается на практике достаточно часто, так что проекты с главной кнопочной формой будут появляться неоднократно.
Продемонстрирую различные аспекты применения скалярных типов и объявления переменных. В проекте роль сервисов, предоставляемых пользователю, будут играть три различных примера - три теста. Каждый тест имеет свою цель и свой интерфейс, позволяющий конечному пользователю проводить исследования возможностей и особенностей объявления скалярных переменных. Для каждого теста будет создан свой интерфейсный класс и спроектирована форма, связанная с этим классом. Каждая командная кнопка главной формы будет запускать свой тест. Обработчик события Click каждой из командных кнопок будет открывать форму, спроектированную для работы с выбранным тестом нашего проекта.
Какова идея этого теста? Давайте дадим конечному пользователю возможность в текстовых окнах формы задать имя скалярного типа и значение, соответствующее этому типу. После этого пользователь может нажать командную кнопку "Ввод", спроектированную в интерфейсе формы. Обработчик события Click командной кнопки "Ввод" должен построить переменную заданного типа и присвоить ей значение, заданное пользователем. Если корректно указано имя типа и значение, то операция пройдет успешно, о чем пользователю и будет выдано соответствующее сообщение. В качестве допустимых типов разрешается указывать любой допустимый в C# скалярный тип, заданный таблицей 2.1. В качестве допустимого значения разрешается указывать любое значение из диапазона, соответствующего выбранному типу, и представленному в таблице 2.1. В случае некорректной работы пользователя появляется сообщение "Неправильно указано имя типа" или, если значение не соответствует заданному типу, должно выдаваться сообщение об ошибке.
Спроектируем в интерфейсе пользователя и командную кнопку "Вывод", по нажатию которой будет выдаваться значение, хранимое в созданной переменной.
Тест должен позволить пользователю изучить все скалярные типы языка C#, понять, какие значения допустимы для каждого типа, увидеть, как можно вводить и выводить значения переменных скалярного типа.
Еще одна важная роль этого теста состоит в демонстрации правильной организации ввода данных с использованием программной конструкции try - catch блоков. Ввод данных, задаваемых пользователем, всегда должен контролироваться, поскольку человеку свойственно ошибаться. Тест показывает, как можно обнаруживать ошибки ввода.
В соответствии с задачами теста спроектируем интерфейс пользователя. Прежде всего в проект нужно добавить новый интерфейсный класс, сопровождаемый формой. Такие классы являются наследниками класса Form из библиотеки FCL. Воспользуемся пунктом меню Project|AddWindowsForm для добавления в проект интерфейсного класса и ,соответственно, новой формы. Отслеживая наши действия, инструментальное средство Designer Form добавит в наш проект необходимые классы. Следуя правилам стиля, произведем переименование, заменив стандартные имена содержательными.
Теперь можно заняться непосредственным проектированием пользовательского интерфейса, размещая в форме нужные элементы управления. Не буду в деталях описывать весь процесс проектирования, поскольку интерфейс достаточно прост. Он отвечает заданной функциональности и включает известные элементы управления - метки, текстовые окна, командные кнопки и контейнеры GroupBox. Контейнеры в формах позволяют придать пользовательскому интерфейсу нужную структуру, объединяя в группу элементы управления, решающие общую задачу - например, ввода исходных данных или вывода результатов.
На рис 2.4 показана спроектированная форма в процессе работы с ней.
(рис 2.4) Форма TestTypes в процессе работыПрежде чем продолжить рассмотрение теста, давайте поговорим об основных принципах, лежащих в основе современного визуального стиля программирования. На начальных этапах программирования работой программы полностью управлял ее текст. Так, в программах на языке Алгол выполнение программы начиналось с оператора begin (начало) и заканчивалось оператором end (конец). Развитие программирования потребовало диалога с пользователем в ходе выполнения программы. Вначале это был простой диалог, характерный для уже рассмотренных нами консольных приложений, когда выполнение программы приостанавливается, и она ждет ответа пользователя, вводимого с консоли. Дальнейшее развитие программирования привело к визуальному стилю, когда программные объекты стали иметь визуальные образы, когда появились графические объекты. Графические образы более информативны, чем текстовые.
Визуальный стиль изменил и стиль диалога с пользователем, позволив строить интерфейс пользователя, основанный на объектах, имеющих визуальный образ. Простейшим и классическим примером является объект класса TextBox, графическим образом которого является текстовое окно - оно может служить как для вывода текстов в окно, так и для ввода текста пользователем. Этот объект обладает мощной функциональностью текстового редактора, позволяет пользователю редактировать вводимый им текст, удаляя, заменяя и вставляя символы в произвольное место текста.
В библиотеке FCL имеется большое число классов, предназначенных для организации пользовательского интерфейса. Со многими из них будем знакомиться по ходу изучения нашего курса. Эти интерфейсные классы и соответствующие объекты получили название элементов управления (control). Класс Control из этой библиотеки обеспечивает базовую функциональность всех элементов управления и является родительским классом для всех интерфейсных классов. Класс Form, который неоднократно упоминался, также является непрямым наследником класса Control и прямым наследником класса ContainerControl. Последний класс определяет базовую функциональность тех элементов управления, которые, как и формы, есть контейнеры, допускающие внутри графического образа размещение образов других элементов управления. Кроме формы, уже известным нам примером контейнера служит класс GroupBox.
Выше говорилось, что проектирование пользовательского интерфейса, как правило, ведется не программным путем, хотя и такое возможно, а "руками". В Visual Studio 2008 имеется специальная инструментальная панель с элементами управления, которые можно перетаскивать на форму в процессе проектирования, располагать эти элементы в нужном месте формы, менять их размеры, устанавливать их свойства, открываемые в окне свойств. Дизайнер формы транслирует все эти действия в соответствующий программный код.
Процедура Main, задающая точку входа в Windows-проект, открывает главную форму, и пользователь попадает в спроектированный для этой формы мир графических объектов. Теперь пользователь становится у руля управления ходом выполнения проекта и является "возмутителем спокойствия" в этом мире объектов. Он начинает вводить тексты в текстовые окна, выбирать нужные ему элементы из открывающихся списков, нажимать командные кнопки и выполнять другие действия над элементами управления. Каждое такое действие пользователя приводит к возникновению соответствующего события у программного объекта. Так, когда пользователь изменяет текст в текстовом окне, у соответствующего объекта класса TextBox возникает событие TextChanged, при нажатии командной кнопки у объекта возникает событие Click, двойной щелчок по командной кнопке приводит к событию DblClick.
В ответ на возникновение события объект посылает сообщение операционной системе. Обрабатывая очередь сообщений, операционная система отыскивает обработчик события, если объект предусмотрел таковой, и передает ему управление.
Подключить к объекту обработчик события можно визуально в процессе проектирования элемента управления. В окне свойств элемента управления можно перейти к списку событий этого элемента и в этом списке выбрать (включить) нужное событие. На рис 2.5 показан момент проектирования формы, когда для выбранного элемента управления buttonInput в окне его свойств отображается список возможных событий этого элемента.
(рис 2.5) Список событий элемента управления buttonInputМожно видеть, что для этого элемента включено событие Click. Заметьте, имя обработчика события строится из имен элемента управления и события, разделенных знаком подчеркивания. У каждого элемента управления есть одно основное событие, которое можно включить простым щелчком по элементу управления, например, для командных кнопок таким событием является событие Click.
При включении события интерфейсный класс автоматически дополняется специальным методом, содержащим заготовку обработчика события -
С точки зрения объектного программирования обработчики событий - это специальные методы интерфейсных классов (события), особенность которых состоит в том, что они вызываются в нужный момент операционной системой в ответ на возникновение события у соответствующего объекта, инициированного работой пользователя.
Стиль программирования, основанный на проектировании визуального пользовательского интерфейса, называется визуальным программированием. Стиль программирования, основанный на событиях и системных вызовах обработчиков событий, называется событийно-управляемым программированием (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.
Конечно, плохо, когда в процессе работы программы возникает исключение. Однако его появление еще не означает, что программа не сможет выполнить свой контракт. Исключение может быть нужным образом обработано, после чего продолжится нормальный ход вычислений приложения. Гораздо хуже, когда возникают ошибки в работе, не приводящие к исключениям. Тогда работа продолжается с неверными данными без исправления ситуации и даже без уведомления о возникновении ошибки.
Наш пример продемонстрировал первое применение
Пользователь, работающий с программой, имеет право на ошибку. Программист, создающий программу, такого права не имеет. Если из-за ошибки пользователя программа перестает работать, то в этом вина программиста, поскольку его программа не смогла вовремя обнаружить ошибку и дать возможность пользователю исправить ее. Ошибки пользователя чаще всего возникают, когда пользователь вводит исходные данные, необходимые программе. Поэтому любой пользовательский ввод должен контролироваться. Операторы,
В рассмотренном выше примере пользовательский ввод контролируется. Поэтому стоит еще раз проанализировать этот пример с позиций того, как организован контроль ввода данных, вводимых пользователем.
Этот тест демонстрирует объявление переменных, часть из которых инициализируется в момент объявления. Пользователю в текстовом окне показывается некоторый фрагмент программы, содержащий объявления переменных и простейшие присваивания. Задача пользователя - в текстовые окна, спроектированные для всех переменных фрагмента, ввести их значения, которые они получат в результате выполнения фрагмента. После чего он может нажать командную кнопку и сравнить введенные им результаты с теми, что дает реальное вычисление.
На рис 2.6 показана спроектированная форма в процессе работы.
(рис 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) класса. О классах и полях предстоит еще обстоятельный разговор, а сейчас сообщу лишь некоторые минимальные сведения, связанные с рассматриваемой темой.
Все переменные, объявленные на уровне класса при его описании, являются полями этого класса.
Поскольку класс, как уже многократно говорилось, задает описание типа данных, то поля класса задают представление этих данных. Необходимо крайне внимательно относиться к проектированию полей класса - всякие "лишние" объявления на этом уровне крайне нежелательны.
Когда конструктор класса создает очередной объект - экземпляр класса, то он в памяти создает набор полей, определяемых классом, и записывает в них значения, характеризующие свойства данного конкретного экземпляра. Так что каждый объект в памяти можно рассматривать как набор соответствующих полей класса со своими значениями. Заметьте, для классов, представленных структурой, объект создается аналогичным способом, но разворачивается в стеке.
Объекты в динамической памяти, с которыми не связана ни одна ссылочная переменная, становятся недоступными. Реально они оканчивают свое существование, когда сборщик мусора (
Есть одно важное исключение. Некоторые поля могут жить дольше. Если при объявлении класса поле объявлено с модификатором static, то такое поле является частью модуля, связанного с классом, и не входит в состав его экземпляров. Поэтому static -поля живут так же долго, как и сам класс. Более подробно эти вопросы будут обсуждаться при рассмотрении классов, структур, интерфейсов.
Наследование классов - это одно из важнейших отношений, существующих между классами одного проекта. О нем мы будем говорить подробно, а сейчас рассмотрим только одну сторону наследования - что происходит с полями классов в процессе наследования. Пусть класс В является наследником класса А. Тогда класс В наследует все поля класса А. Наследник не может ни удалить поле родительского класса, ни изменить его тип. Наследник может лишь добавить собственные поля к уже имеющимся полям родителя. Таким образом, объекты класса наследника обладают всеми свойствами родителя и возможно дополнительным набором свойств.
Как создаются объекты класса наследника? Конструктор этого класса первым делом вызывает конструктор родителя, и тот создает объект родителя - коробочку с набором родительских полей. Только после этого конструктор наследника добавляет, если они есть, собственные поля к уже созданному объекту.
Все сказанное относится лишь к "настоящим" классам, представляющим ссылочный тип. Для развернутых классов, заданных структурой, отношение наследования не определено. Структуры могут иметь в качестве родительских классов лишь интерфейсы.
Поля класса являются глобальными переменными класса. Они видимы во всех методах этого класса. Каждый метод класса может читать и изменять значение любого поля класса независимо от того, какие атрибуты доступа установлены для полей и методов класса.
Если в теле метода объявлена локальная переменная, имя которой совпадает с именем поля класса, то такая ситуация не приводит к ошибке, поскольку конфликт имен разрешим. К полю класса можно добраться, используя уточненное имя поля с префиксом this, задающим имя текущего объекта.
Поля класса видимы не только в пределах самого класса. Если в некотором классе В объявлен и создан объект класса А, то класс В является клиентом класса А. В классе клиенте у объекта видны лишь те поля класса, для которых в момент объявлении был задан атрибут доступа public, что делает эти поля общедоступными. Для классов наследников у объекта видны поля с атрибутами public или protected, но недоступны поля с атрибутом private.
Имеет место следующая иерархия доступности полей объекта в зависимости от значения атрибута доступа - public, protected, private. В самом классе доступны все поля. У наследников не доступны закрытые поля с атрибутом private, у клиентов не доступны закрытые поля и защищенные поля - поля с атрибутами private и protected.
Где еще могут объявляться переменные? Во многих языках программирования переменные могут объявляться на уровне модуля. Такие переменные называются глобальными. Их область действия распространяется, по крайней мере, на весь модуль. Глобальные переменные играют важную роль, поскольку они обеспечивают весьма эффективный способ обмена информацией между различными частями модуля. Обратная сторона эффективности аппарата глобальных переменных - их опасность. Если какая-либо процедура, в которой доступна глобальная переменная, некорректно изменит ее значение, то ошибка может проявиться в другой процедуре, использующей эту переменную. Найти причину ошибки бывает чрезвычайно трудно. В таких ситуациях приходится проверять работу многих компонентов модуля.
В языке C# роль модуля играют классы, пространства имен, проекты, решения. Поля классов, о которых шла речь выше, могут рассматриваться как глобальные переменные класса. Но здесь у них особая роль. Данные, хранимые в полях класса, являются тем центром, вокруг которого вращается мир класса. Методы класса в этом мире, можно сказать, играют второстепенную роль - они обрабатывают данные. Заметьте, каждый экземпляр класса - это отдельный мир. Поля экземпляра (открытые, защищенные и закрытые) - это глобальная информация, которая доступна всем методам класса.
Статические поля класса хранят информацию, общую для всех экземпляров класса. Они представляют определенную опасность, поскольку каждый экземпляр способен менять их значения.
В других видах модуля - пространствах имен, проектах, решениях - нельзя объявлять переменные. В пространствах имен в языке C# разрешено только объявление классов и их частных случаев: структур, интерфейсов, делегатов, перечислений. Поэтому глобальных переменных уровня модуля, в привычном для других языков программирования смысле, в языке C# нет. Классы не могут обмениваться информацией, используя глобальные переменные. Все взаимодействие между ними обеспечивается способами, стандартными для объектного подхода. Между классами могут существовать два типа отношений - клиентские и наследования, а основной способ инициации вычислений - это вызов метода для объекта-цели или вызов обработчика события. Поля класса и аргументы метода позволяют передавать и получать нужную информацию. Устранение глобальных переменных на уровнях более высоких, чем класс, существенно повышает надежность создаваемых на языке C# программных продуктов, поскольку устраняется источник опасных, трудно находимых ошибок.
Перейдем теперь к рассмотрению локальных переменных. Во всех языках программирования, в том числе и в C#, основной контекст, в котором появляются переменные, - это процедуры и функции - методы класса. Тело метода, заключенное в фигурные скобки, будем называть процедурным блоком. Переменные, объявленные в процедурном блоке, называются локальными - они локализованы в методе.
В некоторых языках, например в Паскале, локальные переменные должны быть объявлены в вершине процедурного блока. Иногда это правило заменяется менее жестким, но, по сути, аналогичным правилом - где бы внутри процедурного блока ни была объявлена переменная, она считается объявленной в вершине блока и ее область видимости распространяется на весь процедурный блок. В C# принята другая стратегия. Переменную можно объявлять в любой точке процедурного блока. Область ее видимости распространяется от точки объявления до конца процедурного блока.
На самом деле, ситуация с процедурным блоком в C# не так проста. Процедурный блок имеет сложную структуру; в него могут быть вложены другие блоки, связанные с операторами выбора, цикла и так далее. В каждом таком блоке, в свою очередь, допустимы вложения блоков. В каждом внутреннем блоке допустимы объявления переменных. Переменные, объявленные во внутренних блоках, локализованы именно в этих блоках, их область видимости и время жизни определяются этими блоками. Локальные переменные, объявленные в любом внутреннем блоке, существуют от точки объявления до конца соответствующего блока.
Рассмотрим ситуацию с возможными конфликтами имен, появляющихся в различных блоках. Уже говорилось, что имя локальной переменной может совпадать с именем поля класса. Этот конфликт разрешен, поскольку для поля класса можно использовать уточненное имя. Чтобы избежать других конфликтов, не разрешается во внутреннем блоке метода объявлять локальную переменную, имя которой совпадает с именем формального параметра метода или с именем локальной переменной, объявленной в охватывающем блоке.
Добавим в наш проект новый класс 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.7) Форма FormLocals - интерфейс пользователя Назначение формы поясняется в специальном текстовом окне. В разделе "исходные данные" два текстовых окна позволяют задать данные, необходимые для формирования объекта класса TestingLocals. Две командные кнопки позволяют создать объект этого класса и вызвать метод Test, тело которого представляет систему вложенных внутренних блоков, содержащих объявление локальных переменных. Большой раздел в интерфейсе формы занимают советы, подсказывающие разработчику, что он может делать при объявлении локальных переменных и что является недопустимым.
Напомню, что код интерфейсного класса, создаваемый по умолчанию, состоит из двух частей. Одна часть предназначена для Дизайнера форм, и код в ней появляется автоматически, отражая проектирование дизайна формы, выполняемое руками. Другая часть ориентирована на разработчика интерфейса. В эту часть класса добавляются поля, необходимые для обмена информацией с элементами управления, расположенными на форме. Здесь же находится поле, в котором объявлен объект класса TestingLocals. Заметьте, если создается интерфейсный класс, обеспечивающий поддержку работы с одним или несколькими содержательными классами, то в интерфейсном классе должны быть поля с объектами этих классов. Так интерфейсный класс становится клиентом содержательного класса. В нашем случае интерфейсный класс 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 есть четыре наследуемых метода:
Естественно, что все встроенные типы нужным образом переопределяют методы родителя и добавляют собственные методы и свойства. Учитывая, что и классы, создаваемые пользователем, также являются потомками класса 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)
{
FormLocals testLocalsForm = new FormLocals();
testLocalsForm.Show();
}
В проекте с множеством форм одновременно на экране может быть открыто несколько форм. Может ли пользователь переключаться на работу с той или иной формой? Все зависит от того, как открыта форма. Каждую форму можно открыть как модальную или как немодальную. Хотя термин "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, закрываются все открытые к этому времени формы и приложение заканчивает свою работу. Когда же закрывается любая другая форма, закрывается только эта форма, остальные формы остаются открытыми.
Указание к задаче. Представьте выполняемый блок оператором цикла следующего вида:
do
{
…
}while((answer=="Да") || (answer=="да" ));
Здесь answer - переменная, содержащая ответ пользователя на предложение продолжения работы.Назначение задач этого раздела состоит в знакомстве с основными скалярными встроенными типами, их классификацией, диапазоном возможных значений. Решение задач требует умения объявлять, вводить и выводить значения переменных этих типов.
В эадачах этого раздела необходимо организовать контроль данных, вводимых пользователем.
В практикуме показывается пример создания простой программы в Visual Studio 2010 на языке C#.
Проект к данной лекции Вы можете скачать здесь.
Знакомство с новым языком программирования разумно начинать с изучения системы типов этого языка. Как в нем устроена система типов данных? Какие имеются простые типы, как создаются сложные, структурные типы, как определяются собственные типы,
В первых языках программирования понятие класса отсутствовало - рассматривались только типы данных. При определении типа явно задавалось только множество возможных значений, которые могут принимать переменные этого типа. Например, утверждалось, что тип 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. Знать все типы из этой библиотеки практически невозможно, но умение ориентироваться в ней необходимо.
Типы данных разделяются также на статические и динамические. Для переменных 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.Кроме того, в языке есть
Язык позволяет конструировать типы.
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# однозначно отображаются, а фактически совпадают с системными типами каркаса Net Framework, размещенными в пространстве имен System. Поэтому всюду, где можно использовать имя типа, например, int, с тем же успехом можно использовать и имя System.Int32.
В заключение этого раздела приведу таблицу, содержащую описание базисных встроенных типов языка C# и их основные характеристики.
| Логический тип | |||
|---|---|---|---|
| Имя типа | Системный тип | Значения | Размер |
| bool | System.Boolean | true, false | 8 бит |
| Арифметические целочисленные типы | |||
| Имя типа | Системный тип | Диапазон | Размер |
| sbyte | System.SByte | [-128, 127] | Знаковое, 8-бит |
| byte | System.Byte | [0, 255] | Беззнаковое, 8-бит |
| short | System.Int16 | [-32768, 32767] | Знаковое, 16-бит |
| ushort | System.UInt16 | [0, 65535] | Беззнаковое, 16-бит |
| int | System.Int32 | [-231, 231] | Знаковое, 32-бит |
| uint | System.UInt32 | [0, 232] | Беззнаковое, 32-бит |
| long | System.Int64 | [-263 , 263] | Знаковое, 64-бит |
| ulong | System.UInt64 | $$\approx(0, 2^{64})$$ | Беззнаковое, 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 <имя конструктора>([<список аргументов>])
Такая конструкция инициализатора, применимая и для скалярных переменных
Хороший стиль программирования требует, чтобы все простые переменные объявлялись с инициализацией. Дело в том, что при объявлении без инициализации значение переменной остается неопределенным, и всякая попытка использовать такую переменную в вычислениях, где требуется значение переменной, является ошибкой, которую компилятор обнаруживает еще на этапе компиляции. Конечно, можно объявить переменную без инициализации, а потом в процессе вычислений присвоить ей значение, тем самым инициализируя ее.
Заметьте, компилятор строго следит за тем, чтобы в вычислениях не появлялись переменные, не имеющие значения. Если присвоение значения переменной происходит внутри одной из ветвей оператора 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);
В заключение взгляните на результаты работы этого тестового примера.
(рис 2.1) Типы, допускающие значение nullЗаметьте, при выводе на печать значение null не имеет специального обозначения и выводится как пробел.
Значение 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. Показано, как при умножении появляются значения бесконечности, а также появляется одна из соответствующих констант, задающих бесконечность. В заключение взгляните на результаты работы этого тестового примера.
(рис 2.2) Значения null, NaN, Infinity
Продолжу приводить примеры и сейчас подробнее остановлюсь на том, как строилось Решение (Solution) с именем Ch2, в котором размещены проекты этой лекции. Наряду с консольным проектом ConsoleNullable, примеры из которого приводились в предыдущем разделе, создадим Windows-проект с именем SimpleVariables. В этом проекте почти не будет никаких вычислений. Его главная задача - продемонстрировать различные способы объявления простых переменных встроенных
Побочная цель состоит в том, чтобы показать работу проекта, включающего несколько форм - интерфейсных классов. Проект продемонстрирует часто встречающуюся на практике ситуацию, когда в нем есть
Поскольку в первой лекции подробно рассказывалось о том, как создаются Windows-проекты и первые шаги работы с ними, останавливаться на этом уже не буду. На рис 2.3 показана главная форма на начальном этапе проектирования.
(рис 2.3) Главная форма на начальных шагах процесса ее проектированияЧто же было сделано при проектировании главной формы? Прежде всего, она была переименована и получила содержательное имя. Переименован был и файл, содержащий описание интерфейсного класса, задающего форму. На форме размещена поясняющая надпись "Главная форма" и три командные кнопки.
Для задания надписей, поясняющих назначение формы, можно использовать элемент управления Label или текстовое окно - TextBox. В нашем примере использовалась метка.
Проектируемая главная форма является примером
Подведем итоги. Если в проекте предполагается n различных сервисов, каждый из которых требует собственного интерфейса, то в проект наряду с главной формой, создаваемой по умолчанию в момент создания проекта, добавляются n интерфейсных классов - наследников класса Form, каждый из которых имеет собственную форму. Каждая такая форма заселяется элементами управления, задавая интерфейс соответствующего сервиса. На главной форме располагаются n командных кнопок, а в код интерфейсного класса, задающего главную форму, добавляются n полей, каждое из которых содержит объявление объекта соответствующего интерфейсного класса. Когда пользователь выбирает в главной форме командную кнопку, то обработчик события Click этой кнопки вызывает конструктор интерфейсного класса и создает реальный объект этого интерфейсного класса и реальную форму, связанную с объектом.
Затем в обработчике вызывается метод Show этого объекта, соответствующая форма открывается, показывается на экране дисплея, и пользователь начинает работать с интерфейсом формы. Такая схема работы встречается на практике достаточно часто, так что проекты с главной кнопочной формой будут появляться неоднократно.
Продемонстрирую различные аспекты применения скалярных типов и объявления переменных. В проекте роль сервисов, предоставляемых пользователю, будут играть три различных примера - три теста. Каждый тест имеет свою цель и свой интерфейс, позволяющий конечному пользователю проводить исследования возможностей и особенностей объявления скалярных переменных. Для каждого теста будет создан свой интерфейсный класс и спроектирована форма, связанная с этим классом. Каждая командная кнопка главной формы будет запускать свой тест. Обработчик события Click каждой из командных кнопок будет открывать форму, спроектированную для работы с выбранным тестом нашего проекта.
Какова идея этого теста? Давайте дадим конечному пользователю возможность в текстовых окнах формы задать имя скалярного типа и значение, соответствующее этому типу. После этого пользователь может нажать командную кнопку "Ввод", спроектированную в интерфейсе формы. Обработчик события Click командной кнопки "Ввод" должен построить переменную заданного типа и присвоить ей значение, заданное пользователем. Если корректно указано имя типа и значение, то операция пройдет успешно, о чем пользователю и будет выдано соответствующее сообщение. В качестве допустимых типов разрешается указывать любой допустимый в C# скалярный тип, заданный таблицей 2.1. В качестве допустимого значения разрешается указывать любое значение из диапазона, соответствующего выбранному типу, и представленному в таблице 2.1. В случае некорректной работы пользователя появляется сообщение "Неправильно указано имя типа" или, если значение не соответствует заданному типу, должно выдаваться сообщение об ошибке.
Спроектируем в интерфейсе пользователя и командную кнопку "Вывод", по нажатию которой будет выдаваться значение, хранимое в созданной переменной.
Тест должен позволить пользователю изучить все скалярные типы языка C#, понять, какие значения допустимы для каждого типа, увидеть, как можно вводить и выводить значения переменных скалярного типа.
Еще одна важная роль этого теста состоит в демонстрации правильной организации ввода данных с использованием программной конструкции try - catch блоков. Ввод данных, задаваемых пользователем, всегда должен контролироваться, поскольку человеку свойственно ошибаться. Тест показывает, как можно обнаруживать ошибки ввода.
В соответствии с задачами теста спроектируем интерфейс пользователя. Прежде всего в проект нужно добавить новый интерфейсный класс, сопровождаемый формой. Такие классы являются наследниками класса Form из библиотеки FCL. Воспользуемся пунктом меню Project|AddWindowsForm для добавления в проект интерфейсного класса и ,соответственно, новой формы. Отслеживая наши действия, инструментальное средство Designer Form добавит в наш проект необходимые классы. Следуя правилам стиля, произведем переименование, заменив стандартные имена содержательными.
Теперь можно заняться непосредственным проектированием пользовательского интерфейса, размещая в форме нужные элементы управления. Не буду в деталях описывать весь процесс проектирования, поскольку интерфейс достаточно прост. Он отвечает заданной функциональности и включает известные элементы управления - метки, текстовые окна, командные кнопки и контейнеры GroupBox. Контейнеры в формах позволяют придать пользовательскому интерфейсу нужную структуру, объединяя в группу элементы управления, решающие общую задачу - например, ввода исходных данных или вывода результатов.
На рис 2.4 показана спроектированная форма в процессе работы с ней.
(рис 2.4) Форма TestTypes в процессе работыПрежде чем продолжить рассмотрение теста, давайте поговорим об основных принципах, лежащих в основе современного визуального стиля программирования. На начальных этапах программирования работой программы полностью управлял ее текст. Так, в программах на языке Алгол выполнение программы начиналось с оператора begin (начало) и заканчивалось оператором end (конец). Развитие программирования потребовало диалога с пользователем в ходе выполнения программы. Вначале это был простой диалог, характерный для уже рассмотренных нами консольных приложений, когда выполнение программы приостанавливается, и она ждет ответа пользователя, вводимого с консоли. Дальнейшее развитие программирования привело к визуальному стилю, когда программные объекты стали иметь визуальные образы, когда появились графические объекты. Графические образы более информативны, чем текстовые.
Визуальный стиль изменил и стиль диалога с пользователем, позволив строить интерфейс пользователя, основанный на объектах, имеющих визуальный образ. Простейшим и классическим примером является объект класса TextBox, графическим образом которого является текстовое окно - оно может служить как для вывода текстов в окно, так и для ввода текста пользователем. Этот объект обладает мощной функциональностью текстового редактора, позволяет пользователю редактировать вводимый им текст, удаляя, заменяя и вставляя символы в произвольное место текста.
В библиотеке FCL имеется большое число классов, предназначенных для организации пользовательского интерфейса. Со многими из них будем знакомиться по ходу изучения нашего курса. Эти интерфейсные классы и соответствующие объекты получили название элементов управления (control). Класс Control из этой библиотеки обеспечивает базовую функциональность всех элементов управления и является родительским классом для всех интерфейсных классов. Класс Form, который неоднократно упоминался, также является непрямым наследником класса Control и прямым наследником класса ContainerControl. Последний класс определяет базовую функциональность тех элементов управления, которые, как и формы, есть контейнеры, допускающие внутри графического образа размещение образов других элементов управления. Кроме формы, уже известным нам примером контейнера служит класс GroupBox.
Выше говорилось, что проектирование пользовательского интерфейса, как правило, ведется не программным путем, хотя и такое возможно, а "руками". В Visual Studio 2008 имеется специальная инструментальная панель с элементами управления, которые можно перетаскивать на форму в процессе проектирования, располагать эти элементы в нужном месте формы, менять их размеры, устанавливать их свойства, открываемые в окне свойств. Дизайнер формы транслирует все эти действия в соответствующий программный код.
Процедура Main, задающая точку входа в Windows-проект, открывает главную форму, и пользователь попадает в спроектированный для этой формы мир графических объектов. Теперь пользователь становится у руля управления ходом выполнения проекта и является "возмутителем спокойствия" в этом мире объектов. Он начинает вводить тексты в текстовые окна, выбирать нужные ему элементы из открывающихся списков, нажимать командные кнопки и выполнять другие действия над элементами управления. Каждое такое действие пользователя приводит к возникновению соответствующего события у программного объекта. Так, когда пользователь изменяет текст в текстовом окне, у соответствующего объекта класса TextBox возникает событие TextChanged, при нажатии командной кнопки у объекта возникает событие Click, двойной щелчок по командной кнопке приводит к событию DblClick.
В ответ на возникновение события объект посылает сообщение операционной системе. Обрабатывая очередь сообщений, операционная система отыскивает обработчик события, если объект предусмотрел таковой, и передает ему управление.
Подключить к объекту обработчик события можно визуально в процессе проектирования элемента управления. В окне свойств элемента управления можно перейти к списку событий этого элемента и в этом списке выбрать (включить) нужное событие. На рис 2.5 показан момент проектирования формы, когда для выбранного элемента управления buttonInput в окне его свойств отображается список возможных событий этого элемента.
(рис 2.5) Список событий элемента управления buttonInputМожно видеть, что для этого элемента включено событие Click. Заметьте, имя обработчика события строится из имен элемента управления и события, разделенных знаком подчеркивания. У каждого элемента управления есть одно основное событие, которое можно включить простым щелчком по элементу управления, например, для командных кнопок таким событием является событие Click.
При включении события интерфейсный класс автоматически дополняется специальным методом, содержащим заготовку обработчика события -
С точки зрения объектного программирования обработчики событий - это специальные методы интерфейсных классов (события), особенность которых состоит в том, что они вызываются в нужный момент операционной системой в ответ на возникновение события у соответствующего объекта, инициированного работой пользователя.
Стиль программирования, основанный на проектировании визуального пользовательского интерфейса, называется визуальным программированием. Стиль программирования, основанный на событиях и системных вызовах обработчиков событий, называется событийно-управляемым программированием (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.
Конечно, плохо, когда в процессе работы программы возникает исключение. Однако его появление еще не означает, что программа не сможет выполнить свой контракт. Исключение может быть нужным образом обработано, после чего продолжится нормальный ход вычислений приложения. Гораздо хуже, когда возникают ошибки в работе, не приводящие к исключениям. Тогда работа продолжается с неверными данными без исправления ситуации и даже без уведомления о возникновении ошибки.
Наш пример продемонстрировал первое применение
Пользователь, работающий с программой, имеет право на ошибку. Программист, создающий программу, такого права не имеет. Если из-за ошибки пользователя программа перестает работать, то в этом вина программиста, поскольку его программа не смогла вовремя обнаружить ошибку и дать возможность пользователю исправить ее. Ошибки пользователя чаще всего возникают, когда пользователь вводит исходные данные, необходимые программе. Поэтому любой пользовательский ввод должен контролироваться. Операторы,
В рассмотренном выше примере пользовательский ввод контролируется. Поэтому стоит еще раз проанализировать этот пример с позиций того, как организован контроль ввода данных, вводимых пользователем.
Этот тест демонстрирует объявление переменных, часть из которых инициализируется в момент объявления. Пользователю в текстовом окне показывается некоторый фрагмент программы, содержащий объявления переменных и простейшие присваивания. Задача пользователя - в текстовые окна, спроектированные для всех переменных фрагмента, ввести их значения, которые они получат в результате выполнения фрагмента. После чего он может нажать командную кнопку и сравнить введенные им результаты с теми, что дает реальное вычисление.
На рис 2.6 показана спроектированная форма в процессе работы.
(рис 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) класса. О классах и полях предстоит еще обстоятельный разговор, а сейчас сообщу лишь некоторые минимальные сведения, связанные с рассматриваемой темой.
Все переменные, объявленные на уровне класса при его описании, являются полями этого класса.
Поскольку класс, как уже многократно говорилось, задает описание типа данных, то поля класса задают представление этих данных. Необходимо крайне внимательно относиться к проектированию полей класса - всякие "лишние" объявления на этом уровне крайне нежелательны.
Когда конструктор класса создает очередной объект - экземпляр класса, то он в памяти создает набор полей, определяемых классом, и записывает в них значения, характеризующие свойства данного конкретного экземпляра. Так что каждый объект в памяти можно рассматривать как набор соответствующих полей класса со своими значениями. Заметьте, для классов, представленных структурой, объект создается аналогичным способом, но разворачивается в стеке.
Объекты в динамической памяти, с которыми не связана ни одна ссылочная переменная, становятся недоступными. Реально они оканчивают свое существование, когда сборщик мусора (
Есть одно важное исключение. Некоторые поля могут жить дольше. Если при объявлении класса поле объявлено с модификатором static, то такое поле является частью модуля, связанного с классом, и не входит в состав его экземпляров. Поэтому static -поля живут так же долго, как и сам класс. Более подробно эти вопросы будут обсуждаться при рассмотрении классов, структур, интерфейсов.
Наследование классов - это одно из важнейших отношений, существующих между классами одного проекта. О нем мы будем говорить подробно, а сейчас рассмотрим только одну сторону наследования - что происходит с полями классов в процессе наследования. Пусть класс В является наследником класса А. Тогда класс В наследует все поля класса А. Наследник не может ни удалить поле родительского класса, ни изменить его тип. Наследник может лишь добавить собственные поля к уже имеющимся полям родителя. Таким образом, объекты класса наследника обладают всеми свойствами родителя и возможно дополнительным набором свойств.
Как создаются объекты класса наследника? Конструктор этого класса первым делом вызывает конструктор родителя, и тот создает объект родителя - коробочку с набором родительских полей. Только после этого конструктор наследника добавляет, если они есть, собственные поля к уже созданному объекту.
Все сказанное относится лишь к "настоящим" классам, представляющим ссылочный тип. Для развернутых классов, заданных структурой, отношение наследования не определено. Структуры могут иметь в качестве родительских классов лишь интерфейсы.
Поля класса являются глобальными переменными класса. Они видимы во всех методах этого класса. Каждый метод класса может читать и изменять значение любого поля класса независимо от того, какие атрибуты доступа установлены для полей и методов класса.
Если в теле метода объявлена локальная переменная, имя которой совпадает с именем поля класса, то такая ситуация не приводит к ошибке, поскольку конфликт имен разрешим. К полю класса можно добраться, используя уточненное имя поля с префиксом this, задающим имя текущего объекта.
Поля класса видимы не только в пределах самого класса. Если в некотором классе В объявлен и создан объект класса А, то класс В является клиентом класса А. В классе клиенте у объекта видны лишь те поля класса, для которых в момент объявлении был задан атрибут доступа public, что делает эти поля общедоступными. Для классов наследников у объекта видны поля с атрибутами public или protected, но недоступны поля с атрибутом private.
Имеет место следующая иерархия доступности полей объекта в зависимости от значения атрибута доступа - public, protected, private. В самом классе доступны все поля. У наследников не доступны закрытые поля с атрибутом private, у клиентов не доступны закрытые поля и защищенные поля - поля с атрибутами private и protected.
Где еще могут объявляться переменные? Во многих языках программирования переменные могут объявляться на уровне модуля. Такие переменные называются глобальными. Их область действия распространяется, по крайней мере, на весь модуль. Глобальные переменные играют важную роль, поскольку они обеспечивают весьма эффективный способ обмена информацией между различными частями модуля. Обратная сторона эффективности аппарата глобальных переменных - их опасность. Если какая-либо процедура, в которой доступна глобальная переменная, некорректно изменит ее значение, то ошибка может проявиться в другой процедуре, использующей эту переменную. Найти причину ошибки бывает чрезвычайно трудно. В таких ситуациях приходится проверять работу многих компонентов модуля.
В языке C# роль модуля играют классы, пространства имен, проекты, решения. Поля классов, о которых шла речь выше, могут рассматриваться как глобальные переменные класса. Но здесь у них особая роль. Данные, хранимые в полях класса, являются тем центром, вокруг которого вращается мир класса. Методы класса в этом мире, можно сказать, играют второстепенную роль - они обрабатывают данные. Заметьте, каждый экземпляр класса - это отдельный мир. Поля экземпляра (открытые, защищенные и закрытые) - это глобальная информация, которая доступна всем методам класса.
Статические поля класса хранят информацию, общую для всех экземпляров класса. Они представляют определенную опасность, поскольку каждый экземпляр способен менять их значения.
В других видах модуля - пространствах имен, проектах, решениях - нельзя объявлять переменные. В пространствах имен в языке C# разрешено только объявление классов и их частных случаев: структур, интерфейсов, делегатов, перечислений. Поэтому глобальных переменных уровня модуля, в привычном для других языков программирования смысле, в языке C# нет. Классы не могут обмениваться информацией, используя глобальные переменные. Все взаимодействие между ними обеспечивается способами, стандартными для объектного подхода. Между классами могут существовать два типа отношений - клиентские и наследования, а основной способ инициации вычислений - это вызов метода для объекта-цели или вызов обработчика события. Поля класса и аргументы метода позволяют передавать и получать нужную информацию. Устранение глобальных переменных на уровнях более высоких, чем класс, существенно повышает надежность создаваемых на языке C# программных продуктов, поскольку устраняется источник опасных, трудно находимых ошибок.
Перейдем теперь к рассмотрению локальных переменных. Во всех языках программирования, в том числе и в C#, основной контекст, в котором появляются переменные, - это процедуры и функции - методы класса. Тело метода, заключенное в фигурные скобки, будем называть процедурным блоком. Переменные, объявленные в процедурном блоке, называются локальными - они локализованы в методе.
В некоторых языках, например в Паскале, локальные переменные должны быть объявлены в вершине процедурного блока. Иногда это правило заменяется менее жестким, но, по сути, аналогичным правилом - где бы внутри процедурного блока ни была объявлена переменная, она считается объявленной в вершине блока и ее область видимости распространяется на весь процедурный блок. В C# принята другая стратегия. Переменную можно объявлять в любой точке процедурного блока. Область ее видимости распространяется от точки объявления до конца процедурного блока.
На самом деле, ситуация с процедурным блоком в C# не так проста. Процедурный блок имеет сложную структуру; в него могут быть вложены другие блоки, связанные с операторами выбора, цикла и так далее. В каждом таком блоке, в свою очередь, допустимы вложения блоков. В каждом внутреннем блоке допустимы объявления переменных. Переменные, объявленные во внутренних блоках, локализованы именно в этих блоках, их область видимости и время жизни определяются этими блоками. Локальные переменные, объявленные в любом внутреннем блоке, существуют от точки объявления до конца соответствующего блока.
Рассмотрим ситуацию с возможными конфликтами имен, появляющихся в различных блоках. Уже говорилось, что имя локальной переменной может совпадать с именем поля класса. Этот конфликт разрешен, поскольку для поля класса можно использовать уточненное имя. Чтобы избежать других конфликтов, не разрешается во внутреннем блоке метода объявлять локальную переменную, имя которой совпадает с именем формального параметра метода или с именем локальной переменной, объявленной в охватывающем блоке.
Добавим в наш проект новый класс 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.7) Форма FormLocals - интерфейс пользователя Назначение формы поясняется в специальном текстовом окне. В разделе "исходные данные" два текстовых окна позволяют задать данные, необходимые для формирования объекта класса TestingLocals. Две командные кнопки позволяют создать объект этого класса и вызвать метод Test, тело которого представляет систему вложенных внутренних блоков, содержащих объявление локальных переменных. Большой раздел в интерфейсе формы занимают советы, подсказывающие разработчику, что он может делать при объявлении локальных переменных и что является недопустимым.
Напомню, что код интерфейсного класса, создаваемый по умолчанию, состоит из двух частей. Одна часть предназначена для Дизайнера форм, и код в ней появляется автоматически, отражая проектирование дизайна формы, выполняемое руками. Другая часть ориентирована на разработчика интерфейса. В эту часть класса добавляются поля, необходимые для обмена информацией с элементами управления, расположенными на форме. Здесь же находится поле, в котором объявлен объект класса TestingLocals. Заметьте, если создается интерфейсный класс, обеспечивающий поддержку работы с одним или несколькими содержательными классами, то в интерфейсном классе должны быть поля с объектами этих классов. Так интерфейсный класс становится клиентом содержательного класса. В нашем случае интерфейсный класс 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 есть четыре наследуемых метода:
Естественно, что все встроенные типы нужным образом переопределяют методы родителя и добавляют собственные методы и свойства. Учитывая, что и классы, создаваемые пользователем, также являются потомками класса 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)
{
FormLocals testLocalsForm = new FormLocals();
testLocalsForm.Show();
}
В проекте с множеством форм одновременно на экране может быть открыто несколько форм. Может ли пользователь переключаться на работу с той или иной формой? Все зависит от того, как открыта форма. Каждую форму можно открыть как модальную или как немодальную. Хотя термин "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, закрываются все открытые к этому времени формы и приложение заканчивает свою работу. Когда же закрывается любая другая форма, закрывается только эта форма, остальные формы остаются открытыми.
Указание к задаче. Представьте выполняемый блок оператором цикла следующего вида:
do
{
…
}while((answer=="Да") || (answer=="да" ));
Здесь answer - переменная, содержащая ответ пользователя на предложение продолжения работы.Назначение задач этого раздела состоит в знакомстве с основными скалярными встроенными типами, их классификацией, диапазоном возможных значений. Решение задач требует умения объявлять, вводить и выводить значения переменных этих типов.
В эадачах этого раздела необходимо организовать контроль данных, вводимых пользователем.
В практикуме показывается пример создания простой программы в Visual Studio 2010 на языке C#.
Для получения официальных документов о завершении программы дополнительного профессионального образования (удостоверения о повышении квалификации, дипломов о профессиональной переподготовке и MBA) необходимо предоставить:
Внимание! Вы можете не заказывать доставку бумажной версии официального документы, а скачать его в электронном виде и распечатать самостоятельно. Информация о выданном документе в течение 1 месяца загружается в Федеральную информационную систему «Федеральный реестр сведений о документах об образовании и (или) о квалификации, документах об обучении» - ФИС ФРДО.
Доступ на новый сайт осуществляется с использованием адреса электронной почты, который был указан вами при регистрации на "старом". Мы постарались перенести все ваши данные с прежнего ресурса, однако не исключена вероятность потери части информации.
При возникновении проблемы со входом, воспользуйтесь функцией сброса пароля
Если вы обнаружите несоответствия, пожалуйста, сообщите нам.