ГГУ им. Ф. Скорины

http://www.gsu.by

Разработка Web-приложений ASP .NET с использованием
Visual Studio .NET

3. Лекция: Основы языка C#: версия для
печати и PDA
Рассматривается система
типов языка C#, приводятся отличия и особенности ссылочных и
значимых типов данных, контейнерных типов и коллекций.
Рассматриваются вопросы выполнения основных операций преобразования
между различными типами данных, а также использования динамических
массивов и коллекций. Рассматриваются основные принципы работы со
строками, ориентированные на решение ряда практических задач,
определяются принципы описания, вызова и передачи параметров в
процедуры и функции. Рассматриваются классы, описание их полей,
методов и свойств, их отличия от структур.

Цель лекции: познакомиться с основными отличительными
особенностями языка C#, рассмотреть примеры использования новых
средств и операторов языка, типов данных и их преобразований в
объеме, необходимом для дальнейшего изучения материала.

Основные операторы языка C#

Состав и синтаксис операторов C# унаследован от языка
С++. Тем не менее существует ряд отличий, которые улучшают некоторые
характеристики C++, делая его более легким в использовании.
Предполагая, что читатель уже знаком с основными операторами языка
C++ и имеет минимальный опыт программирования, остановимся на
рассмотрении только наиболее значимых операторов C#, а также
операторов, специфических для данного языка программирования.

Цикл foreach

Новым видом цикла, который появился в C# и отсутствует в
C++, является цикл foreach. Он удобен
при работе с массивами, коллекциями и другими контейнерами данных.
Синтаксис оператора выглядит следующим образом:

foreach (тип идентификатор in контейнер) оператор

Тело цикла выполняется для каждого идентификатора в
контейнере. Тип идентификатора должен быть согласован с типом
элементов, хранящихся в контейнере. На каждом шаге цикла
идентификатор, задающий текущий элемент контейнера, получает
значение очередного элемента в соответствии с порядком,
установленным на элементах контейнера. С использованием этого
текущего элемента и выполняется тело цикла. Количество шагов цикла
равно количеству элементов, находящихся в контейнере. Таким образом,
цикл заканчивается в тот момент, когда были перебраны все элементы
контейнера.

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

В следующем примере демонстрируется работа с двумерным
массивом, который в начале заполняется случайными числами с помощью
цикла for, затем с помощью цикла foreach подсчитывается сумма всех элементов
массива, а также находятся минимальный и максимальный элементы.

public void SumMinMax()
{
int[,] myArray = new int[10, 10];
Random rnd = new Random();
for (int i = 0; i < 10; i++)
{
for (int j = 0; j < 10; j++)
{
myArray[i, j] = rnd.Next(100);
Response.Write(myArray[i, j] + " ");
}
Response.Write("<br/>");
}
long sum = 0;
int min = myArray[0, 0];
int max = myArray[0, 0];
foreach (int i in myArray)
{
sum += i;
if (i > max) max = i;
if (i < min) min = i;
}
Response.Write("Sum=" + sum.ToString() + " Min=" +
min.ToString() + " Max=" + max.ToString());
}

Типы данных. Преобразования типов

Типы данных принято разделять на простые и сложные в
зависимости от того, как устроены их данные. У простых (скалярных) типов возможные значения
данных едины и неделимы. Сложные типы
характеризуются способом структуризации данных — одно значение сложного типа состоит из множества
значений данных, организующих сложный
тип.

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

Еще одна важная классификация типов — это их деление на
значимые и ссылочные. Для значимых типов значение
переменной (объекта) является неотъемлемой собственностью переменной
(точнее, собственностью является память, отводимая значению, а само
значение может изменяться). Для ссылочных
типов значением служит ссылка на некоторый объект в памяти,
расположенный обычно в динамической памяти - "куче". Объект, на который указывает ссылка, может быть разделяемым. Это
означает, что несколько ссылочных переменных могут указывать на один
и тот же объект и разделять его значения. Значимый тип принято называть развернутым,
подчеркивая тем самым, что значение объекта развернуто
непосредственно в памяти, отводимой объекту.

Для большинства процедурных языков, реально используемых
программистами - Паскаль, C++, Java, Visual Basic, C#, — система
встроенных типов более или менее
одинакова. Всегда в языке присутствуют арифметический, логический (булев),
символьный типы. Арифметический тип всегда разбивается на
подтипы. Всегда допускается организация данных в виде массивов и записей
(структур). Внутри арифметического типа всегда допускаются
преобразования, всегда есть функции, преобразующие строку в
число и обратно.

Поскольку язык C# является непосредственным потомком языка C++, то и системы типов этих двух языков близки и
совпадают вплоть до названий типов и областей их определения. Но
отличия, в том числе принципиального характера, есть и здесь.

Система типов

Давайте рассмотрим, как устроена система типов в языке C#, но вначале для
сравнения приведем классификацию типов в стандарте языка C++.

Стандарт языка C++ включает следующий набор
фундаментальных типов.

Логический тип — bool.

Символьный тип — char.

Целые типы. Целые типы могут
быть одного из трех размеров: short, int,
long, сопровождаемые описателем signed или unsigned, который указывает, как
интерпретируется значение — со знаком или без оного.

Типы с плавающей точкой. Эти
типы также могут быть одного из трех размеров — float, double, long double.

Тип void, используемый для указания на
отсутствие информации.

Указатели (например, int* — типизированный указатель на переменную типа int).

Ссылки (например, double& — типизированная ссылка на переменную типа double).

Массивы (например, char[] — массив элементов типа char).

Перечислимые типы enum для представления значений из
конкретного множества.

Структуры — struct.

Классы.

Первые три вида типов называются интегральными, или счетными. Значения их перечислимы и
упорядочены. Целые типы и типы с плавающей точкой относятся к
арифметическому типу. Типы подразделяются также на встроенные и типы, определенные
пользователем.

Эта схема типов сохранена и в языке C#. Однако здесь на
верхнем уровне используется и другая классификация, имеющая для C#
принципиальный характер. Согласно этой классификации, все типы можно
разделить на четыре категории:

Типы-значения — value, или значимые типы.

Ссылочные — reference.

Указатели — pointer.

Тип void.

Эта классификация основана на том, где и как хранятся
значения типов. Для ссылочного типа
значение задает ссылку на область
памяти в "куче", где расположен
соответствующий объект. Для значимого
типа используется прямая адресация, значение хранит
собственно данные, и память для них отводится, как правило, в стеке.

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

Особый статус имеет и тип void, указывающий на отсутствие какого-либо
значения.

