
Объектно-ориентированное программирование.-6
.pdf
Если бы ссылка refobj указывала на экземпляр value, то после операции value = 7 ссылка refobj указывала бы на объект со значением 7, а она, как видно, по-прежнему указывает на объект со значением 5. Вызов метода ReferenceEquals подтверждает это. Получается, что в момент операции refobj = value был создан новый ссылочный объект, в который была помещена копия объекта-значения, т.е. произошла упаковка типа (см. пункт 3.1.5).
Во-вторых, ссылка может не быть инициализированной, т.е. содержать значение null. До инициализации использовать экземпляр ссылочного типа нельзя, это приведет к ошибке компиляции. Вызов методов экземпляра, инициализированного пустой ссылкой (null), приведет к ошибке при выполнении программы. Пример:
object obj;
obj.ToString(); // Ошибка компиляции obj = new Object();
obj.ToString(); // ОК Object obj2 = null;
obj2.ToString(); // Ошибка во время выполнения
Синонимом класса System.Object является ключевое слово языка C# object.
3.1.3.1. Строки
Строки – очень важный тип данных. В языке C++ нет встроенной поддержки строк, из-за чего у многих начинающих программистов (особенно писавших ранее на языке Pascal) возникали проблемы. Позже строки были реализованы в различных библиотеках (STL, VCL и т.д.) в виде классов.
Пример: Samples\3.1\3_1_3_string.
Неизменяемые строки
Строки в языке C# представлены классом System.String (синоним – ключевое слово string). Строка представлена массивом символов Unicode. Однако, проводить аналогию между классом String и массивом из элементов типа char нельзя, хотя существуют способы преобразования между двумя этими способами представления строк.
Как любой элемент программы, заключенный в одинарные кавычки, является экземпляром типа char, так и любой элемент программы, заключенный в двойные кавычки, является экземпляром типа string.
111

Основные члены класса String представлены в табл. Б.1 (приложение Б). Пример (ввиду большого количества членов ограничимся демонстрацией только некоторых из них):
string s1 |
= string.Empty; // 1 |
|||
string s2 |
= new |
string("Hello!".ToCharArray()); // 2 |
||
string |
s3 |
= |
"Hello!"; // 3 |
|
string |
s4 |
= |
s3; |
// 4 |
Console.WriteLine("s1 = " + s1); Console.WriteLine("s2 = " + s2); Console.WriteLine("s3 = " + s3); Console.WriteLine("s4 = " + s4); Console.WriteLine(Object.ReferenceEquals(s3, s4) ?
"ref s3 == ref s4" : "ref s3 != ref s4"); // 5
s4 = "А теперь это новый объект string"; // 6 Console.WriteLine("s3 = " + s3); Console.WriteLine("s4 = " + s4); Console.WriteLine(Object.ReferenceEquals(s3, s4) ?
"ref s3 == ref s4" : "ref s3 != ref s4"); // 7
string s5 = "c:\\path\\file.ext"; // 8 string s6 = @"c:\path\file.ext"; // 9
Console.WriteLine("s5 = " + s5);
Console.WriteLine("s6 = " + s6);
string s7 = new string('!', 10); // 10
Console.WriteLine("s7 = " + s7);
char[] cstr = "строка C++".ToCharArray(); // 11 string s8 = new string(cstr); // 12
string s9 = new string(cstr, 0, 8); // 13
Console.WriteLine("s8 = " + s8);
Console.WriteLine("s9 = " + s9);
Результат работы кода:
s1 =
s2 = Hello!
s3 = Hello!
s4 = Hello!
ref s3 == ref s4 s3 = Hello!
s4 = А теперь это новый объект string ref s3 != ref s4
s5 = c:\path\file.ext
s6 = c:\path\file.ext
s7 = !!!!!!!!!!
s8 = строка C++ s9 = строка C
Разберем подробнее строки, помеченные комментариями: 1) Строка s1 создается пустой.
112
2)Строка s2 создается вызовом конструктора с параметром типа char[] (массив char). Т.к. строка «"Hello!"» является экземпляром типа string, а среди конструкторов класса String нет конструктора с аргументом такого типа, преобразуем string в массив char (такой конструктор есть).
3)Но гораздо проще использовать оператор «=» – строка s3 инициализирована строкой «Hello!».
4)Строки – ссылочный тип, поэтому s4 будет ссылкой на экземпляр s3. Вернее, обе эти ссылки указывают на один и тот же объект. Это позволяет экономить память при работе со строками. Следующие операторы выводят на экран строку s1 (пустую) и остальные, содержащие «Hello!».
5)Убеждаемся, что ссылки s3 и s4 указывают на один и тот же объект.
6)Как только мы модифицируем содержимое строки s4, в памяти создается новый экземпляр класса String (содержащий строку «А теперь это новый объект string»), и ссылка s4 теперь указывает на него. Выводим на консоль строки s3 и s4, видим, что теперь они различаются.
7)Убеждаемся, что ссылки s3 и s4 теперь указывают на два разных
объекта.
8)Помещаем в строку путь к файлу. Учитывая, что обратный слеш используется для обозначения управляющих символов, ставим слеши парами.
9)Если перед строковым литералом поставить символ «@», то все управляющие символы будут рассматриваться как обычные символы строки. Теперь достаточно одинарных обратных слешей. Далее выводим на консоль строки s5 и s6, убеждаемся, что их содержимое идентично.
10)Еще один вариант конструктора класса String, создает строку из указанного количества элементов типа char. В данном случае получаем строку s7, состоящую из десяти восклицательных знаков.
11)Используя метод String.ToCharArray, получаем символы строки «"строка C++"» в виде массива char.
12)Вызываем тот же конструктор, что и в строке 2, получаем экзем-
пляр s8.
13)Если в конструкторе указать еще два целых числа, то строка s9 будет инициализирована не всем массивом cstr. Первое число – стартовый элемент массива, второе – количество элементов. В результате строка s9 будет содержать «строка C». Последующий вывод на консоль позволяет убедиться
вэтом.
113

