Когда лучше использовать String.Format против конкатенации строк?
У меня есть небольшой фрагмент кода, который анализирует значение индекса, чтобы определить входную ячейку в Excel. Это заставило меня задуматься...
В чем разница между
xlsSheet.Write("C" + rowIndex.ToString(), null, title);
а также
xlsSheet.Write(string.Format("C{0}", rowIndex), null, title);
Один "лучше", чем другой? И почему?
15 ответов
До C# 6
Если честно, я думаю, что первая версия проще - хотя я бы упростил ее до:
xlsSheet.Write("C" + rowIndex, null, title);
Я подозреваю, что другие ответы могут говорить о падении производительности, но, честно говоря, оно будет минимальным, если оно вообще присутствует - и эта версия объединения не должна анализировать строку формата.
Форматные строки отлично подходят для локализации и т. Д., Но в таком случае объединение проще и работает так же хорошо.
С C# 6
Строковая интерполяция делает много вещей проще для чтения в C# 6. В этом случае ваш второй код становится:
xlsSheet.Write($"C{rowIndex}", null, title);
что, вероятно, лучший вариант, ИМО.
Мое первоначальное предпочтение (исходящее из фона 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()
имеет преимущество
Я думаю, что первый вариант более читабелен, и это должно быть вашей главной заботой.
xlsSheet.Write("C" + rowIndex.ToString(), null, title);
string.Format использует StringBuilder под капотом (проверьте с помощью отражателя), поэтому он не принесет никакого выигрыша в производительности, если вы не выполните значительное количество операций конкатенации. Это будет медленнее для вашего сценария, но реальность такова, что это решение по оптимизации микропроцессора в большинстве случаев неуместно, и вы должны сосредоточиться на удобочитаемости своего кода, если только вы не зациклены.
В любом случае, сначала пишите для удобства чтения, а затем используйте профилировщик производительности, чтобы определить свои горячие точки, если вы действительно думаете, что у вас есть проблемы с производительностью.
Для простого случая, когда это простая одиночная конкатенация, я чувствую, что это не стоит сложности 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";
Если бы ваша строка была более сложной с объединением многих переменных, я бы выбрал string.Format(). Но для размера строки и количества переменных, соединяемых в вашем случае, я бы выбрал вашу первую версию, она более спартанская.
Я взглянул на String.Format (используя Reflector), и он фактически создает StringBuilder, а затем вызывает AppendFormat для него. Так что это быстрее, чем concat для нескольких перемешиваний. Самым быстрым (я полагаю) будет создание StringBuilder и выполнение вызовов Append вручную. Конечно, число "много" можно угадать. Я бы использовал + (на самом деле & потому что я в основном программист на VB) для чего-то столь же простого, как ваш пример. Поскольку это становится более сложным, я использую String.Format. Если есть много переменных, то я бы пошел на StringBuilder и Append, например, у нас есть код, который строит код, там я использую одну строку фактического кода для вывода одной строки сгенерированного кода.
Кажется, есть некоторые предположения о том, сколько строк создается для каждой из этих операций, поэтому давайте рассмотрим несколько простых примеров.
"C" + rowIndex.ToString();
"С" - это уже строка.
rowIndex.ToString () создает другую строку. (@manohard - бокс rowIndex не будет)
Затем мы получаем финальную строку.
Если мы возьмем пример
String.Format("C(0)",rowIndex);
тогда у нас есть "C{0}" в виде строки
rowIndex упаковывается для передачи в функцию
Новый string Builder создан
AppendFormat вызывается в строителе строк - я не знаю подробностей о том, как функции AppendFormat функционируют, но давайте предположим, что он очень эффективен, ему все равно придется преобразовать упакованный rowIndex в строку.
Затем преобразуйте строку в новую строку.
Я знаю, что StringBuilders пытаются предотвратить бессмысленное копирование памяти, но String.Format по-прежнему приводит к дополнительным издержкам по сравнению с простой конкатенацией.
Если мы сейчас возьмем пример с несколькими строками
"a" + rowIndex.ToString() + "b" + colIndex.ToString() + "c" + zIndex.ToString();
у нас есть 6 строк для начала, которые будут одинаковыми для всех случаев.
Используя конкатенацию, мы также имеем 4 промежуточные строки плюс конечный результат. Это те промежуточные результаты, которые устраняются с помощью String,Format (или StringBuilder).
Помните, что для создания каждой промежуточной строки предыдущая должна быть скопирована в новую область памяти, а не только выделение памяти, которая может быть медленной.
Этот пример, вероятно, слишком тривиален, чтобы заметить разницу. На самом деле, я думаю, что в большинстве случаев компилятор может оптимизировать любую разницу вообще.
Однако, если бы я должен был угадать, я бы дал string.Format()
преимущество для более сложных сценариев. Но это скорее интуитивное чувство, что лучше использовать буфер, вместо того, чтобы создавать несколько неизменяемых строк, не основываясь на каких-либо реальных данных.
Мне нравится String.Format, потому что он может сделать ваш форматированный текст намного проще для просмотра и чтения, чем для встроенной конкатенации, а также гораздо более гибким, позволяя вам форматировать параметры, однако для коротких операций, таких как ваша, я не вижу проблем с конкатенацией.
Для конкатенации внутри циклов или в больших строках вы всегда должны использовать класс StringBuilder.
О производительности:
void Main()
{
var start = CurrentTimeMillis();
for (var i = 0; i < 1000000; i++)
{
var s = "Hi " + i.ToString() + "; Hi to you " + (i * 2).ToString();
}
var end = CurrentTimeMillis();
Console.WriteLine("Concatenation = " + ((end - start)).ToString() + " millisecond");
start = CurrentTimeMillis();
for (var i = 0; i < 1000000; i++)
{
var s = String.Format("Hi {0}; Hi to you {1}", i, +i * 2);
}
end = CurrentTimeMillis();
Console.WriteLine("Format = " + ((end - start)).ToString() + " millisecond");
start = CurrentTimeMillis();
for (var i = 0; i < 1000000; i++)
{
var s = String.Concat("Hi ", i.ToString(), "; Hi to you ", (i * 2).ToString());
}
end = CurrentTimeMillis();
Console.WriteLine("Strng.concat = " + ((end - start)).ToString() + " millisecond");
start = CurrentTimeMillis();
for (int i = 0; i < 1000000; i++)
{
StringBuilder bldString = new StringBuilder("Hi ");
bldString.Append(i).Append("; Hi to you ").Append(i * 2);
}
end = CurrentTimeMillis();
Console.WriteLine("String Builder = " + ((end - start)) + " millisecond");
}
private static readonly DateTime Jan1st1970 = new DateTime
(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
public static long CurrentTimeMillis()
{
return (long)(DateTime.UtcNow - Jan1st1970).TotalMilliseconds;
}
Результат
Concatenation = 69 millisecond
Format = 142 millisecond
Strng.concat = 62 millisecond
String Builder = 91 millisecond
Чтобы сделать это справедливым сравнение, я создаю новый StringBuilder, а не тот, который был создан вне цикла (который, вероятно, будет быстрее из-за добавления цикла в конце пространства перераспределения одного строителя).
Я согласен со многими пунктами выше, еще одним моментом, который, я считаю, следует упомянуть, является удобство сопровождения кода. 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"
со строкой. Форматировать все, что мне нужно сделать, это изменить строку. для объединения я должен изменить это сообщение
если используется в нескольких местах, можно сэкономить время.
У меня сложилось впечатление, что 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 с.
Я немного профилировал различные строковые методы, включая string.Format, StringBuilder и конкатенацию строк. Конкатенация строк почти всегда превосходила другие методы построения строк. Так что, если производительность является ключевой, то лучше. Однако, если производительность не критична, я лично нахожу string.Format, чтобы легче было следовать в коде. (Но это субъективная причина) Однако StringBuilder, вероятно, наиболее эффективен в отношении использования памяти.
string.Format, вероятно, является лучшим выбором, когда шаблон формата ("C{0}") хранится в файле конфигурации (например, Web.config / App.config).
Конкатенация строк занимает больше памяти по сравнению с String.Format. Поэтому лучший способ объединить строки - использовать объект String.Format или System.Text.StringBuilder.
Давайте рассмотрим первый случай: "C" + rowIndex.ToString() Предположим, что rowIndex является типом значения, поэтому метод ToString () должен Box для преобразования значения в String, а затем CLR создает память для новой строки с обоими значениями.
Где как string.Format ожидает параметр объекта и принимает rowIndex в качестве объекта и преобразует его во внутреннюю строку, то будет Boxing, но он свойственный, а также он не будет занимать столько памяти, сколько в первом случае.
Для коротких струн это не имеет большого значения, я думаю...