Когда лучше использовать String.Format против конкатенации строк? - PullRequest
108 голосов
/ 18 ноября 2008

У меня есть небольшой фрагмент кода, который анализирует значение индекса для определения ввода ячейки в Excel. Это заставило меня задуматься ...

В чем разница между

xlsSheet.Write("C" + rowIndex.ToString(), null, title);

и

xlsSheet.Write(string.Format("C{0}", rowIndex), null, title);

Один "лучше", чем другой? И почему?

Ответы [ 14 ]

152 голосов
/ 18 ноября 2008

Мое начальное предпочтение (исходящее из фона C ++) было для String.Format. Я отказался от этого позже по следующим причинам:

  • Конкатенация строк, возможно, "безопаснее". Это случилось со мной (и я видел, как это случилось с несколькими другими разработчиками), чтобы удалить параметр или испортить порядок параметров по ошибке. Компилятор не будет проверять параметры по строке формата, и в результате вы получите ошибку во время выполнения (то есть, если вам повезет, что ее нет в непонятном методе, таком как регистрация ошибки). При объединении удаление параметра менее подвержено ошибкам. Можно утверждать, что вероятность ошибки очень мала, но может случиться.

- Конкатенация строк допускает нулевые значения, String.Format - нет. Запись "s1 + null + s2" не прерывается, она просто обрабатывает нулевое значение как String.Empty. Ну, это может зависеть от вашего конкретного сценария - бывают случаи, когда вы хотите получить сообщение об ошибке, а не игнорировать пустое имя FirstName. Однако даже в этой ситуации я лично предпочитаю самому проверять наличие нулей и выдавать конкретные ошибки вместо стандартного ArgumentNullException, получаемого из String.Format.

  • Конкатенация строк работает лучше. Некоторые из постов выше уже упоминают об этом (без объяснения причин, по которым я решил написать этот пост :).

Идея. Компилятор .NET достаточно умен, чтобы преобразовать этот кусок кода:

public static string Test(string s1, int i2, int i3, int i4, 
        string s5, string s6, float f7, float f8)
{
    return s1 + " " + i2 + i3 + i4 + " ddd " + s5 + s6 + f7 + f8;
}

на это:

public static string Test(string s1, int i2, int i3, int i4,
            string s5, string s6, float f7, float f8)
{
    return string.Concat(new object[] { s1, " ", i2, i3, i4, 
                    " ddd ", s5, s6, f7, f8 });
}

Что происходит под капотом String.Concat легко угадать (используйте Reflector). Объекты в массиве преобразуются в их строку с помощью ToString (). Затем вычисляется общая длина и выделяется только одна строка (с общей длиной). Наконец, каждая строка копируется в результирующую строку через wstrcpy в некотором небезопасном фрагменте кода.

Причины String.Concat намного быстрее? Что ж, мы все можем посмотреть, что делает String.Format - вы будете удивлены количеством кода, необходимого для обработки строки формата. Вдобавок к этому (я видел комментарии относительно потребления памяти), String.Format использует StringBuilder внутри. Вот как это сделать:

StringBuilder builder = new StringBuilder(format.Length + (args.Length * 8));

Таким образом, для каждого переданного аргумента он резервирует 8 символов. Если аргумент представляет собой однозначное значение, то это очень плохо, у нас есть немного потерянного пространства. Если аргумент является пользовательским объектом, возвращающим некоторый длинный текст в ToString(), то может потребоваться даже некоторое перераспределение (конечно, в худшем случае).

По сравнению с этим конкатенация тратит впустую только пространство массива объектов (не слишком много, учитывая, что это массив ссылок). Нет синтаксического анализа спецификаторов формата и промежуточного StringBuilder. Накладные / распаковывающие накладные расходы присутствуют в обоих методах.

Единственная причина, по которой я бы выбрал String.Format, - это когда задействована локализация. Размещение строк форматирования в ресурсах позволяет поддерживать разные языки, не вмешиваясь в код (подумайте о сценариях, в которых форматированные значения меняют порядок в зависимости от языка, то есть «после {0} часов и {1} минут» на японском языке может выглядеть совершенно по-разному): ).


Подводя итог моему первому (и довольно длинному) сообщению:

  • лучший способ (с точки зрения производительности и удобства обслуживания / читаемости) для меня - это использование конкатенации строк без каких-либо ToString() вызовов
  • если вы после спектакля, сделайте ToString() звонки, чтобы избежать бокса (я несколько склонен к читабельности) - так же, как первый вариант в вашем вопросе
  • если вы показываете локализованные строки пользователю (здесь это не так), String.Format() имеет преимущество.
105 голосов
/ 18 ноября 2008

До C # 6

Если честно, я думаю, что первая версия проще - хотя я бы упростил ее до:

xlsSheet.Write("C" + rowIndex, null, title);

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

