Символы и строки

Лабораторная работа №8.

 

Для решения задач, связанных с обработкой текстовой информации, C# предоставляет такие средства:

- символы char,

- неизменяемые строки string,

- изменяемые строки StringBuider

- регулярные выражения RegularExpression.

Символы char

 

Символьный тип char предназначен для хранения одного символа в двухбайтной кодировке Unicode.

При работе с символами часто приходится задавать символьные константы (или символьные литералы). Это можно сделать несколькими способами:

- символом, заключенным в одинарные кавычки;

- escape-последовательностью, задающей код символа;

- Unicode-последовательностью, задающей Unicode-код символа.

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

 

char a = 'Я', // конкретный символ

b = '\x1A', // код символа

c = '\u000F'; // Unicode-код символа

Console.WriteLine("{0}, {1}, {2}", a, b, c);

 

На экран выводится следующее:

 

 

Заметьте, что при использовании второго и третьего способов, перед кодами указывается \x для escape-последовательности и \u для Unicode-последовательности.

Тип char относится к встроенным типам данных C# и соответствует стандартному классу Сhar библиотеки .NET из пространства имен System. В этом классе определено несколько статических методов для работы с символами. Основные методы приведены в таблице 8.1.

 

Таблица 8.1

Некоторые методы класса System.Char

 

Метод Описание
GetNumericValue() Возвращает числовое значение символа, если он является цифрой, и –1 в противном случае
GetUnicodeCategory() Возвращает категорию Unicode-символа (например, цифры разделители строк, буквы в нижнем или верхнем регистре)
IsControl() Возвращает true, если символ является управляющим
IsDigit() Возвращает true, если символ является десятичной цифрой
IsLetter() Возвращает true, если символ является буквой
IsLetterOrDigit() Возвращает true, если символ является буквой или десятичной цифрой
IsLower() Возвращает true, если символ задан в нижнем регистре
IsNumber() Возвращает true, если символ является цифрой (десятичной или шестнадцатеричной)
IsPunctuation() Возвращает true, если символ является знаком препинания
IsSeparator() Возвращает true, если символ является разделителем
IsUpper() Возвращает true, если символ задан в нижнем регистре
IsWhiteSpace() Возвращает true, если символ является пробельным (пробел, перевод строки, возврат каретки)
Parse() Преобразует строку в символ (строка должна состоять из одного символа)
ToLower() Преобразует символ в нижний регистр
ToUpper() Преобразует символ в верхний регистр

 

Далее рассмотрим пример применения нескольких методов класса Char.

 

char a = 'в'; // char a = '5'; //char a = 'Q';

//char a = '+'; //char a = '.'; //char a = ' ';

Console.WriteLine("Символ: {0}", a);

Console.WriteLine("Код символа: {0}", (int)a);

Console.WriteLine("Unicode-категория: {0}",

char.GetUnicodeCategory(a));

Console.WriteLine("Числовое значение: {}", char.GetNumericValue(a));

if (char.IsLetter(a))

{

Console.WriteLine("ToLower: {0}", char.ToLower(a));

Console.WriteLine("ToUpper: {0}", char.ToUpper(a));

}

 

Результаты выполнения этого фрагмента при различных значениях символа a:

 

 

Кроме статических, у класса Char есть и динамические методы, например, метод CompareTo(), позволяющий сравнивать символы. Для совпадающих символов его результатом является нуль, а для несовпадающих – “расстояние” между символами в соответствии с их упорядоченностью в кодировке Unicode. Рассмотрим пример:

 

char ch1 = 'a', ch2 = 'e';

int dist = ch1.CompareTo(ch2);

Console.WriteLine("Расстояние между символами = {0}", dist);

 

Результат выполнения:

 

 

Неизменяемые строки string

 

Переменные типа string представляют собой строки из символов в кодировке Unicode. Как и массивы, строки в C# относятся к ссылочному типу. Создать строку типа string можно разными способами.

- объявление с отложенной инициализацией:

 

string s1;

 

- объявление с явной инициализацией:

с неявным вызовом конструктора:

 

string s2 = "мёд ждём"; //инициализация строковым литералом

 

В таких обычных строковых константах, заключенных в двойные кавычки, некоторые символы интерпретируются особым образом. Используя комбинации символов, начинающихся “\” – обратной косой чертой, можно задать символ перехода на новую строку, символ табуляции и т. п. (см. про управляющие последовательности, используемые в операторах вывода на консоль, в Лабораторной работе № 2 первой части лабораторного практикума). Также можно использовать комбинации вида “\xYYYY”, задающие символ, определяемый шестнадцатеричным кодом YYYY.

 

