Цель лекции: познакомиться с основными отличительными особенностями языка C#, рассмотреть примеры использования новых средств и операторов языка, типов данных и их преобразований в объеме, необходимом для дальнейшего изучения материала.
Состав и синтаксис операторов C# унаследован от языка С++. Тем не менее существует ряд отличий, которые улучшают некоторые характеристики C++, делая его более легким в использовании. Предполагая, что читатель уже знаком с основными операторами языка C++ и имеет минимальный опыт программирования, остановимся на рассмотрении только наиболее значимых операторов C#, а также операторов, специфических для данного языка программирования.
Новым видом цикла, который появился в 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# однозначно отображаются, а фактически совпадают с системными типами каркаса .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, то фактический аргумент может быть выражением любого типа.
Рассмотрим частный случай присваивания 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) Результат вывода на экран содержимого "ломаного" массива
Для более детального понимания особенностей использования массивов в C# необходимо рассмотреть специализированный класс, реализующий функции массива. Как уже упоминалось ранее, все типы данных в C# являются классами, для которых в качестве базового выступает класс Object. Класс Array - не исключение. Он реализует все базовые свойства классов и является предком для всех типов массивов, к которым мы привыкли в языке C++ и синтаксис описания которых был приведен выше. То, что класс Array является потомком класса Object, кажется на первый взгляд малоинтересным фактом, однако на самом деле говорит о многом. В частности, благодаря этому в классе Array оказывается определено множество разнообразных операций, таких как копирование, поиск, обращение, сортировки и т. д.
Ниже приведены наиболее интересные методы класса Array
|
Поиск элементов в одномерном отсортированном массиве. |
Sort() |
Сортировка элементов одномерного массива. |
Clear() |
Очистка элементов массива в заданном диапазоне индексов. |
CopyTo() |
Копирование элементов исходного массива в массив назначения. |
GetLength(), Length |
Определение количества элементов в указанном измерении массива. |
GetLowerBound() |
Определение нижней границы массива. |
GetUpperBound() |
Определение верхней границы массива. |
GetValue() |
Возвращает значение указанного индекса для массива. |
SetValue() |
Устанавливает значение указанного индекса для массива. |
Reverse() |
Расставляет элементы одномерного массива в обратном порядке. |
Rank |
Определение количества измерений указанного массива. |
Рассмотрим пример использования класса Array. Для этого создадим массив и обеспечим возможность поиска в нем элементов.
Объявим массив myArray как статический член класса Page, состоящий из шести элементов типа int:
static Array myArray = Array.CreateInstance(typeof(Int32), 6);
Разместим на форме элементы .
(рис 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# происходят от одного базового класса - 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.7 отображен результат выполнения следующей команды:
string esc = "Имя программы \"Visual Studio 2005\", местонахождение: C:\\Program Files\\Microsoft Visual Studio 8\\"; Label1.Text = esc;
(рис 3.7) Пример отображения строки с использованием управляющих символовКроме управляющих последовательностей в C# предусмотрен специальный префикс @, предназначенный для дословного вывода строки вне зависимости от наличия в ней управляющих символов. Можно сказать, что этот префикс отключает присутствующие в строке управляющие символы.
При работе со строками в 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# в полной мере является объектно-ориентированным. Из представленного выше материала видно, что все действия, осуществляемые в 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 Clients this[int pos]
{
get
{
return ((Clients)ClientsList[pos]);
}
set
{
ClientsList[pos] = value;
}
}
Как видно, от обычного метода индексатор отличается именем ( this ), а также наличием квадратных скобок, в которых указывается параметр - номер извлекаемого элемента. С помощью индексатора становится возможным извлечение элемента из коллекции, а также запись элемента в коллекцию под определенным номером:
Response.Write(cl[1].name); cl[1].name = "Сидоров"; Response.Write(cl[1].name);
Теперь можно организовать обработку элементов массива в цикле. При работе с коллекциями довольно удобной является возможность использования цикла foreach. Для реализации такой функциональности при работе с классом Clients необходимо использовать метод GetEnumerator() интерфейса IEnumerator - задать наследование классом Clients интерфейса IEnumerator и реализовать метод, как показано ниже.
public class Clients:IEnumerable
{
public IEnumerator GetEnumerator()
{
return ClientsList.GetEnumerator();
}
}
Пример применения цикла foreach с использованием класса Clients показан ниже.
foreach (Clients 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, позволяющие четко указывать на направление передачи параметров: в процедуру или из нее.
Очень важным компонентом языка является возможность использования динамических массивов и расширенные возможности при работе со строками.
Цель лекции: познакомиться с основными отличительными особенностями языка C#, рассмотреть примеры использования новых средств и операторов языка, типов данных и их преобразований в объеме, необходимом для дальнейшего изучения материала.
Состав и синтаксис операторов C# унаследован от языка С++. Тем не менее существует ряд отличий, которые улучшают некоторые характеристики C++, делая его более легким в использовании. Предполагая, что читатель уже знаком с основными операторами языка C++ и имеет минимальный опыт программирования, остановимся на рассмотрении только наиболее значимых операторов C#, а также операторов, специфических для данного языка программирования.
Новым видом цикла, который появился в 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# однозначно отображаются, а фактически совпадают с системными типами каркаса .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, то фактический аргумент может быть выражением любого типа.
Рассмотрим частный случай присваивания 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) Результат вывода на экран содержимого "ломаного" массива
Для более детального понимания особенностей использования массивов в C# необходимо рассмотреть специализированный класс, реализующий функции массива. Как уже упоминалось ранее, все типы данных в C# являются классами, для которых в качестве базового выступает класс Object. Класс Array - не исключение. Он реализует все базовые свойства классов и является предком для всех типов массивов, к которым мы привыкли в языке C++ и синтаксис описания которых был приведен выше. То, что класс Array является потомком класса Object, кажется на первый взгляд малоинтересным фактом, однако на самом деле говорит о многом. В частности, благодаря этому в классе Array оказывается определено множество разнообразных операций, таких как копирование, поиск, обращение, сортировки и т. д.
Ниже приведены наиболее интересные методы класса Array
|
Поиск элементов в одномерном отсортированном массиве. |
Sort() |
Сортировка элементов одномерного массива. |
Clear() |
Очистка элементов массива в заданном диапазоне индексов. |
CopyTo() |
Копирование элементов исходного массива в массив назначения. |
GetLength(), Length |
Определение количества элементов в указанном измерении массива. |
GetLowerBound() |
Определение нижней границы массива. |
GetUpperBound() |
Определение верхней границы массива. |
GetValue() |
Возвращает значение указанного индекса для массива. |
SetValue() |
Устанавливает значение указанного индекса для массива. |
Reverse() |
Расставляет элементы одномерного массива в обратном порядке. |
Rank |
Определение количества измерений указанного массива. |
Рассмотрим пример использования класса Array. Для этого создадим массив и обеспечим возможность поиска в нем элементов.
Объявим массив myArray как статический член класса Page, состоящий из шести элементов типа int:
static Array myArray = Array.CreateInstance(typeof(Int32), 6);
Разместим на форме элементы .
(рис 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# происходят от одного базового класса - 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.7 отображен результат выполнения следующей команды:
string esc = "Имя программы \"Visual Studio 2005\", местонахождение: C:\\Program Files\\Microsoft Visual Studio 8\\"; Label1.Text = esc;
(рис 3.7) Пример отображения строки с использованием управляющих символовКроме управляющих последовательностей в C# предусмотрен специальный префикс @, предназначенный для дословного вывода строки вне зависимости от наличия в ней управляющих символов. Можно сказать, что этот префикс отключает присутствующие в строке управляющие символы.
При работе со строками в 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# в полной мере является объектно-ориентированным. Из представленного выше материала видно, что все действия, осуществляемые в 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 Clients this[int pos]
{
get
{
return ((Clients)ClientsList[pos]);
}
set
{
ClientsList[pos] = value;
}
}
Как видно, от обычного метода индексатор отличается именем ( this ), а также наличием квадратных скобок, в которых указывается параметр - номер извлекаемого элемента. С помощью индексатора становится возможным извлечение элемента из коллекции, а также запись элемента в коллекцию под определенным номером:
Response.Write(cl[1].name); cl[1].name = "Сидоров"; Response.Write(cl[1].name);
Теперь можно организовать обработку элементов массива в цикле. При работе с коллекциями довольно удобной является возможность использования цикла foreach. Для реализации такой функциональности при работе с классом Clients необходимо использовать метод GetEnumerator() интерфейса IEnumerator - задать наследование классом Clients интерфейса IEnumerator и реализовать метод, как показано ниже.
public class Clients:IEnumerable
{
public IEnumerator GetEnumerator()
{
return ClientsList.GetEnumerator();
}
}
Пример применения цикла foreach с использованием класса Clients показан ниже.
foreach (Clients 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, позволяющие четко указывать на направление передачи параметров: в процедуру или из нее.
Очень важным компонентом языка является возможность использования динамических массивов и расширенные возможности при работе со строками.
Для получения официальных документов о завершении программы дополнительного профессионального образования (удостоверения о повышении квалификации, дипломов о профессиональной переподготовке и MBA) необходимо предоставить:
Внимание! Вы можете не заказывать доставку бумажной версии официального документы, а скачать его в электронном виде и распечатать самостоятельно. Информация о выданном документе в течение 1 месяца загружается в Федеральную информационную систему «Федеральный реестр сведений о документах об образовании и (или) о квалификации, документах об обучении» - ФИС ФРДО.
Доступ на новый сайт осуществляется с использованием адреса электронной почты, который был указан вами при регистрации на "старом". Мы постарались перенести все ваши данные с прежнего ресурса, однако не исключена вероятность потери части информации.
При возникновении проблемы со входом, воспользуйтесь функцией сброса пароля
Если вы обнаружите несоответствия, пожалуйста, сообщите нам.