Строки форматирования отлично подходят для локализации и т. Д., Но в таком случае конкатенация проще и работает так же хорошо.

С C # 6

Строковая интерполяция упрощает чтение многих вещей в C # 6. В этом случае ваш второй код становится:

xlsSheet.Write($"C{rowIndex}", null, title);

что, наверное, лучший вариант, ИМО.

5 голосов
/ 18 ноября 2008

Для простого случая, когда это простая одиночная конкатенация, я чувствую, что она не стоит сложности string.Format (и я не проверял, но подозреваю, что для простого случая, подобного этому, string.Format может быть немного медленнее, чем при разборе строки формата и все). Как и Джон Скит, я предпочитаю не вызывать явно .ToString(), поскольку это будет сделано неявно из-за перегрузки string.Concat(string, object), и я думаю, что код выглядит чище и его легче читать без него.

Но для более чем нескольких конкатенаций (сколько субъективно) я определенно предпочитаю string.Format. В какой-то момент я думаю, что и читаемость, и производительность неоправданно страдают от конкатенации.

Если в строке формата есть много параметров (опять же, «много» субъективно), я обычно предпочитаю включать закомментированные индексы в аргументы замены, чтобы не потерять отслеживание того, какое значение идет к какому параметру. Придуманный пример:

Console.WriteLine(
    "Dear {0} {1},\n\n" +

    "Our records indicate that your {2}, \"{3}\", is due for {4} {5} shots.\n" +
    "Please call our office at 1-900-382-5633 to make an appointment.\n\n" +

    "Thank you,\n" +
    "Eastern Veterinary",

    /*0*/client.Title,
    /*1*/client.LastName,
    /*2*/client.Pet.Animal,
    /*3*/client.Pet.Name,
    /*4*/client.Pet.Gender == Gender.Male ? "his" : "her",
    /*5*/client.Pet.Schedule[0]
);

Обновление

Мне приходит в голову, что приведенный мною пример немного сбивает с толку, поскольку кажется, что я использовал и конкатенацию, и string.Format здесь. И да, логически и лексически, это то, что я сделал. Но все объединения будут оптимизированы компилятором 1 , поскольку все они являются строковыми литералами. Таким образом, во время выполнения будет одна строка. Поэтому я должен сказать, что предпочитаю избегать многих конкатенаций во время выполнения .

Конечно, большая часть этой темы сейчас устарела, если только вы не застряли на C # 5 или старше. Теперь у нас есть интерполированные строки , которые по читаемости намного превосходят string.Format почти во всех случаях. В наши дни, если только я не конкатенирую значение непосредственно в начало или конец строкового литерала, я почти всегда использую интерполяцию строк. Сегодня я напишу свой предыдущий пример так:

Console.WriteLine(
    $"Dear {client.Title} {client.LastName},\n\n" +

    $"Our records indicate that your {client.Pet.Animal}, \"{client.Pet.Name}\", " +
    $"is due for {(client.Pet.Gender == Gender.Male ? "his" : "her")} " +
    $"{client.Pet.Schedule[0]} shots.\n" +
    "Please call our office at 1-900-382-5633 to make an appointment.\n\n" +

    "Thank you,\n" +
    "Eastern Veterinary"
);

Таким образом вы теряете конкатенацию во время компиляции. Каждая интерполированная строка преобразуется компилятором в вызов string.Format, и их результаты объединяются во время выполнения. Это означает, что это жертва производительности во время выполнения для удобства чтения. В большинстве случаев это оправданная жертва, потому что штраф за время выполнения ничтожно мал. Однако в коде, критичном к производительности, может потребоваться профилирование различных решений.


1 Вы можете увидеть это в спецификации C # :

... в константных выражениях допускаются следующие конструкции:

...

  • Предопределенный бинарный оператор + ... ...

Вы также можете проверить это с помощью небольшого кода:

const string s =
    "This compiles successfully, " +
    "and you can see that it will " +
    "all be one string (named `s`) " +
    "at run time";
5 голосов
/ 18 ноября 2008

Я думаю, что первый вариант более читабелен, и это должно быть вашей главной задачей.

xlsSheet.Write("C" + rowIndex.ToString(), null, title);

string. Формат использует StringBuilder под капотом (проверьте с помощью отражатель ), поэтому он не принесет никакого выигрыша в производительности, если вы не выполните значительное количество операций объединения. Это будет медленнее для вашего сценария, но реальность такова, что это решение по оптимизации производительности микропроцессора в большинстве случаев неуместно, и вам действительно следует сосредоточиться на удобочитаемости кода, если вы не зациклены.

В любом случае, сначала пишите для удобства чтения, а затем используйте профилировщик производительности , чтобы определить ваши горячие точки, если вы действительно считаете, что у вас есть проблемы с производительностью.

3 голосов
/ 18 ноября 2008