string s3 = @" ""Бычок"" // Агния Барто

Идет бычок, качается,

Вздыхает на ходу:

- Ох, доска кончается,

Сейчас я упаду!";

Console.WriteLine(s3);

 

На экран выводится:

 

 

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

 

int x = 12345;

string s5 = x.ToString();

 

После инициализации переменной арифметического типа, с помощью метода ToString() преобразуем ее в строку. Строка s5 равна “12345”.

 

с явным вызовом конструктора класса:

 

string s4 = new string('a', 10);

 

Конструктор создает строку из 10 символов 'a'.

 

char [] a={'м', 'ё', 'д', ' ', 'ж', 'д', 'ё', 'м'};

string s6 = new string(a);

 

В этом примере массив символов преобразуется в строку. Строка s6 равна “мёд ждём” и совпадает со строкой s2.

 

string s7 = new string(a, 0, 3);

 

Строка s7 создается из части массива символов, при этом первый параметр – сам массив символов, второй – с какого символа происходит копирование, третий параметр – сколько символов используется для инициализации строки. В результате выполнения указанного оператора строка s7 равна “мёд”.

 

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

Несмотря на то, что строки относятся к ссылочным типам, при выполнении таких операций, как присваивание (=), проверка эквивалентности (== или !=) или конкатенация (сцепление, склеивание, +) строк, работа происходит как со значимыми типами. Рассмотрим это на примере:

 

string s1 = "abc", s2 = "abc";

string s3 = s1 + s2; // s3 = "abcabc";

if (s1 == s2) Console.WriteLine("s1 и s2 равны");

else Console.WriteLine("s1 и s2 не равны");

if (s1 != s3) Console.WriteLine("s1 и s3 не равны");

else Console.WriteLine("s1 и s3 равны");

 

Результат выполнения:

 

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

 

char c1 = s1[0], c2 = s2[2];

Console.WriteLine("c1 = '{0}', c2 = '{1}'", c1, c2);

 

Результат выполнения:

 

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

Типу string, который является встроенным типом C#, соответствует базовый класс System.String библиотеки .NET. Он предоставляет большой набор методов для работы со строками. Некоторые из них приведены в таблице 8.2. Напомним, что вызов статических методов происходит через обращение к имени класса, например, String.Compare(s1, s2), а вызов свойств и экземплярных методов выполняется через обращение к экземплярам класса, например, s.ToUpper().

 

Таблица 8.2

Свойства и методы класса System.String

 

Название Вид Описание
Compare() статический метод Сравнение двух строк в лексикографическом порядке[1] с учетом или без учета регистра
CompareTo() экземплярный метод Сравнение строки с другой строкой
Concat() статический метод Конкатенация произвольного числа строк
Copy() статический метод Создание копии строки
Format() статический метод Форматирование строки (неявно использовался нами при выводе на консоль)
IndexOf() IndexOfAny() LastIndexOf() LastIndexOfAny() экземплярные методы Определение индексов первого и последнего вхождения заданной подстроки или символа или любого символа из заданного массива в данную строку
Insert() экземплярный метод Вставка подстроки с заданной позиции
Join() статический метод Слияние массива строк в единую строку. Между элементами массива вставляются разделители
Length свойство Возвращает длину строки
PadLeft() PadRigth() экземплярные методы Выравнивают строки по левому или правому краю путем вставки нужного числа пробелов или других символов в начале или в конце строки
Remove() экземплярный метод Удаление подстроки, начинающейся с заданной позиции
Replace() экземплярный метод Замена всех вхождений заданной подстроки или символа новыми подстрокой или символом
Split() экземплярный метод Разделяет строку на элементы, используя разные разделители. Результаты помещаются в массив строк
StartWith() EndWith() экземплярные методы Возвращают true, если строка начинается (или заканчивается) заданной подстрокой
Substring() экземплярный метод Выделение подстроки, начиная с заданной позиции
ToCharArray() экземплярный метод Преобразование строки в массив символов
ToLower() ToUpper() экземплярные методы Преобразование строки к нижнему или верхнему регистру
Trim() TrimStart() TrimEnd() экземплярные методы Удаление пробелов в начале и конце строки или только с одного ее конца

 

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

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

 

string s1 = "Абракадабра";

string s2 = string.Copy(s1);

