Символы и строки
Класс String
Основным типом при работе со строками является тип string, задающий строки переменной длины. Класс string в языке C# относится к ссылочным типам. Над строками - объектами этого класса - определен широкий набор операций, соответствующий современному представлению о том, как должен быть устроен строковый тип.
Объявление строк. Конструкторы класса string
Объекты класса string объявляются как все прочие объекты простых типов - с явной или отложенной инициализацией, с явным или неявным вызовом конструктора класса. Чаще всего, при объявлении строковой переменной конструктор явно не вызывается, а инициализация задается строковой константой. Но у класса string достаточно много конструкторов. Они позволяют сконструировать строку из:
- символа, повторенного заданное число раз;
- массива символов char[];
- части массива символов.
Некоторым конструкторам в качестве параметра инициализации можно передать строку, заданную типом char*. Но все это небезопасно, и подобные примеры приводиться и обсуждаться не будут. Приведу примеры объявления строк с вызовом разных конструкторов:
public void TestDeclStrings() { //конструкторы string world = "Мир"; //string s1 = new string("s1"); //string s2 = new string(); string sssss = new string('s',5); char[] yes = "Yes".ToCharArray(); string stryes = new string(yes); string strye = new string(yes,0,2); Console.WriteLine("world = {0}; sssss={1}; stryes={2};"+ " strye= {3}", world, sssss, stryes, strye); }
Объект world создан без явного вызова конструктора, а объекты sssss, stryes, strye созданы разными конструкторами класса string. Заметьте, не допускается явный вызов конструктора по умолчанию - конструктора без параметров. Нет также конструктора, которому в качестве аргумента можно передать обычную строковую константу. Соответствующие операторы в тексте закомментированы.
Операции над строками
Над строками определены следующие операции:
- присваивание (=);
- две операции проверки эквивалентности (= =) и (!=);
- конкатенация или сцепление строк (+);
- взятие индекса ([]).
Начну с присваивания. Поскольку string - это ссылочный тип, в результате присваивания создается ссылка на константную строку, хранимую в "куче". С одной и той же строкой в "куче" может быть связано несколько переменных строкового типа. Эти переменные являются синонимами - разными именами одного и того же объекта, разделяя общую память.
В отличие от других ссылочных типов операции, проверяющие эквивалентность, сравнивают значения строк, а не ссылки. Эти операции выполняются так же, как над значимыми типами.
Бинарная операция "+" сцепляет две строки, приписывая вторую строку к хвосту первой.
Возможность взятия индекса при работе со строками отражает тот приятный факт, что строку можно рассматривать как массив и получать без труда каждый ее символ. Каждый символ строки имеет тип char, он доступен только для чтения, но не для записи.
Вот пример, в котором над строками выполняются данные операции:
/// <summary> /// Операции над строками /// </summary> public void TestOpers() { const string DEL = "->"; string s1 = "ABC", s2 = "CDE"; string s3 = s1 + s2; string s4 = s3.Substring(0, 3); bool b1 = (s1 == s4); char ch1 = s1[2]; Console.WriteLine(s1 + DEL + s2 + DEL + s3 + DEL + b1.ToString() + DEL + ch1.ToString()); }
Строковые константы
Без констант не обойтись. В C# существуют два вида строковых констант:
- обычные константы, которые представляют строку символов, заключенную в кавычки;
- @-константы, заданные обычной константой c предшествующим знаком @.
В обычных константах некоторые символы интерпретируются особым образом. Связано это, прежде всего, с тем, что необходимо уметь задавать в строке непечатаемые символы, такие как, например, символ табуляции. Возникает необходимость задавать символы в виде escape -последовательностей. Для всех этих целей используется комбинация символов, начинающаяся символом "\" - обратная косая черта. Так, пары символов: "\n", "\t", "\\", "\"" задают соответственно символ перехода на новую строку, символ табуляции, сам символ обратной косой черты, символ кавычки, вставляемый в строку, но не сигнализирующий о ее окончании. Комбинация "\xNNNN" задает символ, определяемый шестнадцатеричным кодом NNNN. Хотя такое решение возникающих проблем совершенно естественно, иногда возникают неудобства: например, при задании констант, определяющих путь к файлу, приходится каждый раз удваивать символ обратной косой черты. Это одна из причин, по которой появились @-константы.
В @-константах все символы трактуются в полном соответствии с их изображением. Поэтому путь к файлу лучше задавать @-константой. Единственная проблема в таких случаях: как задать символ кавычки, чтобы он не воспринимался как конец самой константы. Решением является удвоение символа. Вот соответствующие примеры:
/// <summary> /// Два вида констант /// </summary> public void TestConstants() { string s1 = "\x50"; string s2 = @"\x50"""; bool b1 = (s1 == s2); Console.WriteLine("s1={0}, s2={1}, b1={2}", s1, s2, b1); s1 = "c:\\c#book\\ch5\\chapter5.doc"; s2 = @"c:\c#book\ch5\chapter5.doc"; b1 = (s1 == s2); Console.WriteLine("s1={0}, s2={1}, b1={2}", s1, s2, b1); s1 = "\"A\""; s2 = @"""A"""; b1 = (s1 == s2); Console.WriteLine("s1={0}, s2={1}, b1={2}", s1, s2, b1); }
Первая проверка эквивалентности строк в этом примере даст значение False, остальные - True.
Неизменяемый класс string
В языке C# существует понятие неизменяемый (immutable) класс. Для такого класса невозможно изменить значение объекта. Методы могут создавать новый объект на основе существующего, но не могут изменить значение существующего объекта.
К таким неизменяемым классам относится и класс string. Ни один из методов этого класса не меняет значения существующих объектов. Когда метод изменяет строку, результатом является новая строка - новый объект в куче. Невозможность изменять значения строк касается не только методов. Аналогично при работе со строкой как с массивом разрешено только чтение отдельных символов, но не их замена. Оператор присваивания, в котором делается попытка изменить первый символ строки, не допустим, а потому закомментирован:
//Неизменяемые значения s1= "Zenon"; ch1 = s1[0]; //s1[0]='L';
По какой причине на класс string наложены такие строгие ограничения? Цель благая. Хотя класс string по целому ряду причин целесообразно отнести к ссылочным типам, но для переменных этого типа хотелось бы иметь ту же семантику, что и для переменных арифметического типа, полагая, что каждая переменная имеет собственную память. Неизменяемость типа обеспечивает эту семантику. Поясним ситуацию на примере.
/// <summary> /// String - неизменяемый тип данных! /// </summary> public void TestUnchanged() { string s1 = "Zenon"; string s2 = s1; Console.WriteLine( "s1 = " + s1 + "\t s2 = " + s2);
Обе переменные указывают на один объект, у них общая память, одно и то же значение. Представим себе, что s1 хочет изменить свое значение, вызвав метод Insert для вставки нового текста. Но метод Insert класса string реализован как функция, возвращающая строку в качестве результата. Поэтому возможно лишь такое присваивание
s1 = s1.Insert(s1.Length, " - это философ!"); Console.WriteLine( "s1 = " + s1 + "\t s2 = " + s2);
Теперь в куче два объекта, s1 стала ссылкой на вновь созданный объект, изменение ее значения никак не отразилось на переменной s2. Переменная s2 также может изменить свое значение, например, так:
s2 = s2.Replace(s2[0], 'L') + " - это музыкант!"; Console.WriteLine( "s1 = " + s1 + "\t s2 = " + s2); }
Строка "Zenon" осталась в куче висячей, без ссылок на нее и является объектом для сборщика мусора. Как видите, невозможность изменять значение строки непосредственно в памяти, где хранится ее значение, гарантирует, что изменение значения строковой переменной никак не отражается на других строковых переменных - это соответствует семантике поведения развернутых типов.
На рис. 7.3 показаны результаты работы метода TestUnchanged.
Статические свойства и методы класса string
Метод Format
Метод Format в наших примерах встречался многократно. Всякий раз, когда выполнялся вывод результатов на консоль, неявно вызывался и метод Format. Рассмотрим оператор печати:
Console.WriteLine("s1={0}, s2={1}", s1,s2);
Здесь строка, задающая первый аргумент метода, помимо обычных символов содержит форматы, заключенные в фигурные скобки, и, как следствие, автоматически вызывается метод Format, форматирующий строку перед выдачей ее на печать. В данном примере используется простейший вид формата, - он определяет объект, который должен быть подставлен в участок строки, занятый данным форматом. Помимо неявных вызовов метода Format нередко возникает необходимость явного форматирования строки.
Давайте рассмотрим общий синтаксис метода Format и используемых в нем форматов. Метод Format, как и большинство методов, является перегруженным и может вызываться с разным числом параметров. Первый необязательный параметр метода задает провайдера, определяющего национальные особенности, которые используются в процессе форматирования. В качестве такого параметра должен быть задан объект, реализующий интерфейс System.IFormatProvider. Если этот параметр не задан, то используется культура, заданная по умолчанию. Вот примеры сигнатуры двух реализаций этого метода:
public static string Format(IFormatProvider, string, object); public static string Format(string, params object[]);
Параметр типа string задает форматируемую строку. Заданная строка содержит один или несколько форматов, составляющих список форматов. Признаком формата в строке являются фигурные скобки, окружающие формат. Списку форматов ставится в соответствие список объектов, следующий за форматируемой строкой. Чаще всего оба списка имеют одинаковую длину, но это не обязательное требование, поскольку один и тот же объект может по-разному форматироваться. Каждый формат однозначно определяет объект из списка объектов. Этот объект преобразуется в строку текста, текст форматируется в соответствии с параметрами, задаваемыми форматом, и подставляется в то место строки, где расположен формат. Так что форматы в строке - это держатели места (placeholder), куда подставляется форматируемый текст. Метод Format в качестве результата возвращает переданную ему строку, где все форматы заменены строками, полученными в результате форматирования объектов.
Общий синтаксис, специфицирующий формат, таков:
{N [,M [:<коды_форматирования>]]}
Обязательный параметр N задает индекс объекта в списке объектов. Индексация объектов начинается с нуля, как это принято в массивах.
Второй параметр M, если он задан, определяет минимальную ширину поля, которое отводится строке, вставляемой вместо формата. Параметр M может быть положительным или отрицательным, в зависимости от этого производится выравнивание подставляемой строки по левому или правому краю поля, отводимого вставляемому тексту.
Третий необязательный параметр задает коды форматирования, указывающие, как следует форматировать объект. Применяются разные коды форматирования для числовых данных, дат, перечислений. Например, для числовых данных код C (currency) говорит о том, что параметр должен форматироваться как валюта с учетом национальных особенностей представления. Код P (percent) задает форматирование в виде процентов с точностью до сотой доли. Код F для дат позволяет вывести в полном формате дату и время. Полный набор кодов форматирования можно посмотреть в справочной системе. Частично их эффект демонстрируется в данном примере:
enum Rainbow {красный, желтый, голубой}; /// <summary> /// Форматирование чисел, дат, перечислений /// </summary> public void TestFormat() { int x = 77; double p = 0.52; double d = -151.17; DateTime today = DateTime.Now; //Форматирование чисел string s = string.Format("Итого:{0:P}\n" + "Сумма_1 = {1:C}\n" + "x = {1:#######} рублей\n" + "d = {2,-10:F} рублей\n" + "d = {2, 10:F} рублей\n" + "d = {2:E}\n", p, x, d); Console.WriteLine(s); //Форматирование дат s = string.Format("Время: {0:t}, Дата: {0:d}\n" + "Дата и время - {0:F}", today); Console.WriteLine(s); //Форматирование перечислений s = string.Format("Цвет1: {0:G}, Цвет2: {1:F}\n", Rainbow.голубой, Rainbow.красный); Console.WriteLine(s); //Национальные особенности System.Globalization.CultureInfo ci = new System.Globalization.CultureInfo("en-US"); s = string.Format(ci, "Итого:{0,4:C} ", 77.77); Console.WriteLine(s); }//TestFormat
Приведу некоторые комментарии к этой процедуре. Заметьте, консольный вывод всегда можно свести к форме Console.WriteLine(s), если строку s предварительно отформатировать, используя явный вызов метода Format. Этот метод полезно вызывать и в Windows-проектах при выводе специфических данных - денежных сумм, процентов, дат и времени. В примере показано использование различных спецификаций формата с разными кодами форматирования для таких данных. В заключительном фрагменте кода демонстрируется задание провайдером национальных особенностей. С этой целью создается объект класса CultureInfo, инициализированный так, чтобы он задавал особенности форматирования, принятые в США. Класс CultureInfo наследует интерфейс IFormatProvider. Российские национальные особенности форматирования установлены по умолчанию. При необходимости их можно установить таким же образом, как это сделано для США, задав соответственно константу "ru-RU". Результаты работы метода показаны на рис. 7.4.