суббота, 26 апреля 2014 г.

Так ли прост строковый оператор +

Введение


Строковый тип данных является одним из фундаментальных типов, наряду с числовыми (int, long, double) и логическим (bool). Тяжело себе представить хоть, сколько либо полезную программу, не использующую данный тип.

На платформе .NET строковый тип представлен в виде неизменяемого класса String. Кроме того, он является сильно интегрированным в общеязыковую среду CLR, а так же имеет поддержку со стороны компилятора языка C#.

В этой статье я бы хотел поговорить о конкатенации, операции, которая выполняется над строками так же часто, как операция сложения над числами. Казалось бы, о чем тут можно говорить, ведь все мы знаем о строковом операторе +, но как оказалось, есть у него свои тонкости.


Спецификация языка о строковом операторе +


Спецификация языка C# предоставляет три перегрузки оператора + для строк:
string operator + (string x, string y)
string operator + (string x, object y)
string operator + (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)
public static String Concat(String str0, String str1, String str2)
public static String Concat(String str0, String str1, String str2, String str3)
public static String Concat(params String[] values)
public static String Concat(IEnumerable<String> values)

public static String Concat(Object arg0)
public static String Concat(Object arg0, Object arg1)
public static String Concat(Object arg0, Object arg1, Object arg2)
public static String Concat(Object arg0, Object arg1, Object arg2, Object arg3, __arglist)

public static 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.Concat VS StringBuilder.Append


Следует сказать пару слов и об этом сравнении. Рассмотрим следующий код:
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-a который ничего не знает о длине результирующей строки.

Реализация метода Concat для 3 строк:
public static string Concat(string str0, string str1, string str2)
 {
   if (str0 == null && str1 == null && str2 == null)
     return string.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, интересно все же знать, как дела обстоят там. Компилятор языка Java оптимизирует оператор +, так что он использует класс StringBuilder и вызов метода append.

Предыдущий код преобразуется в
String fio = new StringBuilder(String.valueOf(surname)).append(name).append(patronymic).ToString()
Стоит так же сказать, что от такой оптимизации в C# отказались намеренно, у Эрика Липперта есть пост на эту тему. Дело в том, что такая оптимизация не является оптимизацией как таковой, она является переписыванием кода. Плюс к этому создатели языка C# считают, что разработчики должны знать особенности работы с классом String и в случае необходимости перейдут на использование StringBuilder.

Кстати именно Эрик Липперт занимался оптимизациями компилятора C#, связанными с конкатенацией строк.

Заключение


Возможно, на первый взгляд может показаться странным, что класс String не определяет оператор +, пока мы не подумаем о возможностях оптимизации компилятора, связанных с видимостью большего фрагмента кода. Например, если бы оператор + был определен в классе String, то выражение s = a + b + c + d приводило бы к созданию двух промежуточных строк, единственный вызов метода string.Concat(a, b, c, d) позволяет выполнить объединение более эффективно.

Комментариев нет:

Отправить комментарий