Сколько объектов String будет создано при использовании знака плюс?

115

Сколько объектов String будет создано при использовании знака плюса в приведенном ниже коде?

String result = "1" + "2" + "3" + "4";

Если бы это было так, как показано ниже, я бы сказал три объекта String: «1», «2», «12».

String result = "1" + "2";

Я также знаю, что объекты String кэшируются в пуле / таблице String Intern для повышения производительности, но вопрос не в этом.

Свет
источник
Строки интернируются только в том случае, если вы явно вызываете String.Intern.
Джо Уайт
7
@JoeWhite: они?
Игорь Корхов 03
13
Не совсем. Все строковые литералы автоматически интернируются. Результаты строковых операций - нет.
Стефан Пол Ноак
Более того, в примере OP есть только одна строковая константа, и она интернирована. Я обновлю свой ответ, чтобы проиллюстрировать.
Крис Шейн
+1. В качестве реального примера необходимости кодирования цепочки строк в этом стиле в разделе «Примеры» на msdn.microsoft.com/en-us/library/… есть такой, который был бы невозможен, если бы компилятор не смог его оптимизировать. к одной константе из-за ограничений на значения, присваиваемые параметрам атрибутов.
ClickRick

Ответы:

161

Удивительно, но это зависит от обстоятельств.

Если вы сделаете это методом:

void Foo() {
    String one = "1";
    String two = "2";
    String result = one + two + "34";
    Console.Out.WriteLine(result);
}

то компилятор, кажется, испускает код, используя, String.Concatкак ответил @Joachim (+1 к нему, кстати).

Если вы определяете их как константы , например:

const String one = "1";
const String two = "2";
const String result = one + two + "34";

или как литералы , как в исходном вопросе:

String result = "1" + "2" + "3" + "4";

тогда компилятор устранит эти +признаки. Это эквивалентно:

const String result = "1234";

Кроме того, компилятор удалит посторонние константные выражения и выдаст их только в том случае, если они используются или открываются. Например, эта программа:

const String one = "1";
const String two = "1";
const String result = one + two + "34";

public static void main(string[] args) {
    Console.Out.WriteLine(result);
}

Генерирует только одну строку - константу result(равную «1234»). oneи twoне отображаются в результирующем IL.

Имейте в виду, что во время выполнения могут быть дальнейшие оптимизации. Я просто ориентируюсь на то, что производится ИЛ.

Наконец, что касается интернирования, константы и литералы интернируются, но интернированное значение является результирующим постоянным значением в IL, а не литералом. Это означает, что вы можете получить даже меньше строковых объектов, чем ожидаете, поскольку несколько идентично определенных констант или литералов фактически будут одним и тем же объектом! Об этом свидетельствует следующее:

public class Program
{
    private const String one = "1";
    private const String two = "2";
    private const String RESULT = one + two + "34";

    static String MakeIt()
    {
        return "1" + "2" + "3" + "4";
    }   

    static void Main(string[] args)
    {
        string result = "1" + "2" + "34";

        // Prints "True"
        Console.Out.WriteLine(Object.ReferenceEquals(result, MakeIt()));

        // Prints "True" also
        Console.Out.WriteLine(Object.ReferenceEquals(result, RESULT));
        Console.ReadKey();
    }
}

В случае, когда строки объединяются в цикл (или иным образом динамически), вы получаете одну дополнительную строку на объединение. Например, следующее создает 12 экземпляров строки: 2 константы + 10 итераций, каждая из которых приводит к новому экземпляру String:

public class Program
{
    static void Main(string[] args)
    {
        string result = "";
        for (int i = 0; i < 10; i++)
            result += "a";
        Console.ReadKey();
    }
}

Но (что также удивительно), несколько последовательных конкатенаций объединяются компилятором в одну многострочную конкатенацию. Например, эта программа также производит только 12 экземпляров строк! Это потому, что « Даже если вы используете несколько операторов + в одном операторе, содержимое строки копируется только один раз ».