Как видно из таблицы Б.1, строки поддерживают операцию конкатенации (+). Мы этим часто пользовались и ранее, когда выводили данные на консоль. Если при использовании оператора «+» один из аргументов имеет тип string, а второй – нет, то второй аргумент преобразуется к типу string неявным вызовом метода ToString. Однако, надо помнить, что сложение в языке C# выполняется слева направо (см. § 3.3), поэтому в данном примере:
Console.WriteLine(s8 + " " + 1 + 2 + 3);
Console.WriteLine(1 + 2 + 3 + " " + s8);
на консоли увидим следующее:
строка C++ 123 6 строка C++
В первом случае к строке последовательно добавлялись числа, будучи также предварительно преобразованными к строкам. Во втором случае сначала складывались числа, и только потом результат их сложения (6) был сцеплен со строкой. Если такой результат нежелателен, необходимо использовать явное преобразование к строке:
Console.WriteLine(1.ToString() + 2 + 3 + " " + s8);
В этом случае будет использоваться конкатенация строк:
123 строка C++
Если внимательно изучить методы и операторы класса String, можно заметить, что ни один из них строку не модифицирует – при проведении над строкой любой операции, изменяющей ее содержимое, результат возвращается в виде новой строки, а исходная строка остается без изменений. И попытка модификации строки через индексатор вызовет ошибку компиляции:
string s10 = "Hello!";
s10[5] = '?'; // Ошибка
Подробнее об индексаторах мы будем говорить в § 4.5. Что же необходимо сделать, чтобы изменить содержимое строки? Если у нас есть очень большая строка, а в ней требуется модифицировать всего один символ (пусть его позиция хранится в переменной spos), мы вынуждены будем сделать следующее:
string s11 = "Очень длинная строка"; int slen = s11.Length;
int spos = 9;
s11 = s11.Substring(0, spos) + 'Н' + s11.Substring(spos + 1, slen - spos
114

- 1);
Console.WriteLine(s11);
В итоге получим строку «Очень длиНная строка». Но данный код, мало того, что не очень наглядный, еще и очень расточительный в плане ресурсов
–в процессе его работы выделяется память для четырех строк:
1.Сначала для строки «Очень длинная строка»;
2.Затем, после вызова Substring(0, spos), создается новая строка «Очень
дли»;
3.Далее к ней прибавляется символ «Н», получается новая строка «Очень длиН»;
4.Затем добавляем s11.Substring(spos + 1, slen – spos – 1), и получаем окончательный результат «Очень длиНная строка».
Рано или поздно сборщик мусора удалит временные объекты-строки, на которые не осталось ссылок, а это потребует еще некоторого количества дополнительных ресурсов.
Строки с возможностью модификации
Для решения подобных проблем в языке C# есть еще один класс для работы со строками, допускающими модификацию содержимого – это класс System.Text.StringBuilder. Основные его члены приведены в табл. Б.2 (приложение Б).
Теперь изменить один символ в строке очень легко:
StringBuilder s12 = new StringBuilder("Очень длинная строка");
s12[9] = 'Н';
Console.WriteLine(s12);
Класс StringBuilder имеет некоторые особенности. Во-первых, он описан с модификатором sealed (см. § 4.2), поэтому от него нельзя наследоваться. Во-вторых, раз строка может модифицироваться, следовательно, ее длина непостоянна. Если же длина строки меняется, это ведет к фрагментации памяти, да и на регулярное выделение и освобождение памяти тратятся ресурсы ПК. Поэтому в классе StringBuilder есть такое понятие, как емкость. Емкость говорит о том, сколько реально выделено места для хранения строки. Текущая длина строки всегда меньше либо равна ее емкости. Пример:
StringBuilder s13 = new StringBuilder("Hello!");
Console.WriteLine("емкость: " + s13.Capacity);
115