Console.WriteLine("Строка: {0} \nДлина строки: {1}", s1, s1.Length);

int i = s1.IndexOf("аб");

Console.WriteLine("Индекс первого вхождения подстроки 'аб': {0}", i);

s1 = s1.Replace("ра", "УРА");

Console.WriteLine("После замены \"ра\" на \"УРА\": {0}", s1);

s1 = s1.Remove(4, 2);

Console.WriteLine("После удаления 2 символов с позиции 4: {0}", s1);

s1 = s1.Insert(4, "ак");

Console.WriteLine("После вставки подстроки \"ак\" с 3 символа: {0}",

s1);

s1 = s1.Replace('Р', 'р');

Console.WriteLine("После замены 'Р' на 'р': {0}", s1);

s2 = s2.ToLower();

string s3 = s2.ToUpper();

Console.WriteLine("Версия исходной строки со строчными буквами :

{0}", s2);

Console.WriteLine("Версия исходной строки с прописными буквами: {0}",

s3);

i = String.Compare(s2, s3, true); //сравниваем без учета регистра

if (i == 0) Console.WriteLine("Равны без учета регистра.");

else Console.WriteLine("Не равны без учета регистра.");

 

На экране выводятся следующие результаты:

 

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

 

string s = "Идет бычок качается"; // исходная строка

char[] div = { ' ' }; // в качестве разделителя используем пробел

string[] words = s.Split(div); // разбиваем строку на части

Console.WriteLine("Результат разбиения строки на части: ");

for (int i = 0; i < words.Length; i++)

Console.WriteLine(i + " " + words[i]);

// собираем части в одну строку, в качестве разделителя используем *

string all = String.Join(" * ", words);

Console.WriteLine("Результат сборки: ");

Console.WriteLine(all);

 

На экране выводятся следующие результаты:

 

 

Если в нашем примере в качестве раздилителя при сборке использовать один пробел, то результат совпадет с исходными данными, т. е. методы Split() и Join() будут выполнять взаимно обратные преобразования. Если массив разделителей состоит из нескольких элементов, то результат сборки не будет совпадать с исходными данными.

Изменим в нашем примере первые две строки – зададим другие строку s и массив разделителей div.

 

string s = "Идет бычок, качается,"; // исходная строка

// в качестве разделителей используем пробел и запятую

char[] div = { ' ', ',' };

Результат отличается от предыдущего:

 

 

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

 

for (int i = 0; i < words.Length; i++)

if (words[i].EndsWith(","))

words[i] = words[i].Remove(words[i].Length - 1, 1);

 

Используя метод Split(), можно организовать удобный ввод двумерного массива:

 

//объявляем массив из row строк и col столбцов

int row = 3, col = 4;

int[,] MyArrray = new int[row, col];

char[] div = { ' ' }; //задаем разделитель

for (int i = 0; i < row; i++)

{

string s = Console.ReadLine(); // считываем строку

string[] elements = s.Split(div); // разбиваем на элементы

// преобразуем массив строк в массив целых чисел

for (int j = 0; j < col; j++)

MyArrray[i, j] = int.Parse(elements[j]);

}

 

Этот фрагмент будет верно формировать двумерный массив размерности 3×4, только если данные будут правильно вводиться – три строки по четыре числа, разделенных одним пробелом. Если ввести лишний пробел или какой-либо иной символ, произойдет ошибка.

При работе с объектами класса string необходимо помнить, что это неизменяемые строки – методы, возвращают новые копии строк, а исходные при этом остаются в памяти и засоряют ее, при этом ссылки на них теряются. Борется с таким засорением Garbage Collector (сборщик мусора), но это сказывается на производительности программы, поэтому, если в приложении нужно изменять строку, то лучше использовать не класс string, а класс StringBuilder.

Изменяемые строки StringBuilder

 

Для работы со строками, которые можно изменять, в C# существует класс StringBuilder, определенный в пространстве имен System.Text.

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

Рассмотрим примеры создания изменяемых строк:

 

//создание пустой строки, размер по умолчанию 16 символов

// (значение по умолчанию зависит от реализации)

StringBuilder sb1 = new StringBuilder();

 

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

StringBuilder sb2 = new StringBuilder("ABCDEF");

 

//создание пустой строки и выделение памяти под 50 символов

StringBuilder sb3 = new StringBuilder(50);

 

//создание пустой строки, выделение памяти под 10 символов

// и задание максимальной емкости под 50 символов

StringBuilder sb4 = new StringBuilder(10, 50);

 

//инициализация строки и выделение памяти под 50 символов

StringBuilder sb5 = new StringBuilder("ABCDEF", 50);

 

//инициализация подстрокой "CDE" и выделение памяти под 50 символов

StringBuilder sb6 = new StringBuilder("ABCDEF", 2, 3, 50);

 

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

Над строками класса StringBuilder, как и над строками класса string, определены операции присваивания (=), проверки эквивалентности (== и !=) и обращения к элементу по индексу ([]), но не определена операция конкатенации (+) (вместо нее существует метод Append(), дописывающий новую строку в конец существующей). Со строкой класса StringBuilder можно работать как с одномерным массивом символов, причем в отличие от класса string, здесь допускается не только чтение отдельного символа, но и его изменение. Продемонстрируем это на примере:

 

StringBuilder sb1 = new StringBuilder("abc");

StringBuilder sb2 = new StringBuilder("abc");

Console.WriteLine("sb1={0} sb2={1}", sb1, sb2);

if (sb1 == sb2) Console.WriteLine("sb1 и sb2 равны");

else Console.WriteLine("sb1 и sb2 не равны");

 

sb2 = sb1.Append("def"); // sb1.Append("def");

Console.WriteLine("sb1={0} sb2={1}", sb1, sb2);

if (sb1 != sb2) Console.WriteLine("sb1 и sb2 не равны");

else Console.WriteLine("sb1 и sb2 равны");

 

StringBuilder sb3 = new StringBuilder();

sb3 = sb1;

char c1 = sb3[1];

sb1[1] = '!';

Console.WriteLine("c1 = '{0}', sb3 = '{1}'", c1, sb3);

 

На экран выводится следующее:

 

 

Заметьте, что работая со строками StringBuilder, мы работаем со ссылками на объект, и операции проверки на эквивалентность работают не со значениями строк, а с ссылками на них. Для склеивания строки здесь используется метод Append(). Если изменить строку, в которой вызывается этот метод, на оператор, указанный в комментариях, то в результате sb1 и sb2 при второй проверке окажутся не равны.

Кроме перечисленных выше операций, для строк класса StringBuilder существует еще ряд свойств и методов. Некоторые из них приведены в таблице 8.3.

 

Таблица 8.3

Свойства и методы класса StringBuilder

 

Название Вид Описание
Append() экземплярный метод Добавление данных (любых встроенных типов, массивы символов, строки и подстроки string) в конец строки
AppendFormat() ээкземплярный метод Добавление форматированной строки в конец строки
Capacity изменяемое свойство Емкость буфера (если устанавливаемое значение меньше текущей длины строки или больше максимального, то генерируется исключение ArgumentOutOfRangeException)
Chars изменяемое свойство Возвращает из массива или устанавливает в массиве символ с заданным индексом. Вместо него можно пользоваться квадратными скобками []
CopyTo() экземплярный метод Копирует подмножество символов строки в массив символов char
EnsureCapacity() экземплярный метод Изменение емкости буфера (если значение параметра меньше размера строки, то емкость устанавливается такой, чтобы гарантировать размещение строки)
Equals() экземплярный метод Возвращает true, если объекты имеют одну и ту же длину и состоят из одних и тех же символов
Insert() экземплярный метод Вставка подстроки с заданной позиции
Length изменяемое свойство Длина строки (если присвоить значение 0, содержимое сбрасывается и строка очищается)
MaxCapacity неизменяемое свойство Возвращает наибольшее количество символов, которое может быть размещено в строке (2 147 483 647)
Remove() экземплярный метод Удаление подстроки, начинающейся с заданной позиции
Replace() экземплярный метод Замена всех вхождений заданной подстроки или символа новой подстрокой или символом
ToString() экземплярный метод Преобразование в строку типа string
Отметим, что методов в классе StringBuilder гораздо меньше, чем в классе String, но они позволяют изменять значения строк и при этом эффективнее использовать память. Преимущественно класс StringBuilder используется для строк большой длины или при большом количестве изменений в строках для повышения производительности.

На практике часто комбинируют использование изменяемых и неизменяемых строк, при этом работа со строками происходит следующим образом: сначала конструируется строка класса StringBuilder, над ней выполняются операции, требующие изменение значения; затем полученная строка преобразуется в строку класса String и над ней выполняются операции, не требующие изменения значения строки.

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

 

static void Main()

{

StringBuilder sb = new StringBuilder("Число"); StringInfo(sb);

sb.Append(" P равно"); StringInfo(sb);

sb.AppendFormat(" {0:f4}", Math.PI); StringInfo(sb);

sb.Remove(8, 5); StringInfo(sb);

sb.Insert(8, "="); StringInfo(sb);

sb.Replace("P", "ПИ"); StringInfo(sb);

Console.WriteLine("Максимальный объем буфера: " +

sb.MaxCapacity);

sb.EnsureCapacity(16);

Console.WriteLine("Объем буфера: {0}", sb.Capacity);

sb.EnsureCapacity(100);

Console.WriteLine("Объем буфера: {0}", sb.Capacity);

Console.ReadKey();

}

 

static void StringInfo(StringBuilder s)

{

Console.WriteLine("Строка: " + s);

Console.WriteLine("Текущая длина строки: " + s.Length);

Console.WriteLine("Объем буфера: {0}\n", s.Capacity);

}

На экран выводятся следующие результаты:

 

В этом примере демонстрируется работа нескольких методов изменения строк, а также изменение емкости буфера при изменении строки, а также при помощи метода EnsureCapacity(). Обратите внимание, что при увеличении размера строки автоматически растет текущая емкость буфера, а при уменьшении строки емкость буфера не сокращается.

Со строкой класса StringBuilder можно работать как с массивом символов. Пример:

 

StringBuilder sb = new StringBuilder("1+4+3+7");

int s = 0;

for (int i = 0; i < sb.Length; i++)

if (char.IsDigit(sb[i])) s += int.Parse(sb[i].ToString());

 

Определите, чему в результате будет равна переменная s.

 


Задания для самостоятельного выполнения:

1. Даны две строки. Если они начинаются с одинаковых символов, то вывести на экран “yes”, иначе “no”.

2. Определить количество символов ‘a’ в строке.

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

4. Определить количество гласных букв в строке.

5. Определить, имеются ли в строке два соседних одинаковых символа.

6. Вывести на экран по одному разу в алфавитном порядке все строчные латинские буквы, входящие в заданную строку.

7. Вывести на экран все символы, расположенные до первой точки.

8. Вывести на экран все символы, расположенные после последнего тире.

9. Подсчитать количество цифр, встречающихся в строке.

10. Подсчитать сумму чисел, встречающихся в строке. Символ ‘-’ перед числом считается знаком отрицательного числа. Все остальные символы (кроме цифр и минуса) считаются разделителями между числами.

11. Если длина строки нечетное число, то удалить средний символ, иначе удалить два средних символа.

12. Если в строке имеются подряд идущие одинаковые символы, то изменить строку, оставив только один из таких символов.

13. Сформировать новую строку, отличающуюся от исходной тем, что группы, идущих подряд одинаковых символов, разделены символом ‘*’.

14. В строке, содержащей минимум два символа ‘-’, переставить в обратном порядке все символы, расположенные между первым и последним вхождениями символа ‘-’.

15. Удвоить каждое вхождение буквы, введенной пользователем.

16. Удалить все вхождения буквы, введенной пользователем.

17. В исходной строке после каждой заданной пользователем буквы вставить заданную подстроку.

18. Заменить все вхождения подстроки s1 на подстроку s2.

19. Удалить из строки последовательности символов, расположенных между круглыми скобками вместе со скобками (считается, что скобки в строке расставлены без ошибок).

20. Подсчитать количество слов в сообщении и вывести на экран только те из них, которые начинаются с буквы ‘a’ и заканчиваются на букву ‘z’.

21. Вывести слова предложения, которые встречаются в тексте ровно один раз.

22. Вывести те слова предложения, которые встречаются в сообщении не менее трех раз.

23. Вывести слова сообщения, которые содержат ровно две цифры.

24. Найти в тексте слова-рифмы для слова максимальной длины (рифма – совпадение трех последних символов).

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

26. Удалить из текста самое короткое слово.

27. Удалить из сообщения все слова, содержащие введенный пользователем символ без учета регистра.

28. Вывести слова сообщения в алфавитном порядке.

29. Вывести слова сообщения по убыванию их длин.

30. Исправить строку в соответствии с правилом расстановки знаков препинания: перед каждым знаком препинания (кроме тире) пробел отсутствует, а после любого знака препинания стоит ровно один пробел; в многоточии между точками пробелы отсутствуют.


[1] Сравнение в лексикографическом порядке производится на основе упорядочения, присущего набору символов, т. е. в соответствии с кодами символов. Больше тот символ, код которого больше.