В языке C# жестко определено, какие типы относятся к
ссылочным, а какие к значимым. К значимым типам относятся:
логический, арифметический, структуры, перечисление. Массивы, строки и классы относятся к ссылочным типам. На первый взгляд, такая
классификация может вызывать некоторое недоумение: почему это структуры, которые в C++ близки к классам, относятся к значимым типам, а массивы и строки — к ссылочным. Однако ничего удивительного
здесь нет. В C# массивы
рассматриваются как динамические, их размер может определяться на
этапе вычислений, а не в момент трансляции. Строки в C# также
рассматриваются как динамические переменные, длина которых может
изменяться. Поэтому строки и массивы относятся к ссылочным типам, требующим распределения
памяти в "куче".

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

Согласно принятой классификации все типы делятся на
встроенные и определенные пользователем. Все встроенные типы C# однозначно
отображаются, а фактически совпадают с системными типами каркаса
.NET Framework, размещенными в пространстве имен System. Поэтому всюду, где можно использовать
имя, например int, с тем же успехом
можно использовать и имя System.Int32.

Система встроенных
типов языка C# не только содержит практически все встроенные типы (за исключением long double) стандарта языка C++, но и
перекрывает его разумным образом. В частности, тип string является встроенным в язык, что вполне
естественно. В области совпадения сохранены имена типов, принятые в
C++, что облегчает жизнь тем, кто привык работать на C++, но
собирается по тем или иным причинам перейти на язык C#.

Язык C# в большей степени, чем язык C++, является языком
объектного программирования. В языке C# сглажено различие между
типом и классом. Все типы — встроенные и пользовательские —
одновременно являются классами,
связанными отношением наследования.
Родительским, базовым классом
является класс Object. Все
остальные типы или, точнее, классы
являются его потомками, наследуя
методы этого класса. У класса Object есть четыре наследуемых
метода:

bool Equals (object obj) —
проверяет эквивалентность текущего объекта и объекта, переданного
в качестве аргумента;

System.Type GetType() — возвращает
системный тип текущего объекта;

string ToString() — возвращает
строку, связанную с объектом. Для арифметических типов
возвращается значение, преобразованное в строку;

int GetHashCode() — служит как
хэш-функция в соответствующих алгоритмах поиска по ключу при
хранении данных в хэш-таблицах.

Естественно, что все встроенные типы нужным образом
переопределяют методы родителя и
добавляют собственные методы и
свойства. Учитывая, что и типы,
создаваемые пользователем, также являются потомками класса Object, для них необходимо переопределить
методы родителя, если
предполагается использование этих методов; реализация родителя,
предоставляемая по умолчанию, не обеспечивает нужного эффекта.

Рассмотрим несколько примеров. Начнем с вполне
корректного в языке C# примера объявления переменных и присваивания
им значений:

int x=11;
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 — это настоящий класс. Правильный ответ состоит в том,
что int — это и тип, и класс. В зависимости от контекста x может восприниматься как переменная типа
int или как объект класса int.
Это же верно и для всех остальных значимых типов. Стоит отметить,
что все значимые типы фактически
реализованы как структуры,
представляющие частный случай класса.

Такая двойственность в языке C# обусловлена тем, что
значимые типы эффективнее в
реализации, им проще отводить память, так что именно соображения
эффективности реализации заставили авторов языка сохранить значимые типы. Более важно, что зачастую
необходимо оперировать значениями, а не ссылками на них, хотя бы из-за различий в
семантике присваивания для переменных ссылочных и значимых типов.

С другой стороны, в определенном контексте крайне
полезно рассматривать переменные типа int как настоящие объекты и обращаться с ними
как с объектами. В частности, полезно иметь возможность создавать и
работать со списками, чьи элементы являются разнородными объектами,
в том числе и принадлежащими к значимым
типам.

Семантика присваивания

Рассмотрим присваивание: x =
e.

Чтобы присваивание было допустимым, типы переменной
x и выражения e должны быть согласованными. Пусть сущность
x согласно объявлению принадлежит классу T.
Будем говорить, что тип T основан на
классе T и является базовым типом x, так что базовый тип определяется классом
объявления. Пусть теперь в рассматриваемом нами присваивании
выражение e связано с объектом типа
T1.

Определение: тип T1 согласован по присваиванию с базовым
типом T переменной x, если класс
T1 является потомком класса T.

Присваивание допустимо, если и только если имеет место
согласование типов. Так как все
классы в языке C# — встроенные и
определенные пользователем — по определению являются потомками класса Object, то отсюда и следует наш частный
случай: переменным класса Object можно присваивать выражения любого
типа.

Несмотря на то, что обстоятельный разговор о
наследовании, родителях и потомках
нам еще предстоит, лучше с самого начала понимать отношения между
родительским классом и
классом-потомком, отношения между объектами этих классов.
Класс-потомок при создании наследует все свойства и методы родителя. Родительский класс не имеет возможности
наследовать свойства и методы, создаваемые его потомками. Наследование — это
односторонняя операция от родителя к потомку. Ситуация с присваиванием —
симметричная. Объекту родительского
класса присваивается объект класса-потомка. Объекту
класса-потомка не может быть присвоен объект родительского класса. Присваивание — это
односторонняя операция от потомка к
родителю. Одностороннее присваивание реально означает, что ссылочная
переменная родительского класса
может быть связана с любыми объектами, имеющими тип потомков родительского класса.

Например, пусть задан некоторый класс Parent,
а класс Child — его потомок, объявленный следующим
образом:

class Child:Parent {...}

Пусть теперь в некотором классе, являющемся клиентом классов Parent и Child,
объявлены переменные этих классов и
созданы связанные с ними объекты:

Parent p1 = new Parent(), p2 = new Parent();
Child ch1 = new Child(), ch2 = new Child();

Тогда допустимы присваивания р1 =
p2; p2= p1; ch1=ch2; ch2 = ch1 p1 = ch1; p1 = ch2

Но недопустимы присваивания ch1 =
p1; ch2 = p1; ch2 = p2;

Заметьте, ситуация не столь удручающая — сын может
вернуть себе переданный родителю объект, задав явное преобразование. Так что следующие
присваивания допустимы:

p1 = ch1; ... ch1 = (Child)p1;

Семантика присваивания справедлива и для другого
важного случая - при рассмотрении соответствия между формальными и
фактическими аргументами процедур и функций. Если формальный
аргумент согласно объявлению имеет тип T, а выражение, задающее фактический аргумент,
имеет тип T1, то имеет место согласование типов формального и
фактического аргументов, если и только если класс T1
является потомком класса T. Отсюда незамедлительно следует, что если
формальный параметр процедуры принадлежит классу Object, то фактический аргумент может быть
выражением любого типа.

Преобразование к типу object

Рассмотрим частный случай присваивания x = e; когда x
имеет тип object. В этом случае
гарантируется полная согласованность по
присваиванию — выражение e может иметь любой тип. В
результате присваивания значением переменной x становится ссылка на объект, заданный выражением
e. Заметьте, текущим типом x становится тип объекта, заданного выражением
e. Уже здесь проявляется одно из важных
различий между классом и типом.
Переменная, лучше сказать — сущность x,
согласно объявлению принадлежит классу Object, но ее тип — тип того объекта, с
которым она связана в текущий момент, — может динамически
изменяться.