Console.WriteLine("длина: " + s13.Length); for (int i = 0; i < 10; i++) s13.Append('!');
Console.WriteLine("емкость: " + s13.Capacity); Console.WriteLine("длина: " + s13.Length); Console.WriteLine(s13);
s13.Append('!');
Console.WriteLine("емкость: " + s13.Capacity); Console.WriteLine("длина: " + s13.Length);
Результат работы кода:
емкость: 16 длина: 6 емкость: 16 длина: 16
Hello!!!!!!!!!!!
емкость: 32 длина: 17
Видно, что изначально была выделена память для 16 символов, хотя строка состояла из шести. Зато потом, когда мы в цикле прибавляем к строке новые символы, перераспределение памяти не требуется. В итоге строка заполняет всю доступную емкость. Если попытаться добавить к строке еще один символ, ее емкость удвоится.
Вцелом, рекомендация такая. Если ожидается, что в строку будут помещены 10000 символов, но не сразу, а постепенно (например, при чтении из файла и т.п.), то имеет смысл заранее задать сопоставимую емкость строки. При интенсивной работе со строками это повысит общую производительность приложения.
Подобного принципа придерживаются и некоторые другие классы с динамически меняющимся содержимым.
3.1.3.2.Массивы
Вязыке C# массивы любого вида являются объектами, производными от базового класса System.Array. Поэтому, хотя синтаксис определения массива напоминает синтаксис C++ или Java, реально мы создаем при этом экземпляр класса .NET. И каждый объявленный массив наследует члены класса
System.Array.
Данный класс мы рассмотрим несколько позже. Основные его члены представлены в табл. В.1 (приложение В).
Пример: Samples\3.1\3_1_3_array.
116

Одномерные массивы
Для объявления одномерного массива на C# нужно поместить пустые квадратные скобки между именем типа и именами переменных:
<тип>"[]" {<имя переменной> [ = <инициализатор>]} [, ...];
Этот синтаксис отличен от синтаксиса языка C++, в котором квадратные скобки идут после имени переменной. Поскольку любой массив – это экземпляр класса, многие из правил объявления переменных ссылочного типа применяются и к массивам. Например, при объявлении массива на самом деле мы не создаем его. Мы должны явно создать экземпляр массива при помощи оператора new, и только после этого он будет существовать в том смысле, что для его элементов будет выделена память. Например:
int[] x1 = new int [3];
В языке C++ существует возможность инициализации элементов массива при его объявлении (если, конечно, память под него не выделяется динамически), например:
int x[3] = { 1, 2, 3 }; int y[] = { 5, 6, 7, 8 };
В последнем случае компилятор сам подсчитывает количество элементов в списке инициализации, поэтому массив «y» будет иметь размер 4 элемента. Аналогичная возможность существует и в языке C#, за некоторыми ограничениями. В языке C++, если при инициализации переменной «x» указать меньше инициализаторов, чем элементов в массиве, оставшиеся элементы будут заполнены нулями. В языке C# количество инициализаторов должно совпадать с размером массива:
int[] x1 = new int [3] { 1, 2, 3 }; int[] y1 = new int [] { 5, 6, 7, 8 };
Более того, если при инициализации задается список элементов, оператор new можно опускать:
int[] x2 = { 1, 2, 3 }; int[] y2 = { 5, 6, 7, 8 };
Последний вариант синтаксиса можно использовать только при создании массива (т.е. когда объявляется экземпляр массива и/или используется оператор new):
int[] z1 = new int[] { 1, 2 }; // ОК
117

