Сообщений 0    Оценка 275        Оценить  
Система Orphus

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

Автор: Гуев Тимур Ахсарбекович
Опубликовано: 18.11.2015
Исправлено: 10.12.2016
Версия текста: 1.0
Введение
Спецификация языка о строковом операторе +
Подробнее
Оптимизации компилятора для литеральных строк
String.Concat VS StringBuilder.Append
Оператор + в Java
Заключение
Список литературы

Введение

Строковый тип данных является одним из фундаментальных типов, наряду с числовыми (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.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, который ничего не знает о длине результирующей строки.

Реализация метода 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, интересно все же знать, как дела обстоят там. Компилятор языка 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) позволяет выполнить объединение более эффективно.

Список литературы

  1. Первый элемент (элементы должны оформляться стилями LiteratureListOL или LiteratureListUL)


Любой из материалов, опубликованных на этом сервере, не может быть воспроизведен в какой бы то ни было форме и какими бы то ни было средствами без письменного разрешения владельцев авторских прав.
    Сообщений 0    Оценка 275        Оценить