Массивы, перечисления, коллекции

Массивы

Массивы в C# с точки зрения синтаксиса практически
ничем не отличаются от массивов в языке C++ и Java. Тем не менее
внутреннее устройство массивов C# сильно отличается от упомянутых
языков. Дело в том, что любой массив в C# является потомком класса
System.Array со всеми вытекающими
последствиями, связанными с наследованием каждым массивом свойств и
методов класса Array. Более подробно
класс Array рассматривается ниже.

Формально массив можно определить как набор однотипных
элементов, доступ к которым производится с помощью числового
индекса. Элементами массива могут быть значения произвольного типа,
в том числе массивы, классы, структуры и интерфейсы. Массивы могут
быть как одномерными, так и многомерными. Объявление массива
происходит путем помещения квадратных скобок после указания типа
данных для элемента массива.

Рассмотрим пример: создание массива d, состоящего из десяти элементов типа int. При этом необходимо учитывать тот факт,
что нумерация элементов начинается с нуля. Т. е. в данном случае
массив будет состоять из десяти элементов с номерами {0…9}:

int[] d;
d = new int[10];

Аналогичное предыдущему действие, требующее всего одной
строки кода:

int[] d = new int[10];

Еще один пример создания массива, состоящего из
двенадцати элементов типа string:

string[] st = new string[12];

При создании массивов необходимо учитывать, что для
создания массива фиксированной длины, как в предыдущих примерах,
необходимо всегда использовать ключевое слово new. Таким образом, следующее определение
массива недопустимо и приведет к возникновению ошибки еще на этапе
компиляции:

int[3] a={3,5,6};

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

int[] a={3,5,6};

Заполнение массивов возможно осуществлять аналогично
C++, с использованием фигурных скобок, как уже было показано выше,
либо обращаясь к элементам массива по номерам элементов:

int[] a=new int[3]{3,5,6};

int[] a = new int[3];
a[0] = 3;
a[1] = 5;
a[2] = 6;

Важным отличием массивов C# от C++ является то, что при
создании массива в C# его элементам автоматически присваиваются
значения по умолчанию, в отличие от C++, где значения элементов
только что созданного массива оказываются не определены.
Присваиваемые значения по умолчанию зависят от типа элемента. Так,
для объектов значением по умолчанию является NULL, а для целых чисел - 0.

Помимо простых одномерных массивов C# поддерживает
также многомерные массивы, которые в свою очередь делятся на две
категории. К первой категории относятся массивы, количество
элементов каждой строки которых состоит из одинакового количества
элементов. Таким образом, массив можно рассматривать как
прямоугольник, а сами такие массивы называют "прямоугольными". Ко
второй категории относятся массивы, количество элементов в строках у
которых не одинаково. Такие массивы образуют прямоугольник, у
которого одна сторона представляет ломаную линию, поэтому такие
массивы называют "ломаными".

Рассмотрим простейшие манипуляции для работы с
прямоугольными многомерными массивами.

Объявление и заполнение массива производится
стандартным способом: для этого организуется два цикла:

Random rnd=new Random();
int[,] Matrix;
Matrix = new int[5, 4];
for (int i = 0; i < 5; i++)
for (int j=0;j<4;j++)
Matrix[i,j] = rnd.Next(10,99);

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

for (int i = 0; i < 5; i++)
{
for (int j = 0; j < 4; j++)
{
Response.Write(Matrix[i, j].ToString());
Response.Write(" ");
}
Response.Write("<br/>");
}

Результат работы программы показан на рис.
3.1.

Рис. 3.1.  Результат
работы программы с "прямоугольным" массивом

"Ломаный" массив может рассматриваться как массив,
каждая ячейка которого представляет собой массив. В качестве примера
создадим массив с переменным количеством элементов в строках:

Random rnd = new Random();
int[][] JMatrix = new int[5][];
for (int i = 0; i < JMatrix.Length; i++)
{
JMatrix[i] = new int[rnd.Next(1,7)];
}

Количество элементов в строке определяется случайным
образом в момент формирования массива. Количество строк задано
жестко и равно пяти. Массив не заполняется никакими числами, поэтому
на экран будут выводиться значения, которыми заполняется массив по
умолчанию (в данном случае это ноль).

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

for (int i = 0; i < JMatrix.Length; i++)
{
Response.Write("Количество элементов в строке " +
i.ToString() + "=" + JMatrix[i].Length.ToString()+" ");
for (int j = 0; j < JMatrix[i].Length; j++)
{
Response.Write(JMatrix[i][j].ToString() + " ");
}
Response.Write("<br/>");
}

В цикле по j происходит
определение количества элементов в строке с помощью свойства Length.

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

Рис. 3.2.  Результат
вывода на экран содержимого "ломаного" массива

Класс Array

Для более детального понимания особенностей
использования массивов в C# необходимо рассмотреть
специализированный класс, реализующий функции массива. Как уже
упоминалось ранее, все типы данных в C# являются классами, для
которых в качестве базового выступает класс Object. Класс Array - не исключение. Он реализует все
базовые свойства классов и является предком для всех типов массивов,
к которым мы привыкли в языке C++ и синтаксис описания которых был
приведен выше. То, что класс Array
является потомком класса Object, кажется
на первый взгляд малоинтересным фактом, однако на самом деле говорит
о многом. В частности, благодаря этому в классе Array оказывается определено множество
разнообразных операций, таких как копирование, поиск, обращение,
сортировки и т. д.

Ниже приведены наиболее интересные методы класса Array

BinarySearch()

Поиск элементов в одномерном
отсортированном массиве.

Sort()

Сортировка элементов
одномерного массива.

Clear()

Очистка элементов массива в
заданном диапазоне индексов.

CopyTo()

Копирование элементов исходного
массива в массив назначения.

GetLength(), Length

Определение количества
элементов в указанном измерении массива.

GetLowerBound()

Определение нижней границы
массива.

GetUpperBound()

Определение верхней границы
массива.

GetValue()

Возвращает значение указанного
индекса для массива.

SetValue()

Устанавливает значение
указанного индекса для массива.

Reverse()

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

Rank

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

Рассмотрим пример использования класса Array. Для этого создадим массив и обеспечим
возможность поиска в нем элементов.

Объявим массив myArray как
статический член класса Page, состоящий
из шести элементов типа int:

static Array myArray = Array.CreateInstance(typeof(Int32), 6);

Разместим на форме элементы TextBox и Button,
которым присвоим имена tb_value и btn_find соответственно, как показано на рис.
3.3.

Рис. 3.3.  Размещение на
форме элементов TextBox и Button