Я взглянул на String.Format (используя Reflector), и он фактически создает StringBuilder, а затем вызывает AppendFormat для него. Так что это быстрее, чем concat для нескольких перемешиваний. Самым быстрым (я полагаю) будет создание StringBuilder и выполнение вызовов Append вручную. Конечно, число «много» можно угадать. Я бы использовал + (на самом деле & потому что я в основном программист на VB) для чего-то столь же простого, как ваш пример. Поскольку это становится более сложным, я использую String.Format. Если есть много переменных, то я бы пошел на StringBuilder и Append, например, у нас есть код, который создает код, там я использую одну строку фактического кода для вывода одной строки сгенерированного кода.

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

"C" + rowIndex.ToString();

"C" - это уже строка.
rowIndex.ToString () создает другую строку. (@manohard - бокс rowIndex не будет)
Затем мы получаем окончательную строку.
Если мы возьмем пример

String.Format("C(0)",rowIndex);

тогда мы имеем "C {0}" в виде строки
rowIndex получает коробку для передачи в функцию
Создан новый stringbuilder
AppendFormat вызывается в строителе строк - я не знаю подробностей о том, как функции AppendFormat функционируют, но давайте предположим, что он очень эффективен, ему все равно придется преобразовать упакованный rowIndex в строку. Затем преобразуйте строку в новую строку.
Я знаю, что StringBuilders пытаются предотвратить бессмысленное копирование памяти, но String.Format по-прежнему приводит к дополнительным издержкам по сравнению с простой конкатенацией.

Если мы возьмем пример с еще несколькими строками

"a" + rowIndex.ToString() + "b" + colIndex.ToString() + "c" + zIndex.ToString();

у нас есть 6 строк для начала, которые будут одинаковыми для всех случаев.
Используя конкатенацию, мы также имеем 4 промежуточные строки плюс конечный результат. Это те промежуточные результаты, которые устраняются с помощью String, Format (или StringBuilder).
Помните, что для создания каждой промежуточной строки предыдущая должна быть скопирована в новую область памяти, а не только выделение памяти, которая может быть медленной.

3 голосов
/ 18 ноября 2008

Если бы ваша строка была более сложной с объединением многих переменных, я бы выбрал строку .Format (). Но для размера строки и количества конкатенируемых переменных в вашем случае, я бы пошел с вашей первой версией, это больше spartan .

2 голосов
/ 18 ноября 2008

Этот пример, вероятно, слишком тривиален, чтобы заметить разницу. На самом деле, я думаю, что в большинстве случаев компилятор может оптимизировать любую разницу.

Однако, если бы мне пришлось угадывать, я бы дал string.Format() преимущество для более сложных сценариев. Но это скорее интуитивное чувство, что лучше использовать буфер, вместо того, чтобы создавать несколько неизменяемых строк и не основываться на реальных данных.

1 голос
/ 01 мая 2015

У меня сложилось впечатление, что string.format был быстрее, в этом тесте он кажется в 3 раза медленнее

string concat = "";
        System.Diagnostics.Stopwatch sw1 = new System.Diagnostics.Stopwatch    ();
        sw1.Start();
        for (int i = 0; i < 10000000; i++)
        {
            concat = string.Format("{0}{1}{2}{3}{4}{5}{6}{7}{8}{9}{10}","1", "2" , "3" , "4" , "5" , "6" , "7" , "8" , "9" , "10" , i);
        }
        sw1.Stop();
        Response.Write("format: "  + sw1.ElapsedMilliseconds.ToString());
        System.Diagnostics.Stopwatch sw2 = new System.Diagnostics.Stopwatch();
        sw2.Start();
        for (int i = 0; i < 10000000; i++)
        {
            concat = "1" + "2" + "3" + "4" + "5" + "6" + "7" + "8" + "9" + "10" + i;
        }
        sw2.Stop();

string.format занял 4,6 с, а при использовании «+» - 1,6 с.

1 голос
/ 21 января 2014

Я согласен со многими пунктами выше, еще один момент, который, я считаю, должен быть упомянут, - это возможность сопровождения кода. string.Format облегчает изменение кода.

т.е. У меня есть сообщение "The user is not authorized for location " + location или "The User is not authorized for location {0}"

если я когда-либо хотел изменить сообщение, чтобы сказать: location + " does not allow this User Access" или "{0} does not allow this User Access"

со строкой. Все, что мне нужно сделать, это изменить строку. для объединения я должен изменить это сообщение

при использовании в нескольких местах может сэкономить время.

1 голос
/ 18 ноября 2008

Мне нравится String.Format, потому что он может сделать ваш отформатированный текст намного проще для просмотра и чтения, чем встроенная конкатенация, а также гораздо более гибким, позволяя вам форматировать ваши параметры, однако для кратких вариантов использования, подобных вашему, я не вижу проблем с конкатенацией.

Для конкатенаций внутри циклов или в больших строках всегда следует использовать класс StringBuilder.

...