public class Program
{
    static void Main(string[] args)
    {
        string result = "";
        for (int i = 0; i < 10; i++)
            result += "a" + result;
        Console.ReadKey();
    }
}
Крис Шейн
источник
как насчет String result = "1" + "2" + три + четыре; где два и три объявлены как строка three = "3"; String four = "4" ;?
The Light
Даже в результате получается одна строка. Я просто запустил его через LinqPad, чтобы дважды проверить себя.
Крис Шейн,
1
@Servy - Комментарий был обновлен. Когда вы меняете комментарий, он не помечается как изменяемый.
Security Hound
1
Один случай, который было бы неплохо рассмотреть для полноты картины, - это объединение в цикл. Например, сколько строковых объектов выделяет следующий код:string s = ""; for (int i = 0; i < n; i++) s += "a";
Joren
1
Я использую LINQPad ( linqpad.net ) или отражателя ( reflector.net ). Первый показывает вам IL произвольных фрагментов кода, второй декомпилирует сборки в IL и может повторно сгенерировать эквивалентный C # из этого IL. Также имеется встроенный инструмент под названием ILDASM ( msdn.microsoft.com/en-us/library/f7dy01k1(v=vs.80).aspx ). Понимание IL - непростая вещь
Крис Шейн
85

Крис Шейн ответил очень хорошо. Как человек, который написал оптимизатор конкатенации строк, я бы просто добавил два дополнительных интересных момента.

Во-первых, оптимизатор конкатенации по существу игнорирует и скобки, и левую ассоциативность, когда это можно сделать безопасно. Предположим, у вас есть метод M (), который возвращает строку. Если вы скажете:

string s = M() + "A" + "B";

то компилятор считает, что оператор сложения является левоассоциативным, и поэтому это то же самое, что:

string s = ((M() + "A") + "B");

Но это:

string s = "C" + "D" + M();

такой же как

string s = (("C" + "D") + M());

так что это конкатенация постоянной строки "CD" с M().

Фактически, оптимизатор конкатенации понимает, что конкатенация строк является ассоциативной , и генерирует String.Concat(M(), "AB")для первого примера, даже если это нарушает левую ассоциативность.

Вы даже можете сделать это:

string s = (M() + "E") + ("F" + M()));

и мы по-прежнему будем генерировать String.Concat(M(), "EF", M()).

Второй интересный момент заключается в том, что нулевые и пустые строки оптимизируются. Итак, если вы сделаете это:

string s = (M() + "") + (null + M());

ты получишь String.Concat(M(), M())

Тогда возникает интересный вопрос: а что насчет этого?

string s = M() + null;

Мы не можем оптимизировать это до

string s = M();

потому что M()может вернуть null, но String.Concat(M(), null)вернет пустую строку, если M()вернет null. Вместо этого мы сокращаем

string s = M() + null;

в

string s = M() ?? "";

Тем самым демонстрируя, что конкатенация строк вообще не требует вызова String.Concat.

Для дальнейшего чтения по этому вопросу см.

Почему String.Concat не оптимизирован для StringBuilder.Append?

Эрик Липперт
источник
Думаю, тут могла проскочить пара ошибок. Конечно, ("C" + "D") + M())порождает String.Concat("CD", M()), а не String.Concat(M(), "AB"). И ниже (M() + "E") + (null + M())должно генерироваться String.Concat(M(), "E", M()), а не String.Concat(M(), M()).
hammar 03
21
+1 за начальный абзац. :) Подобные ответы всегда поражают меня в Stack Overflow.
brichins 07
23

Я нашел ответ в MSDN. Один.

Как: объединить несколько строк (Руководство по программированию на C #)

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

Дэвид
источник
22

Только один. Компилятор C # сворачивает строковые константы и, следовательно, по существу компилируется до

String result = "1234";
JaredPar
источник
Я думал, что всякий раз, когда вы используете "", он создает объект String.
Свет,
1
@ Уильям в целом да. Но постоянное сворачивание удалит ненужные промежуточные шаги
JaredPar 03
13

Я сомневаюсь, что это предусмотрено каким-либо стандартом или спецификацией. Одна версия, вероятно, может делать что-то отличное от другой.

Жалкая переменная
источник
3
Это задокументированное поведение по крайней мере для компилятора Microsoft C # для VS 2008 и 2010 (см. Ответ @ David-Stratton). Тем не менее, вы правы - насколько я могу судить при быстром прочтении, в спецификации C # это не указано, и, вероятно, это следует рассматривать как деталь реализации.
Крис Шейн,
13

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

Если бы они были динамическими, они были бы оптимизированы для одного вызова String.Concat (строка, строка, строка, строка) .

Иоахим Исакссон
источник