В обработчик процедуры нажатия на кнопку введем
следующий код, заполняющий массив случайными числами, сортирующий
его и осуществляющий поиск введенного в текстовое поле элемента. Код
процедуры нажатия на кнопку приведен ниже.

protected void btn_find_Click(object sender, EventArgs e)
{
Random rnd = new Random();
Response.Write("Исходный массив <br/>");
for (int i = myArray.GetLowerBound(0); i <=
myArray.GetUpperBound(0); i++)
{
myArray.SetValue(rnd.Next(1, 10), i);
Response.Write(myArray.GetValue(i) + "\t");
}
Response.Write("<br/>");
Array.Sort(myArray);
Response.Write("После сортировки:<br/>");
for (int i = myArray.GetLowerBound(0); i <=
myArray.GetUpperBound(0); i++)
{
Response.Write(myArray.GetValue(i) + "\t");
}
object o = Convert.ToInt32(tb_value.Text);
int findIndex = Array.BinarySearch(myArray, o);
if (findIndex<0)
{
Response.Write("<br/>Элемент не найден");
}
else
{
Response.Write("<br/>Элемент найден в позиции <b>"+findIndex.
ToString()+"</b>");
}
}

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

Рис. 3.4.  Результат
работы программы заполнения, сортировки и поиска элементов массива

Как видно, работать с массивами в C# достаточно просто,
в особенности учитывая довольно большие возможности класса Array.

Перечисления

При работе с современными языками программирования
можно заметить, что часто вместо цифровых констант и значений
используются имена. Так происходит, например, при указании цвета
того или иного элемента управления либо объекта. Обычно цвет
задается в виде кодов, соответствующих интенсивности каждой его
составляющей: красного, зеленого, синего цвета; однако это неудобно,
т. к. не является наглядным представлением получаемого в результате
цвета. Для решения задачи упрощения использования цветов применяется
именованная совокупность цветов, сопоставляющая код с наименованием.
Это дает возможность в программном коде использовать имена наиболее
часто употребляемых цветов. Например, цвет текста, выводимого на
экран с помощью элемента Label, можно
задать с помощью команды

Label1.ForeColor=Color.Red;

В данном случае имя Red
соответствует красному цвету из набора цветов Colors. Такие наборы называются
перечислениями.

Перечисление можно создать следующим образом:

enum OrganizationType
{
Education,
Trade,
Manufacture,
Services
}

Перечисление OrganizationType определяет четыре именованные
константы, каждой из которых соответствует определенное числовое
значение. По умолчанию первому элементу перечисления соответствует
значение 0, второму - 1, третьему - 2 и т. д.

При необходимости точку отсчета номеров можно изменить.
В следующем примере нумерация элементов перечисления начинается с
числа 10:

enum OrganizationType
{
Education=10,
Trade,
Manufacture,
Services
}

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

По умолчанию к элементам перечисления применяется тип
данных int. Однако для элементов
перечисления можно использовать любой целочисленный тип данных,
например byte:

enum OrganizationType:byte
{
Education,
Trade,
Manufacture,
Services
}

Все перечисления в C# происходят от единого базового
класса System.Enum. Этот класс содержит
методы, существенно облегчающие работу с перечислениями. Так,
статический метод GetUnderlyingType()
позволяет получить информацию о том, какой тип данных используется
для представления числовых значений элементов перечисления.

Например, следующая строка выведет на экран System.Byte, соответствующий типу элементов
перечисления OrganizationType:

Response.Write(Enum.GetUnderlyingType(typeof(OrganizationType)));

Enum позволяет получить
значимые имена элементов перечисления по их числовым значениям. Для
этого используется статический метод Format(). Следующий пример выведет на экран
наименование элемента перечисления OrganizationType, которому соответствует
значение 2 (в данном случае — Manufacture):

Response.Write(Enum.Format(typeof(OrganizationType),(byte)2,"G"));

Параметр "G", указанный в
функции Format, задает режим вывода
строки результата. G означает, что
результат будет выводиться в виде строки. Возможны еще два параметра
форматирования. X - шестнадцатеричное
представление результата, d - десятичное
значение.

В классе System.Enum
существует также метод, позволяющий создать массив на основе
указанного перечисления. Например, следующая строка создает массив
а, состоящий из 4 элементов. В первый
элемент будет записано значение Education, во второе - Trade и т. д.

Array a = Enum.GetValues(typeof(OrganizationType));

Если нужно определить, является ли символьная строка
элементом перечисления, необходимо использовать свойство IsDefined класса Enum. Следующие примеры демонстрируют
применение этого свойства. В первом случае результат проверки будет
положительным, во втором - отрицательным:

bool r = Enum.IsDefined(typeof(OrganizationType), "Trade");
bool r1 = Enum.IsDefined(typeof(OrganizationType), "Entertainment");

Работа со строками в C#

По аналогии с массивами все строки в C# происходят от
одного базового класса - System.String,
в котором реализовано достаточно много различных методов,
осуществляющих всевозможные операции над строками. Наиболее
интересные методы класса String
представлены ниже.

Length

Позволяет получить количество
символов в строке.

Concat()

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

CompareTo()

Позволяет сравнить две строки.
В случае равенства строк результат выполнения функции равен
нулю. При положительном значении функции большей является
строка, для которой вызывался метод.

Copy()

Создает новую копию
существующей строки.

Format()

Применяется для форматирования
строки с использованием различных примитивов (строк и числовых
данных) и подстановочных выражений вида {0}.

Insert()

Позволяет вставить одну строку
внутрь существующей.

Remove()
Replace()

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

ToUpper()
ToLower()

Преобразуют все символы строки
в строчные или прописные.

Chars

Позволяет получить символ,
находящийся в определенной позиции строки.

Join()

Создает строку, соединяя
заданные строки и разделяя их строкой-разделителем.

Replace()

Заменяет один символ строки
другим.

Split()

Возвращает массив строк с
элементами - подстроками основной строки, между которыми
находятся символы-разделители.

Substring()

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

Trim()

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

ToCharArray()

Создает массив символов и
помещает в него символы исходной
строки.

При работе со строками в C# необходимо учитывать
следующее. Тип string является ссылочным
типом. Однако, несмотря на это, при использовании операций сравнения
происходит сравнение значений строковых объектов, а не адресов этих
объектов, размещенных в оперативной памяти. Кроме того, оператор "+"
объекта string перегружен так, что при
его использовании используется метод Concat().

Рассмотрим несколько примеров использования
возможностей работы со строками.

В следующем примере функция MakeLine создает строку, которая состоит из
символов, получающихся путем несложных вычислений. Результаты
вычислений заносятся в массив строк. Таким образом, массив sArr после завершения цикла содержит значения
всех полученных чисел. После этого, используя разделитель,
передаваемый в функцию в качестве аргумента, а также функцию Join, значения всех ячеек массива sArr объединяются в строку:

protected void Page_Load(object sender, EventArgs e)
{
Response.Write(MakeLine(0, 5, ", "));
Response.Write("</br>");
Response.Write(MakeLine(1, 6, " "));
Response.Write("</br>");
Response.Write(MakeLine(9, 9, ": "));
Response.Write("</br>");
Response.Write(MakeLine(4, 7, "< "));
}

private static string MakeLine(int initVal, int multVal, string sep)
{
string[] sArr = new string[10];

for (int i = initVal; i < initVal+10; i++)
sArr[i-initVal] = String.Format("{0,-3}", i * multVal);

return String.Join(sep, sArr);
}

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

Рис. 3.5.  Результат
работы программы с использованием функции Join

Довольно часто при работе со строками возникает
необходимость разделить строку на подстроки, отделенные друг от
друга заданными символами-разделителями. В следующем примере
задается строка символов, в которой присутствует несколько
символов-разделителей. При помощи функции Split данная строка разделяется на подстроки,
которые затем выводятся на экран каждая в отдельной строке. Для
задания символов-разделителей используется массив символов. В этом
примере также применяется функция Trim,
необходимая в данном случае для того, чтобы убедиться в том, что
заданная строка не состоит из одних лишь пробелов.

Исходный код данного примера приведен ниже.

string words = "строка, содержащая несколько слов, а также
знаков пунктуации: таких как двоеточие и точка.";
string [] split = words.Split(new Char [] {' ', ',', '.', ':'});
foreach (string s in split)
{
if (s.Trim() != "")
Response.Write(s+"</br>");
}

Итогом работы данной программы будет следующий
результат, изображенный на рис.
3.6.

Рис. 3.6.  Результат
работы программы с использованием функции Split

Управляющие последовательности и специальные символы в
строках

При выводе на экран строк в C# можно использовать
управляющие последовательности, позволяющие управлять процессом
вывода текстовой информации на экран. Основной сферой применения
этих последовательностей является вывод тех символов, которые
невозможно вывести на экран при помощи обычной стандартной строки,
заключенной в кавычки.

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

\'

Одинарная кавычка

\"

Двойная кавычка

\\

Обратный слэш

\a

Системное оповещение

\b

Backspace

\f

Начало следующей страницы

\n

Начало новой строки

\r

Return

\t

Горизонтальный символ
табуляции

Не все из этих символов допустимо использовать в
строках Web-приложения. Тем не менее на рис.
3.8 отображен результат выполнения следующей команды:

string esc = "Имя программы \"Visual Studio 2005\",
местонахождение: C:\\Program Files\\Microsoft Visual
Studio 8\\";
Label1.Text = esc;

увеличить изображение
Рис. 3.7.
Пример отображения строки с использованием управляющих символов

Кроме управляющих последовательностей в C# предусмотрен
специальный префикс @, предназначенный для дословного вывода строки
вне зависимости от наличия в ней управляющих символов. Можно
сказать, что этот префикс отключает присутствующие в строке
управляющие символы.

Использование класса System.Text.StringBuilder

При работе со строками в C# необходимо учитывать то,
что строки являются неизменяемыми. Все действия, направленные на
изменение строк, на самом деле не изменяют исходный ее вариант. Они
лишь возвращают измененную копию строки. Это можно увидеть на
следующем примере.

string s1 = "Пример строки";
string s2 = s1.ToUpper();

Response.Write(s1);
Response.Write("</br>");
Response.Write(s2);

В данном случае строка s1
не претерпела никаких изменений. Метод ToUpper() создал копию строки s1 и применил к ней необходимые
преобразования, поэтому на экран будет выведена как исходная строка,
так и строка, подвергшаяся изменению. Такое же утверждение
справедливо и для обычной операции конкатенации строк. Результатом
выполнения операции конкатенации строк является также новая
строка.

В ряде случаев следует избегать ситуаций, когда в
результате выполнения операции создается новая строка, т. к. это
неизбежно связано с дополнительными накладными расходами памяти и
других ресурсов компьютера при выполнении операции. C# содержит
специальный класс StringBuilder,
используя который можно избежать создания копий строк при их
обработке. Все изменения, вносимые в объект данного класса,
немедленно отображаются в нем, что в целом ряде случаев гораздо
эффективнее, чем работа с множеством копий строки.

Основной операцией, чаще всего используемой классом
StringBuilder, является операция
добавления к строке содержимого. Для этого существует метод Append. Следующий код добавляет одну строку к
другой и выводит результат в окно браузера. При этом изменяется
оригинал строки, копия не создается:

StringBuilder sb = new StringBuilder("Ехали медведи на
велосипеде");
sb.Append(", а за ними кот задом наперед");
Response.Write(sb);

Кроме добавления класс StringBuilder содержит множество других
методов, наиболее значимые из которых перечисленных ниже. После того
как все необходимые действия, связанные с обработкой строки, были
выполнены, необходимо вызвать метод ToString() для перевода содержимого объекта в
обычный тип данных string.

Append

Добавление заданной строки в
конец строки объекта.

AppendFormat

Добавление заданной
форматированной строки (строки, содержащей управляющие
символы) в конец строки объекта.

CopyTo

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

Insert

Добавление строки в заданную
позицию строки объекта.

Remove

Удаление заданного количества
символов из строки объекта

Replace

Замена заданного символа либо
строки объекта на другой заданный символ либо
строку.

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

Процедуры и функции

Процедуры и функции C# практически полностью
соответствуют их определениям в языке C++. Фактически, в языке С
отсутствует определение процедуры, принятое в других языках
программирования. Если определить функцию, которая не возвращает
никакого результата (тип void) в вызывающую программу, мы получим
аналог процедуры в других языках программирования. Основное отличие
процедуры от функции состоит в том, что функция должна всегда
возвращать некоторый результат, и, кроме того, должна быть вызвана в
выражении. Процедура же не возвращает никакого результата, ее вызов
осуществляется с помощью оператора языка; кроме того, она имеет
входные и выходные аргументы, причем выходных аргументов может быть
достаточно много. Таким образом, создавать функцию целесообразно
только в том случае, когда необходимо произвести какие-либо
вычисления, в результате которых должен быть получен результат в
виде одного значения. Процедуру - соответственно в случае, когда
может быть получен результат, представляющий собой набор каких-либо
значений.

Тем не менее существует ряд нововведений, которые
касаются функций, появившихся в C#. Наиболее важной особенностью
использования функций и процедур в C# является то, что, так как C#
является полностью объектно-ориентированным языком программирования,
функции не могут существовать в отрыве от классов. Поэтому функцию
библиотек процедур и функций в C# выполняют библиотеки классов.

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

Синтаксис описания методов достаточно прост:

[атрибуты][модификаторы]{void | тип результата функции}
имя метода ([список формальных аргументов])

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

Атрибуты и модификаторы являются очень важной
составляющей описания любого метода, тем не менее они будут
рассмотрены при описании классов, т. к. имеют к этому самое
непосредственное отношение. Пока же можно считать, что под
модификаторами подразумеваются модификаторы доступа, из которых мы
будем рассматривать пока только два: private и public.
Private означает, что данный метод
является закрытым, соответственно доступ к нему могут получить
только методы того класса, в котором он объявлен. Public - наоборот, означает, что доступ к
данному методу является открытым и общедоступным из любой точки
приложения.

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

Простейшими примерами описания функций являются
следующие:

private void A()
{}

public int B()
{}

public long Stepen(int a, int b)
{
long r;
r = (long)Math.Pow(a, b);
return (r);
}

Здесь метод A является
закрытой процедурой, методы B и Stepen - открытыми функциями. У методов A и B не
определены формальные параметры, в то время как у метода Stepen два формальных параметра: a и b.

Как видно из этого примера, список формальных
аргументов может быть пустым, что является типичной ситуацией для
методов класса. Однако формальные аргументы, определяемые для
метода, могут содержать и некоторые дополнительные атрибуты,
влияющие на их поведение. Полный синтаксис объявления формального
аргумента выглядит следующим образом:

[ref|out|params] тип_аргумента имя_аргумента

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

Иногда при вызове метода возникает необходимость
передавать в метод произвольное количество фактических аргументов,
несмотря на фиксированное количество аргументов, содержащееся в его
определении. Для реализации такой возможности необходимо задать
ключевое слово params. Оно задается один
раз и указывается только для последнего аргумента списка,
объявляемого как массив произвольного типа. При вызове метода этому
формальному аргументу соответствует произвольное количество
фактических аргументов.

Все аргументы методов можно разделить на три группы:
входные, выходные, обновляемые.

Входные необходимы для передачи информации
методу, их значения в теле метода доступны только для чтения.
Выходные представляют собой результаты метода, они получают
значения в ходе работы метода. Обновляемые способны выполнять
обе функции. Выходные аргументы должны быть помечены ключевым словом
out, при этом в теле метода должен
обязательно присутствовать оператор присваивания значения этому
аргументу; обновляемые аргументы помечаются с помощью ключевого
слова ref.

В качестве примера слегка модернизируем описанный ранее
метод Stepen:

public void Stepen(out long r,int a, int b)
{
r = (long)Math.Pow(a, b);
}

Как видно из примера, функция Stepen была фактически преобразована в
процедуру (тип возвращаемого функцией значения был изменен на void). Также был добавлен формальный аргумент
r, используемый в качестве выходного. В теле метода этому аргументу
присваивается значение, которое впоследствии может быть использовано
в вызывающей программе.

Вызов этого метода может быть осуществлен следующим
образом:

long s;
Stepen(out s, 2, 6);
Response.Write(s.ToString());

Обратите внимание на то, что при вызове этого метода
первый параметр также указывается с ключевым словом out. Из примера видно, что при завершении
работы метода Stepen и возврата в
вызывающую программу происходит передача значения переменной r из метода в переменную s, находящуюся в адресном пространстве
основной программы.

Рассмотрим пример передачи произвольного количества
значений исходных данных в метод для их обработки. Как уже
упоминалось выше, для этого необходимо использовать ключевое слово
params. Создадим пример, в котором в
метод Stepen может передаваться
произвольное количество чисел для нахождения суммы их квадратов.
Объявление метода в этом случае выглядит следующим образом:

public void Stepen(out long r, int a, params int[] b)
{
r = 0;
foreach (int i in b)
r += (long)Math.Pow(i, a);
}

Вызов метода может быть осуществлен так:

int[] digits ={1,8,4};
Stepen(out s, 2, digits);
Response.Write(s.ToString());

Видно, что для вызова метода необходимо сформировать
массив целых чисел, который затем следует передать в качестве
третьего аргумента в метод. Результат вычисления суммы накапливается
в переменной r, а затем передается в
переменную s вызывающей программы.

Результат работы программы показан на рис.
3.8.

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

Иногда возникает необходимость не только передавать
произвольное количество исходных данных для расчета, но и получать
из вызывающей процедуры произвольное количество переменных,
содержащих результаты расчета.

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

Процедура в этом случае будет выглядеть следующим
образом:

public void Stepen(out long[] r,int a, params int[] b)
{
r=new long[0];
Array.Resize(ref r,b.Length);
int j=0;
foreach (int i in b)
r[j++] = (long)Math.Pow(i, a);
}

Здесь необходимо отметить следующие важные особенности.
Во-первых, в качестве параметра, возвращающего значение в вызывающую
программу, использован массив чисел типа long. Во-вторых, прежде чем станет возможным
использовать этот массив, его размер необходимо привести в
соответствие с размером массива b. Для
этого можно воспользоваться методом Resize объекта Array. Однако этот метод позволяет изменять
количество элементов такого массива, для которого это количество уже
определено, поэтому перед вызовом метода Resize создается новый массив r, состоящий из ноля элементов. Стоит отметить
также то, что перед первым параметром метода Resize находится ключевое слово ref, говорящее о том, что данный метод
принимает ссылку на массив r, - именно
поэтому внутри метода становится возможным изменение параметров
самого массива, а не его копии.

Вызов метода можно осуществить следующим образом:

Response.Write("Результаты вычисления значений массива:<br/>");
long [] result;
int [] data={2,3,4,5};
Stepen(out result, 2, data);
foreach (long i in result)
Response.Write(i.ToString()+"<br/>");

Результат работы программы изображен на рис.
3.9.

Рис. 3.9.  Результат
работы метода, возводящего в заданную степень массив чисел

Подытоживая все сказанное выше относительно методов,
необходимо обратить внимание на следующее. Язык C# является
объектно-ориентированным языком программирования. Как известно,
основную роль в таких языках играют ссылочные типы, поэтому когда
методу передается объект ссылочного типа, все поля этого объекта
могут меняться в методе, т. е. программный код метода может получить
полный доступ ко всем полям объекта. Это происходит несмотря на то,
что формально объект не является выходным и не имеет ключевых слов
ref и out,
т. е. использует семантику вызова по значению. При таком вызове сама
ссылка на объект остается неизменной, но состояние объекта, значения
его полей могут полностью измениться. Такая ситуация достаточно
распространена и является типичной, поэтому при работе со ссылочными
типами данных ключевые слова ref и out нечасто появляются в описании аргументов
метода.

Классы и структуры

Классы

Не претендуя на полноту изложения положений
объектно-ориентированного анализа и проектирования, постараемся
разобрать один из аспектов практического применения его принципов.
За дополнительной информацией относительно особенностей объектного
программирования в C# рекомендуется обращаться к [3].

Язык C# в полной мере является
объектно-ориентированным. Из представленного выше материала видно,
что все действия, осуществляемые в C#, являются обращениями к
ресурсам неких классов. Иногда для этого необходимо создавать
объекты классов, иногда (при вызове статических методов) создание
объектов не требуется. Одной из важнейших особенностей C# является
то, что здесь нельзя создать отдельно стоящую процедуру или функцию,
она обязательно должна входить в какой-либо класс.

Данный раздел ставит своей целью рассмотрение основных
особенностей и базовых принципов использования
объектно-ориентированного программирования в C#. Предполагается, что
читатель обладает набором необходимых знаний основ
объектно-ориентированного программирования.

Итак, класс - это шаблон, который определяет свойства
и методы объектов, создаваемых как экземпляры данного класса.
Основное назначение класса - создавать некий тип данных, который
задает реализацию некоторой абстракции данных, характерной для
задачи, в интересах которой создается программная система. Таким
образом, классы - не просто кирпичики, из которых строится система.
Каждый кирпичик теперь имеет важную содержательную начинку,
отвечающую за выполнение своей операции.

Все современные программные системы создаются с
использованием принципов объектно-ориентированного программирования.
Это означает, что любая сущность, с которой осуществляются
какие-либо действия, должна быть описана в виде класса. Таким
образом, система представляет собой совокупность взаимодействующих
друг с другом классов. Определение класса в C# практически идентично
определению такого же класса в C++. Тем не менее в C# существуют
дополнительные элементы, значительно облегчающие практическое
использование классов. Рассмотрим основные правила и примеры
построения и использования классов.

Предполагая, что читатель знаком с основами объектного
программирования на C++, рассмотрим пример создания и использования
классов с помощью языка C#.

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

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

public class Client
{
private string Name;
private string Passport;
private DateTime BirthDate;

public Client()
{

}
}

Здесь определены поля класса, а также конструктор по
умолчанию (без параметров). Для классов C# можно определить любое
количество конструкторов. Если в классе не определен ни один
конструктор, используется конструктор по умолчанию, который при
создании объекта автоматически присвоит всем переменным - членам
класса безопасные значения. В данном случае, конструктор по
умолчанию задается явно.

Область видимости полей класса в соответствии с
правилами должна быть определена либо как закрытая, либо как
защищенная (в случае, если предполагается наследование его полей).
Доступ же к полям - членам класса должен быть организован либо
посредством методов, либо с помощью свойств класса. Ранее в языке
C++ свойства отсутствовали, однако они позволяют организовывать
очень простой доступ к полям - членам класса. Создадим свойства
класса Client, обеспечивающие чтение и
запись значений полей класса.

public string passport
{
get
{
return Passport;
}
set
{
Passport = value;
}
}

public string name
{
get
{
return Name;
}
set
{
Name = value;
}
}

public int age
{
get
{
int a;
a = DateTime.Now.Year - BirthDate.Year;
return a;
}
}

public DateTime birthdate
{
get
{
return BirthDate;
}
set
{
if (DateTime.Now > value)
BirthDate = value;
else
throw new Exception("Введена неверная дата рождения");
}
}

Как видно из данного примера, свойство состоит из
методов set и get. При этом свойство должно содержать хотя
бы один из методов. Set позволяет
изменять значение поля класса, get -
получать значение. В метод Set
передается значение параметра с помощью переменной value. Оба метода могут содержать произвольное
количество операторов, описывающих алгоритм выполнения действий в
процессе чтения или записи значения в поле класса. В данном примере
свойства passport и name позволяют просто получить доступ к полям
класса, считывая или устанавливая значения соответствующих
переменных. Свойство birthdate также
предназначено для чтения и записи значения переменной - члена класса
BirthDate. При этом при чтении значения
(операция get) происходит просто
передача значения переменной BirthDate,
при попытке же записи нового значения в эту переменную происходит
проверка допустимости устанавливаемого значения переменной. В данном
случае проверка сводится к сравнению нового значения даты рождения с
текущей датой. Если устанавливаемое значение даты рождения больше
либо равно текущей дате, генерируется исключение, которое не
позволяет записать новое значение в переменную - член класса.

Свойство age применяется
для получения текущего возраста клиента. Оно предназначено только
для чтения значения из переменной, поэтому содержит лишь метод get. При использовании свойства age происходит вычисление текущего значения
возраста клиента в годах путем вычитания года рождения из текущего
значения года.

Использование свойств аналогично использованию
переменных. В следующем примере создается объект с1 класса Client.
Затем поля данного объекта заполняются значениями с использованием
свойств. После этого на экран выводятся значения полей, для этого
также применяются свойства класса:

Client c1=new Client();
c1.name = "Вася";
c1.passport = "9002";
c1.birthdate = new DateTime(1967, 08, 03);
Response.Write("Имя=" + c1.name+"</br>");
Response.Write("Паспорт=" + c1.passport+"</br>");
Response.Write("Возраст="+c1.age);

Помимо переменных - членов класса и свойств для доступа
к этим переменным практически любой класс содержит методы,
предназначенные для описания алгоритмов выполнения классом действий.
Один из обязательных методов класса - конструктор по умолчанию - уже
был показан ранее. Однако классы в C# могут содержать произвольное
количество конструкторов, предназначенных для инициализации объекта
данного класса. В качестве примера создадим конструктор с
параметрами, позволяющий инициализировать объект класса, вводя
значения полей этого объекта в качестве параметров конструктора.
Сделать это можно следующим образом:

public Client(string ClientName, string ClientPassport,
DateTime ClientBirthDate)
{
name = ClientName;
passport = ClientPassport;
birthdate = ClientBirthDate;
}

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

Создадим также метод, позволяющий изменить значения
полей объекта класса Client:

public void EditClient(string ClientName, string
ClientPassport,DateTime ClientBirthDate)
{
Name = ClientName;
Passport = ClientPassport;
birthdate = ClientBirthDate;
}

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

Теперь, с использованием конструктора с параметрами,
можно создать и сразу же инициализировать объект класса Client:

Client c1=new Client("Вася","9002",new DateTime(1967,08,03));

Структуры

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

Для структур можно определять конструкторы,
реализовывать интерфейсы. Однако для структур в C# не существует
базового класса, поэтому все структуры являются производными от типа
ValueType.

Простейший пример структуры можно представить так:

public struct Employee
{
public string name;
public string type;
public int deptID;
}

Использование структуры возможно следующим образом.
Сначала ее необходимо создать. В момент создания структуры для нее
выделяется память в области стека. В
дальнейшем к элементам структуры возможно обращение путем указания
имени структуры и имени элемента, разделенных точкой:

Employee Alex;
Alex.name = "Alex";
Alex.type = "manager";
Alex.deptID = 2310;

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

public Employee(int DeptID, string Name, string EmpType)
{
deptID = DeptID;
type = EmpType;
name = Name;
}

Использование конструктора выглядит следующим
образом:

Employee Nick=new Employee(278,"Nick","worker");

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

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

Коллекции

Массивы - это удобное средство организации хранения и
упорядочивания информации. Однако при работе с массивами практически
всегда возникают задачи, которые связаны с обработкой данных,
содержащихся в них. В современном программировании очень часто
элементами массива являются объекты, над которыми необходимо
выполнять различные действия. В силу особенностей коллекций, а также
учитывая тот факт, что упор при изложении данного материала был
сделан на использование коллекций для хранения именно объектов,
раздел, посвященный коллекциям, расположен после раздела,
посвященного классам и структурам.

Ранее был рассмотрен класс Array, в котором реализован целый набор часто
используемых операций над элементами массива, таких как сортировка,
клонирование, перечисления и расстановка элементов в обратном
порядке. Тем не менее при работе с массивами возникает и целый ряд
других задач. Решением этих задач и занимаются коллекции, позволяющие организовывать элементы
специальным образом и производить впоследствии над ними определенный
набор операций.

Все определенные в .NET Framework коллекции расположены в пространстве имен System.Collections. Наиболее часто
используемые коллекции этого пространства имен представлены
ниже.

ArrayList

Массив объектов, динамически
изменяющий свой размер.

Hashtable

Набор взаимосвязанных ключей и
значений, основанных на хэш-коде ключа.

Queue

Стандартная очередь,
реализованная по принципу "первым вошел, первым вышел" (FIFO –
First In First Out).

SortedList

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

Stack

Очередь, реализованная по
принципу "первым вошел, последним вышел" (LIFO – Last In First
Out).

В пространстве имен System.Collections.Specialized расположены
классы, предназначенные для использования в специальных случаях.
Так, класс StringCollection представляет
набор строк. Класс StringDictionary
представляет собой набор строковых значений, состоящих из пар
"ключ-значение", в качестве значений которых используется
строка.

В данной лекции будут рассмотрены общие вопросы
применения коллекций. Для более детального изучения их особенностей
и возможностей рекомендуется ознакомиться с разделом "Интерфейсы", а
также использовать дополнительную литературу и справочную систему
MSDN.

Рассмотрим пример применения коллекций на примере
класса ArrayList. Для этого
воспользуемся рассмотренным ранее классом Client. Очень часто возникает ситуация, при
которой необходимо бывает помещать объекты в список или массив для
их дальнейшей обработки. Конечно, при этом может быть достаточно тех
возможностей, которые предоставляют стандартные массивы C#,
рассмотренные ранее. Однако коллекции, как уже было сказано,
обладают более широкими возможностями и, следовательно, их
применение предпочтительно по сравнению с массивами. Так, для
обработки данных о клиентах создадим класс Clients, предназначенный для хранения списка
клиентов и его обработки. При этом необходимо учесть, что класс
Clients должен обеспечивать возможность
добавления, удаления и нумерации элементов (клиентов). Для
реализации этой функциональности необходимо использовать класс ArrayList в качестве вложенного в класс Clients. Таким образом, нам необходимо
определить в данном классе элемент, позволяющий вести список
клиентов, а также реализовать набор открытых методов, которые будут
передавать вызовы на выполнение различных действий внутреннему
классу, производному от ArrayList.
Пример исходного кода класса Clients
приведен ниже.

public class Clients
{
private ArrayList ClientsList;
public Clients()
{
ClientsList=new ArrayList();
}
public int ClientsCount
{
get
{
return ClientsList.Count;
}
}
public void AddClient(Client c)
{
ClientsList.Add(c);
}
public void RemoveClient(int ClientToRemove)
{
ClientsList.RemoveAt(ClientToRemove);
}
public Client GetClient(int ClientID)
{
return (Client)ClientsList[ClientID];
}
}

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

Clients cl=new Clients();
cl.AddClient(new Client("Сидоров","9002",new
DateTime(1980,12,21)));
cl.AddClient(new Client("Петров","9004",new
DateTime(1975,03,15)));

Теперь становится возможным обращение к любому классу
Client, помещенному в коллекцию Clients посредством метода GetClient:

Client MyClient = cl.GetClient(1);
Response.Write(MyClient.name);

Результатом работы этого фрагмента программы будет
строка "Петров".

Тем не менее существует несколько неудобств от
использования класса Clients в его
нынешней реализации. Во-первых, более привычным обращением к
элементу массива либо списка в языке C# является использование
индексаторов, обозначаемых в виде квадратных скобок. Для реализации
этой возможности в классе Clients
необходимо создать индексатор:

public Client this[int pos]
{
get
{
return ((Client)ClientsList[pos]);
}
set
{
ClientsList[pos] = value;
}
}

Как видно, от обычного метода индексатор отличается
именем (this), а также наличием
квадратных скобок, в которых указывается параметр - номер
извлекаемого элемента. С помощью индексатора становится возможным
извлечение элемента из коллекции, а также запись элемента в
коллекцию под определенным номером:

Response.Write(cl[1].name);
cl[1].name = "Сидоров";
Response.Write(cl[1].name);

Теперь можно организовать обработку элементов массива в
цикле. При работе с коллекциями довольно удобной является
возможность использования цикла forech.
Для реализации такой функциональности при работе с классом Clients необходимо использовать метод GetEnumerator() интерфейса IEnumerator - задать наследование классом
Clients интерфейса IEnumerator и реализовать метод, как показано
ниже.

public class Clients:IEnumerable
{
public IEnumerator GetEnumerator()
{
return ClientsList.GetEnumerator();
}
}

Пример применения цикла foreach с использованием класса Clients показан ниже.

foreach (Client c in cl)
{
Response.Write("Имя="+c.name+" ");
Response.Write("Паспорт="+c.passport+" ");
Response.Write("Возраст=" + c.age);
Response.Write("</br>");

}

Краткие итоги

Язык C# является наследником C++, перенимая основные
элементы синтаксиса и базовую систему типов. При этом в него были
введены некоторые элементы, свойственные другим языкам
программирования, таким, например, как Visual Basic. В C# существует
достаточно много нововведений, направленных на упрощение работы с
ним и на повышение гибкости его использования.

Важные отличия произошли в системе типов языка C#. Все
типы были разделены на значимые и ссылочные. Причем в среде .NET
Framework были определены правила использования тем или иным типом
доступной оперативной памяти, которые должен учитывать программист в
процессе разработки программы.

Важнейшей характеристикой языка является тот факт, что
он является полностью объектно-ориентированным. Это означает, что
абсолютно все типы данных описываются с помощью классов. Основу
синтаксиса описания классов C# также позаимствовал у C++. Однако и
здесь были введены новые элементы, которые способствуют решению
части неудобств, возникающих при использовании C++. Правила передачи
параметров в процедуры и функции также претерпели некоторые
изменения. В частности, появились ключевые слова in и out,
позволяющие четко указывать на направление передачи параметров: в
процедуру или из нее.

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

© gsu.by