Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Объектно-ориентированное программирование.-6

.pdf
Скачиваний:
8
Добавлен:
05.02.2023
Размер:
4.5 Mб
Скачать

Если бы ссылка 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