Сообщений 0 Оценка 275 Оценить |
Строковый тип данных является одним из фундаментальных типов, наряду с числовыми (int, long, double) и логическим (bool). Тяжело себе представить хоть сколько-либо полезную программу, не использующую данный тип.
На платформе .NET строковый тип представлен в виде неизменяемого класса String. Кроме того, он является сильно интегрированным в общеязыковую среду CLR, а также имеет поддержку со стороны компилятора языка C#.
В этой статье я бы хотел поговорить о конкатенации, операции, которая выполняется над строками так же часто, как операция сложения над числами. Казалось бы, о чем тут можно говорить, ведь все мы знаем о строковом операторе "+", но, как оказалось, есть у него свои тонкости.
Спецификация языка C# предоставляет три перегрузки оператора "+" для строк:
string operator + (string x, string y) stringoperator + (string x, object y) stringoperator + (object x, string y) |
Если один из операндов объединения строк есть null, то подставляется пустая строка. Иначе любой аргумент, не являющийся строкой, приводится к представлению в виде строки с помощью вызова виртуального метода ToString. Если метод ToString возвращает null, подставляется пустая строка. Следует сказать, что, согласно спецификации, данная операция никогда не должна возвращать значение null.
Описание оператора выглядит достаточно понятно, однако если мы посмотрим на реализацию класса String, то найдем явное определение лишь двух операторов: == и !=. Возникает резонный вопрос: что происходит за кулисами конкатенации строк? Каким образом компилятор обрабатывает строковый оператор "+"?
Ответ на этот вопрос оказался не таким уж сложным. Необходимо присмотреться повнимательнее к статическому методу String.Concat. Метод String.Concat объединяет один или несколько экземпляров класса String или представления в виде String значений одного или нескольких экземпляров Object. Имеются следующие перегрузки данного метода:
public static String Concat(String str0, String str1) publicstatic String Concat(String str0, String str1, String str2) publicstatic String Concat(String str0, String str1, String str2, String str3) publicstatic String Concat(params String[] values) publicstatic String Concat(IEnumerable<String> values) publicstatic String Concat(Object arg0) publicstatic String Concat(Object arg0, Object arg1) publicstatic String Concat(Object arg0, Object arg1, Object arg2) publicstatic String Concat(Object arg0, Object arg1, Object arg2, Object arg3, __arglist) publicstatic String Concat<T>(IEnumerable<T> values) |
Пусть у нас есть выражение s = a + b, где a и b – строки
Компилятор преобразует его в вызов статического метода Concat, то есть в
s = string.Concat(a, b) |
Операция конкатенации строк, как и любая другая операция сложения, в языке C# является лево-ассоциативной.
С двумя строками все понятно, но что если строк больше?
Выражение
s = a + b + c |
учитывая лево-ассоциативность операции могло бы быть заменено на
s = string.Concat(string.Concat(a, b), c) |
Однако, учитывая наличие перегрузки, принимающей три аргумента, оно будет преобразовано в
s = string.Concat(a, b, c). |
Аналогично дела обстоят с конкатенацией четырех строк. Для конкатенации 5 и более строк имеем перегрузку string.Concat(params string[]), так что необходимо учитывать накладные расходы, связанные с выделением памяти под массив.
Следует так же сказать, что операция конкатенации строк является полностью ассоциативной: не имеет никакого значения, в каком порядке мы конкатенируем строки, поэтому выражение
s = a + (b + c) |
несмотря на явное указание приоритета выполнения конкатенации, обрабатывается как
s = (a + b) + c = string.Concat(a, b, c) |
вместо ожидаемого
s = string.Concat(a, string.Concat(b, c)). |
Таким образом, подытоживая сказанное выше: операция конкатенации строк всегда представляется слева направо, и использует вызов статического метода String.Concat.
Компилятор языка C# содержит оптимизации, связанные с литеральными строками. Так, например, выражение s = "a" + "b" + c, учитывая лево-ассоциативность оператора "+", эквивалентно s = ("a" + "b") + c и преобразуется в s = string.Concat("ab", c).
Выражение s = c + "a" + "b", несмотря на лево-ассоциативность операции конкатенации (s = (c + "a") + "b"), преобразуется в s = string.Concat(c, "ab").
В общем, неважно, в каком месте находятся литералы, компилятор конкатенирует всё, что может, а уже потом пытается выбрать соответствующую перегрузку метода Concat. Выражение s = a + "b" + "c" + d преобразуется в s = string.Concat(a, "bc", d).
Следует также сказать об оптимизациях, связанных с пустой и null-строкой. Компилятор знает, что добавление пустой строки не влияет на результат конкатенации, поэтому выражение
s = a + "" + b |
преобразуется в
s = string.Concat(a, b) |
вместо ожидаемого
s = string.Concat (a, "", b). |
Аналогично для const-строки, значение которой есть null, имеем:
const string nullStr = null; s = a + nullStr + b; |
преобразуется в s = string.Concat(a, b).
Выражение
s = a + nullStr |
преобразуется в s = a ?? "", если a - строка, и вызов метода string.Concat(a), если a – не строка, например, s = 17 + nullStr, преобразуется в s = string.Concat (17).
Интересная особенность связана с оптимизацией обработки литералов и лево-ассоциативностью строкового оператора +. Рассмотрим выражение
var s1 = 17 + 17 + "abc"; |
Учитывая лево-ассоциативность, оно эквивалентно:
var s1 = (17 + 17) + "abc"; // вызов метода string.Concat(34, "abc") |
в результате чего на этапе компиляции произойдет сложение чисел, так что результатом будет 34abc.
С другой стороны, выражение
var s2 = "abc" + 17 + 17; |
эквивалентно
var s2 = ("abc" + 17) + 17; // вызов метода string.Concat("abc", 17, 17) |
в результате чего получим abc1717.
Вот так казалось бы одинаковые операцим конкатенации приводят к разным результатам.
Следует сказать пару слов и об этом сравнении. Рассмотрим следующий код:
string name = "Timur"; string surname = "Guev"; string patronymic = "Ahsarbecovich"; string fio = surname + name + patronymic; |
Его можно заменить на следующий код, используя StringBuilder:
var sb = new StringBuilder(); sb.Append(surname); sb.Append(name); sb.Append(patronymic); string fio = sb.ToString(); |
Но едва ли мы получим в данной ситуации преимущества от использования StringBuilder. Помимо того, что код стал менее читаемым, он стал еще и менее эффективным, поскольку реализация метода Concat вычисляет длину результирующей строки и выделяет память только один раз, в отличие от StringBuilder, который ничего не знает о длине результирующей строки.
Реализация метода Concat для 3 строк:
public static string Concat(string str0, string str1, string str2) { if (str0 == null && str1 == null && str2 == null) returnstring.Empty; if (str0 == null) str0 = string.Empty; if (str1 == null) str1 = string.Empty; if (str2 == null) str2 = string.Empty; string dest = string.FastAllocateString(str0.Length + str1.Length + str2.Length); string.FillStringChecked(dest, 0, str0); string.FillStringChecked(dest, str0.Length, str1); string.FillStringChecked(dest, str0.Length + str1.Length, str2); return dest; } |
Скажу пару слов о строковом операторе + в Java. Хотя я и не программирую на Java, интересно все же знать, как дела обстоят там. Компилятор языка Java оптимизирует оператор "+", так что он использует класс StringBuilder и вызов метода append.
Предыдущий код преобразуется в
String fio = new StringBuilder(String.valueOf(surname)).append(name).append(patronymic).ToString() |
Стоит также сказать, что от такой оптимизации в C# отказались намеренно, у Эрика Липперта есть пост на эту тему. Дело в том, что такая оптимизация не является оптимизацией как таковой, она является переписыванием кода. Вдобавок к этому создатели языка C# считают, что разработчики должны знать особенности конкатенации, и в случае необходимости перейдут на использование StringBuilder.
Кстати, именно Эрик Липперт занимался оптимизациями компилятора C#, связанными с конкатенацией строк.
На первый взгляд может показаться странным, что класс String не определяет оператор "+" – пока мы не подумаем о возможностях оптимизации компилятора, связанных с видимостью большего фрагмента кода. Например, если бы оператор "+" был определен в классе String, то выражение s = a + b + c + d приводило бы к созданию двух промежуточных строк. Единственный вызов метода string.Concat (a, b, c, d) позволяет выполнить объединение более эффективно.
Сообщений 0 Оценка 275 Оценить |