![]() |
Работа со строками в C# |
![]() |
Создание строковых значений
В предыдущих работах был представлен наиболее часто используемый способ
создания переменных типа string
- с помощью непосредственной строковой
константы:
string s = "asdfgh";
Рассмотрим альтернативные подходы к инициализации строковых переменных:
string s1 = @"Строка с символом \";
string s2 = s1;
string s3 = new string('a', 20);
char[] chars = { 'a', 's', 'd', 'f' };
string s4 = new string(chars);
string s5 = null;
int x = 5;
string s6 = $"Переменная х равна {x}";
string s7 = string.Format("Переменная х равна {0}", x);
Строка s1
так же инициализируется строковой константой, однако перед
открывающей кавычкой находится символ @
. Добавление этого символа позволяет
использовать внутри строки другой символ - '\'
. В обычных инициализаторах
символ косой черты сигнализирует о начале управляющей последовательности
(escape sequence), которые необходимы для набора различных непечатаемых
символов.
Строка s2
создается путем копирования другой строки. Это может быть нужно,
когда требуется одновременно каким-либо образом изменить некоторую строку и
сохранить её неизменённый вариант.
Инициализатор строки s3
создаёт её путем повторения символа, указанного в
первом аргументе, столько раз, сколько указано во втором аргументе.
Строка s4
создаётся на основе массива отдельных символов.
Для строки s5
в качестве инициализатора применяется специальное значение
null
. Это значение используется для того, чтобы обозначить “отсутствие”
строки. Над строкой, хранящей null
нельзя производить никакие операции, кроме
перезаписывания её новой строкой.
Объявление s6
содержит пример интерполяции строк. Интерполированная строка
должна иметь символ $
перед открывающей кавычкой, а внутри такой строки можно
использовать произвольные выражения C#, обрамленные фигурными скобками. В
простейшем случае такие выражения содержат имя переменной, содержимое которой
требуется подставить в строку. Если тип выражения отличается от string
, то
над ним автоматически вызывается .ToString()
. Именно благодаря этому в примере
выше интерполяция переменной int x
не вызывает ошибки.
Наконец, s7
является примером использования форматных строк. По своему
смыслу форматные строки очень похожи на интерполированные, однако эта концепция
встречается во многих языках программирования и сама по себе гораздо старше.
Первый аргумент функции string.Format()
принимает строку, в которой находятся
вставки вида {n}
, где n
- некоторое целое число. После строки следует
произвольное число аргументов, которое должно соответствовать количеству вставок.
Каждый аргумент подставляется вместо соответствующей его номеру вставки. Отличие
от интерполяции заключается в том, что каждая вставка форматной строки может
иметь дополнительные модификаторы, задающие ширину вставляемой строки, её
выравнивание и другие параметры. Функция string.Format()
необычна еще тем,
что имеет произвольное число аргументов.
Операции над строками
Прежде чем рассматривать возможные операции над данными типа string
, отметим,
что любое изменение строки в действительности приводит к созданию новой строки.
Конкатенация
Приписывание одной строки к другой называется конкатенацией и реализуется
с помощью оператора +
:
string s = "aaa" + "bbb";
С помощью функции string.Join()
можно, во-первых, конкатенировать несколько
строк или массив строк, а во-вторых, разместить между объединяемыми строками
другую строку:
string s1 = string.Join(" ", "a", "b", "c"); // даёт "a b c"
string[] words = { "one", "two", "three" };
string s2 = string.Join(", ", words); // даёт "one, two, three"
Вариант string.Join()
, использованный для инициализации s1
, подобно
string.Format()
, является функцией с произвольным числом аргументов.
Функция string.Concat()
эквивалентна string.Join()
, у которой первый
аргумент зафиксирован в значение ""
. Иными словами, string.Concat()
конкатенирует строки без возможности вставки промежуточного значения:
string s1 = string.Concat("a", "b", "c"); // даёт "abc"
Чтение отдельных символов
Как уже отмечалось ранее, тип string
внутренне представлен как неизменяемый
массив char[]
. Это означает, что доступ к отдельным символам строки можно
получать с помощью оператора []
:
string s = "abc";
char c = s[1]; // даёт 'b'
Строку можно преобразовать в массив отдельных символов с помощью следующей функции:
string s = "abc";
char[] arr = s.ToCharArray(); // даёт массив {'a', 'b', 'c'}
Функции .ToCharArray()
и использованная ранее .ToString()
имеют
синтаксическую особенность - их первый аргумент располагается не внутри скобок,
а слева от символа '.'
перед именем функции. Функции такого вида называются
методами, однако подробно это понятие будет рассматриваться в последующих
работах. На данный момент, достаточно вместо строки arg1.Func(arg2, arg3, ...)
мысленно представлять Func(arg1, arg2, arg3, ...)
.
Вычисление длины строки
Длину данной строки можно получить с помощью выражения .Length
:
string input = Console.ReadLine();
.WriteLine(string.Format("Пользователь ввел {0} символов", input.Length)); Console
Важно отметить, что .Length
является не функцией, а свойством, поэтому
пустые скобки при обращении не пишутся. Подробнее свойства будут рассмотрены
в следующих лабораторных работах.
Разбиение на подстроки
Функция .Split()
по своему смыслу производит операцию, обратную к .Join()
:
string s = "one, two, three";
string[] words = s.Split(", "); // даёт массив {"one", "two", "three"}
Другой вариант вызова позволяет указать несколько разделителей:
string s = "one,two|three/four";
string[] words = s.Split(',', '|', '/'); // даёт массив {"one", "two", "three", "four"}
Сравнение строк
Простейшим способом проверки эквивалентности двух строк является оператор ==
:
string login = Console.ReadLine();
if (login == "Vasya")
.WriteLine("Добро пожаловать, " + login); Console
Однако, существует более сложная функция для сравнения строк, string.Compare
:
string str1 = "[ hello vasya ]";
string str2 = "{ hello petya }";
bool eq = string.Compare(str1, 2, str2, 2, 5); // вернёт true
Приведённый выше вариант вызова string.Compare
принимает сравниваемые строки
в первом и третьем аргументах; индекс символа с которого начинать сравнение в
каждой из строк - во втором и четвёртом аргументах; количество сравниваемых
символов - в пятом аргументе. Таким образом, несмотря на то, что строки str1
и str2
отличаются, функция вернет true
, т.к. сравнение производится только
над словом “hello”.
Другой полезный вариант вызова string.Compare
позволяет сравнивать строки, не
обращая внимания на регистр символов:
bool eq = string.Compare("HELLO", "hello", true); // вернёт true
Третий аргумент типа bool
определяет, нужно ли игнорировать регистр.
Функции .StartsWith()
и .EndsWith()
определяют, начинается ли или
заканчивается ли строка заданным символом или строкой, соответственно:
bool yes = "asd".StartsWith('a'); // вернёт true
= "asd".EndsWith("sd"); // вернёт true yes
Обрезка по краям строки
При работе с неформатированными строковыми данными часто возникает необходимость их нормализации, т.е. удаления лишних символов и приведения к некоторому “стандартному” виду. Примером нормализации может служить удаление произвольного числа пробелов в начале и в конце строки:
string s1 = " hello ";
string s2 = s1.Trim(); // вернёт "hello"
В альтернативном способе вызова .Trim()
существует возможность передать
список символов, подлежащих обрезке:
string s1 = "{[(hel|lo})]";
string s2 = s1.Trim('{', '[', '(', ')', ']', '}', '|'); // вернёт "hel|lo"
В примере выше необходимо обратить внимание на следующие моменты:
- Все скобки по краям строки будут удалены, несмотря на то, что они следуют в разном порядке;
- Символ
'|'
не будет удалён из середины слова “hel|lo”, несмотря на то, что он присутствует в аргументах.Trim()
, т.к. находится в середине слова.
Функции .TrimStart()
и .TrimEnd()
полностью аналогичны .Trim()
, но
работают только с начала или только с конца строки, соответственно.
Изменение регистра
Функция .ToLower()
переводит символы строки (те, к которым это понятие
применимо) в нижний регистр:
string s = "asdASD123(){}[]".ToLower(); // вернёт "asdasd123(){}[]"
Функция .ToUpper()
работает аналогичным образом и переводит символы строки
в верхний регистр.
Поиск внутри строки
Функция .Contains()
позволяет ответить на вопрос, содержит ли данная строка
определенный символ или группу символов:
bool yes = "bzzzt".Contains('z'); // вернёт true
= "bzzzt".Contains("zzz"); // вернёт true yes
Функция .IndexOf()
, в отличие от .Contains()
позволяет не только выяснить
факт нахождения символа в строке, но и вычислить индекс первого найденного
символа:
int position = "bzzzt".IndexOf('z'); // вернёт 1
= "bzzzt".IndexOf("zt"); // вернёт 3
position = "bzzzt".IndexOf('!'); // вернёт -1, т.к. символ не найден position
Существуют варианты вызова .IndexOf()
, в которых можно указать индекс, с
которого начинается поиск, а так же максимальное число символов, которые нужно
проверить.
Функция .IndexOfAny()
работает аналогично .IndexOf()
, однако позволяет
обнаружить первое вхождение одного из нескольких указанных символов:
int position = "bzzzt".IndexOfAny({'z', 't'}); // вернёт 1, т.к. 'z' встречается раньше 't'
В отличие от .Trim()
или .Format()
, функция .IndexOfAny()
не принимает
произвольное число аргументов. По этой причине необходимо использовать
инициализатор массива символов, чтобы передать массив в качестве единственного
аргумента.
Функции .LastIndexOf()
и .LastIndexOfAny()
аналогичны рассмотренным выше,
однако ищут последнее, а не первое вхождение заданных символов или строк:
int position = "bzzzt".LastIndexOfAny({'z', 't'}); // вернёт 4, т.к. 't' встречается после 'z'
Извлечение подстрок
Функция .Substring()
позволяет извлекать различные последовательности символов
из исходной строки:
string s1 = "abcd".Substring(2); // вернёт "cd"
string s2 = "abcd".Substring(0, 2); // вернёт "ab"
Вариант .Substring()
, использованный при инициализации s1
, возвращает
подстроку, начинающуюся с указанного индекса и оканчивающуюся так же, как и
оригинальная строка.
Вариант .Substring()
, использованный при инициализации s2
, возвращает
подстроку, начинающуюся с индекса, указанного в первом аргументе, длиной равной
второму аргументу.
Вставка, удаление и замена частей строки
Функция .Insert()
вставляет произвольную строку в исходную по указанному индексу:
string s = "ad".Insert(1, "bc"); // вернёт "abcd"
Функция .Remove()
производит обратную операцию:
string s1 = "abcd".Remove(2); // вернёт "ab"
string s2 = "abcd".Remove(0, 2); // вернёт "cd"
Наконец, функция .Replace()
позволяет заменить некоторый символ или подстроку
другим символом или подстрокой, соответственно:
string s1 = "Жызнь, жыр, жывот".Replace('ы', 'и');
string s2 = "Hello world".Replace("world", "Vasya");
Ленивая обработка строк
Рассмотренные выше операции над строковыми данными, как и большинство операций в C# вообще, используют строгую модель выполнения. Это означает, что непосредственное выполнение какой-либо функции начинается только после того, как будут вычислены её аргументы. Такой подход является естественным для императивных языков программирования, однако в редких случаях он является субоптимальным.
Представим, что программе поступают строковые данные из некоторого внешнего источника, которые необходимо конкатенировать. Длины строк и их количество заранее неизвестны. Очевидным решением будет организовать цикл, на каждой итерации которого будет производиться получение очередной порции данных и их конкатенация:
string итог = "";
while(true)
{
string кусок = ПолучитьОчереднойКусок();
if (кусок == "") break;
= итог + кусок;
итог }
.WriteLine(итог); Console
Как уже было сказано, конкатенация двух строк в C# приводит к созданию новой
строки. Это означает, что выполнении кода итог + кусок
происходит следующее:
- Вычисляется суммарная длина строк
итог
икусок
; - Выделяется новый участок памяти, способный вместить строку вычисленной длины;
- В начало участка копируется старая строка
итог
; - Затем дописывается содержимое строки
кусок
; - Память, содержащая старую строку
итог
освобождается.
Нетрудно заметить, что чем больше итераций сделает цикл, тем медленнее будет
работать программа. Увеличение размера строки итог
будет приводить к тому,
что при конкатенации программе придётся копировать всё больше и больше
информации. Особенно неэффективным этот код становится в случае, если
ПолучитьОчереднойКусок()
возвращает строки из одного символа - ради дописывания
одного символа в конец строки приходится создавать копию всей этой строки.
Решением описанной проблемы является применение ленивых вычислений. На бытовом уровне, ленивыми называются вычисления, которые не производятся сразу, а заносятся в “список дел”, который в последствии можно оптимизировать и, наконец, в действительности выполнить. Применительно к рассматриваемой выше задаче, ленивая конкатенация позволит сперва накопить множество строк, и лишь по окончании цикла выполнить непосредственную конкатенцию. Это значительно сократит количество выделений памяти и операций копирования, что положительно скажется на производительности кода.
Библиотека C# предоставляет специальный тип StringBuilder
для ускорения кода,
производящего много строковых операций. С одной стороны, StringBuilder
производит операцию конкатенации лениво, а с другой - изменения строки,
управляемой StringBuilder
производятся без создания копии. Для использования
StringBuilder
в начале файла с кодом должна располагаться инструкция
using System.Text
. Рассмотрим пример использования этого типа:
= new StringBuilder();
StringBuilder builder
.Append("aaa");
builder.Append("bbb");
builder
.AppendFormat("{0}", 123);
builder
.AppendJoin(", ", "c", "d", "e");
builder
.Insert(0, "???");
builder
.Replace("???", "!!!");
builder
.Remove(3, 3);
builder
string result = builder.ToString(); // вернёт "!!!bbb123c, d, e"
В первой строке создаётся переменная, содержащая StringBuilder
. Внутри этой
переменной хранятся частичные строки и “список дел”, из которого должна быть
собрана финальная строка.
Функция .Append()
реализует конкатенцию, подобно оператору +
, но в ленивом
режиме.
Функция .AppendFormat()
аналогична .Append()
, но способна принимать форматные
строки в качестве аргумента.
Функция .AppendJoin()
сперва производит операцию string.Join()
над своими
параметрами в ленивом режиме, а затем конкатенирует результат с текущим
содержимым StringBuilder
.
Функция .Insert()
реализует вставку произвольных строк в указанное место в
ленивом режиме.
Функция .Replace()
заменяет все вхождения заданной подстроки другой строкой.
Если строки имеют одинаковую длину, то операция происходит сразу, путём
изменения строки, хранимой в StringBuilder
.
Функция .Remove()
удаляет заданное количество символов с заданной позиции в
ленивом режиме.
Наконец, функция .ToString()
выполняет все операции, которые были заложены в
“список дел”, и производит результирующую строку с типом string
.
Познакомившись с типом StringBuilder
, перепишем исходный код программы из
примера выше:
= new StringBuilder();
StringBuilder b while(true)
{
string кусок = ПолучитьОчереднойКусок();
if (кусок == "") break;
.Append(кусок);
b}
string итог = b.ToString();
.WriteLine(итог); Console
Несмотря на преимущество в производительности, StringBuilder
обладает
недостатками, напрямую проистекающими из использования ленивого подхода:
StringBuilder
не имеет функций для поиска отдельных символов, вроде.Contains()
или.IndexOf()
;- Итерация по отдельным символам с помощью
[]
может оказаться очень медленной, еслиStringBuilder
содержит большой “список дел”.
Файловый текстовый ввод/вывод
Подробно работа с файлами и файловой системой в C# будет рассматриваться в следующих лабораторных работах. В этой работе рассмотрим лишь простейшие функции чтения и записи:
string весьФайл = File.ReadAllText(@"C:\Пользователи\User\Рабочий стол\файл.txt");
string[] строкиФайла = File.ReadAllLines(@"C:\Пользователи\User\Рабочий стол\файл.txt");
.WriteAllText(@"C:\Пользователи\User\Рабочий стол\файл.txt", весьФайл); File
Функция File.ReadAllText()
принимает путь до файла и прочитывает его
содержимое целиком в переменную типа string
.
Функция File.ReadAllLines()
прочитывает указанный файл и разбивает его на
отдельные строки, которые возвращаются в виде массива.
Функция File.WriteAllText()
записывает строку, переданную во втором аргументе,
в файл, путь до которого указан в первом аргументе.
Перед применением представленных функций необходимо подключить пространство
имён System.IO
с помощью директивы using
.
Пути в Windows используют символ '\'
в качестве разделителя, поэтому при их
записи в коде удобно использовать @
-нотацию. Иначе, вместо каждого вхождения
'\'
необходимо будет писать "\\"
.
Контрольные вопросы
- Что будет, если число подстановок в форматной строке будет больше, чем число переданных аргументов?
- Что получится, если вызывать
.Split(",")
над строкой, не содержащей символов','
? - Каким образом можно поменять в строке все вхождения двух заданных букв друг на друга?
- Почему в
StringBuilder
нет.Contains()
?
Задание на лабораторную работу
Написать программу, производящую операции над текстовыми входными данными согласно своему варианту. Указанные в варианте операции производятся циклично над каждой отдельной строкой. Если программе не переданы аргументы командной строки, входные данные запрашиваются с клавиатуры. В противном случае первый аргумент командной строки используется как имя файла, из которого входные данные прочитываются построчно.
При обработке ручного ввода:
- Пользователь сначала вводит количество строк, а затем сами строки;
- Допустимо использовать тип данных
string
; - Результат работы программы должен выводиться на экран.
При обработке данных из файла:
- Необходимо применить
StringBuilder
; - Результат работы программы должен выводиться в другой файл, путь до которого указывается во втором аргументе командной строки.
Вариант 1
Разбить каждую строку на отдельные слова. Обрамить каждое слово в рамку из
ASCII-символов ('|'
, '='
и пр.).
Вариант 2
Разбить каждую строку на отдельные слова. В каждом слове перевести символы в верхний регистр, если длина слова - чётное число, либо в нижний регистр, если длина слова - нечётное число.
Вариант 3
В каждой строке заменить отдельно стоящие цифры (т.е. числа от 0 до 9, окруженные пробелами) на их текстовое представление (“ноль”, “один” и т.д.)
Вариант 4
Разбить каждую строку на отдельные слова. В каждом слове переставить символы таким образом, чтобы сперва следовали цифры (если есть), затем - гласные буквы, затем - согласные.
Вариант 5
Разбить каждую строку на отдельные слова. В начало каждого слова приписать его длину.
Вариант 6
Транслитировать каждую строку, т.е. заменить русские буквы английскими (или их сочетаниями, например “ч” -> “ch”).
Вариант 7
Разбить каждую строку на отдельные слова. Обратить порядок слов в строке.
Вариант 8
Разбить каждую строку на отдельные слова. В каждом слове переставить буквы таким образом, чтобы сперва следовали буквы в алфавитном порядке, а затем - цифры (если есть).
Вариант 9
В каждой строке после каждого слова поместить символ '.'
, кроме слов,
разделённых знаками препинания (','
, '-'
, и т.д.). В словах, перед которыми
оказался символ '.'
сделать первую букву заглавной.
Вариант 10
Разбить каждую строку на отдельные слова. В каждой строке оставить только самое длинное слово.
Вариант 11
Разбить каждую строку на отдельные слова. Если два слова расположены рядом друг со другом, и первое слово оканчивается на ту же букву, на которую начинается второе, то конкатенировать эти слова.
Вариант 12
В каждой строке исправить возможные орфографические ошибки (“шы” -> “ши” и т.д.).
Вариант 13
В каждой строке перевести русский текст в дореволюционную орфографию путём добавления “ъ” к словам, оканчивающимся на согласную букву и заменой букв “е” на букву “ять”.
Вариант 14
Разбить каждую строку на отдельные слова. В каждой строке оставить только слова,
содержащие символы '-'
и '_'
.
Вариант 15
Разбить каждую строку на отдельные слова. Слова, содержащие орфографические
ошибки, обрамить символами '!'
.
