вторник, 10 мая 2016 г.

Конкатенация строк в .NET

Строки

Ходит распространённое заблуждение о том, что конкатенация строк в .NET с использованием оператора + менее предпочтительна, чем использование класса StringBuilder. Обычно это аргументируется тем, что класс string является immutable-классом и конкатенация строк влечёт создание нового экземпляра с выделением памяти, копированием и прочими свистоплясками. StringBuilder же в свою очередь выделяет память с лихвой изначально (свойство Capacity), при необходимости выделяя память под новый массив большего размера и копируя себя туда. В общем случае всё так, "но есть ньюансы".
Однако, давайте разберёмся так ли прост оператор конкатенации?

String.Concat

Начнём с того, что если вы посмотрите исходники FCL, то не найдёте оператора + в классе String. Строго говоря, из операторов там определены всего два: == и !=. Функционал оператора + обеспечивает сам механизм CLR, заменяя его на вызов статического метода String.Concat (за исключением, когда оба аргумента являются строковыми литералами, тогда конкатенация происходит при компиляции).
Т.е., например, все три определённых оператора + в спецификации языка C#:

string operator + (string x, string y)
string operator + (string x, object y)
string operator + (object x, string y)

 реально разворачиваются в вызов одного из перегруженных методов Concat:

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)

Обилие перегрузок в данном случае и несёт основную ценность. Если опять же заглянуть в реализацию, то мы увидим следующий код:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
        public static String Concat(String str0, String str1, String str2) {
            Contract.Ensures(Contract.Result<String>() != null);
            Contract.Ensures(Contract.Result<String>().Length ==
                (str0 == null ? 0 : str0.Length) +
                (str1 == null ? 0 : str1.Length) +
                (str2 == null ? 0 : str2.Length));
            Contract.EndContractBlock();
 
            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;
            }
 
            int totalLength = str0.Length + str1.Length + str2.Length;
 
            String result = FastAllocateString(totalLength);
            FillStringChecked(result, 0, str0);
            FillStringChecked(result, str0.Length, str1);
            FillStringChecked(result, str0.Length + str1.Length, str2);
 
            return result;
        }

Самой интересной в данном случае является 25ая строка (выделил отдельно в коде). В ней происходит вычисление длины финальной строки, получаемой после сложения сразу всех аргументов. Это значит, что выражение a + b + c + d + e + f повлечёт за собой лишь одно выделение памяти, а не 5, как можно было подумать. Основным (и единственным) преимуществом метода Concat является, то, что CLR знает про контекст выполнения "чуть" больше, чем метод StringBuilder.Append и способен самостоятельно выбрать подходящую перегрузку метода Concat, который обработает все аргументы разом.

StringBuilder.Append

В отличие от String.Concat, вызов данного метода всегда производится пользователем явно. Поэтому цепочка вызовов:

1
2
3
4
5
var sb = new StringBuilder();
sb.Append(a);
sb.Append(b);
sb.Append(c);
string result = sb.ToString();

теоретически может привести к многократному выделению памяти (зависит от capcity и размеров конкретных строк a, b и c).

Заключение

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

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

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