int[] z2 = { 1, 2 }; // ОК int[] z3, z4;
z3 = new int[] { 1, 2 }; // ОК z4 = { 1, 2 }; // Ошибка
Еще один вариант инициализации массива – это инициализация другим экземпляром:
z4 = z3; z3[0] = 123;
Console.WriteLine("z4[0] = " + z4[0]); // z4[0] = 123 z4 = (int[])z3.Clone();
z3[0] = 321;
Console.WriteLine("z4[0] = " + z4[0]); // z4[0] = 123
Надо только помнить, что при присваивании копируется ссылка. Поэтому, если мы в данном примере изменяем элементы массива z3, то тем самым мы изменяем и элементы массива z4. Если мы хотим копировать не ссылки, а содержимое массива, то можно использовать метод Clone. Видно, что после этого ссылки указывают на разные массивы в динамической куче.
Как и в языке C++, если N – количество элементов в массиве, то их индексы лежат в диапазоне от 0 до N – 1, а доступ осуществляется при помощи оператора «[]» (который на самом деле является индексатором, см. § 4.5).
Теперь нам понятно, почему аргументы командной строки передаются в функцию Main при помощи конструкции «string[] args». Это одномерный массив строк, и узнать его размер можно при помощи свойства Length. Можно вывести его элементы на консоль:
Console.WriteLine("Количество аргументов: " + args.Length);
for (int i = 0; i < args.Length; i++)
{
Console.WriteLine("Аргумент {0}: \"{1}\"", i, args[i]);
}
Чтобы задать аргументы командной строки из среды разработки, необходимо зайти в свойства проекта (окно «Проект → Свойства…») и выбрать вкладку «Отладка» (рис. 3.3).
Вводим аргументы, сохраняем изменения и запускаем программу. Вывод этого фрагмента кода будет следующим:
Количество аргументов: 3 Аргумент 0: "это" Аргумент 1: "/мои" Аргумент 2: "-аргументы"
118

Рис. 3.3 – Аргументы командной строки в Visual Studio 2008 Professional
Прямоугольные массивы
Прямоугольные (или ровные) массивы представляют собой многомерные кубы значений. Элементы таких массивов идентифицируются набором индексов – «координат» в многомерном пространстве. Каждое измерение имеет свою размерность, не зависящую от других. Отметим, что многомерные массивы являются важным отличием от других подобных языков (Java), ибо по сравнению с неровными массивами (рассмотренными ниже), обеспечивают большую производительность.
При декларации размерности измерений указываются через запятую:
<тип>"[",[...]"]" {<имя переменной> [ = <инициализатор>]} [, ...];
Варианты инициализации те же, что и для одномерных массивов – явный вызов оператора new (с инициализацией значениями или без), неявный (при инициализации значениями) либо присвоение ссылки на другой массив.
119

Доступ к элементам прямоугольного массива также производится с помощью оператора [], в котором индексы указываются через запятую.
Пример:
double [,] m = new double [10, 10];
string [,] s = {{"11", "12"}, {"21", "22"}, {"31", "32"}};
Console.WriteLine("Размерность m: " + m.Rank);
Console.WriteLine("m: " + m.GetLength(0) + "x" + m.GetLength(1));
Console.WriteLine("Элементов в s: " + s.Length);
Console.WriteLine("s: " + s.GetLength(0) + "x" + s.GetLength(1));
for (int i = 0; i < s.GetLength(0); i++)
{
for (int j = 0; j < s.GetLength(1); j++)
{
Console.Write(s[i, j] + " ");
}
Console.WriteLine();
}
Свойство Length возвращает суммарное число элементов массива, поэтому в данном примере это свойство для массива «s» вернет 6. Для определения размера каждого измерения массива в методе используется метод GetLength. Число измерений массива называется рангом, а его значение позволяет получить свойство Rank.
Вывод на консоль:
Размерность m: 2 m: 10x10
Элементов в s: 6 s: 3x2
11 12
21 22
31 32
Ортогональные массивы
Ортогональные, или неровные (jagged) массивы – это, по сути, массивы массивов. Собственно, формы декларации и доступа вытекают из этого:
<тип>"[]""[]"[...] {<имя переменной> [ = <инициализатор>]} [, ...];
Т.е. если в прямоугольных массивах количество измерений указывалось количеством запятых, то в данном случае – количеством квадратных скобок. Для доступа к элементам ортогонального массива каждый индекс также стоит в отдельных скобках.
Пример:
int[][] ort = new int[3][];
120