Коли використовувати StringBuilder?


83

Я розумію переваги StringBuilder.

Але якщо я хочу об'єднати 2 рядки, то я припускаю, що краще (швидше) це зробити без StringBuilder. Це правильно?

У який момент (кількість рядків) стає краще використовувати StringBuilder?


1
Я вважаю, що це вже висвітлювалось раніше.
Марк Шультхайс


Відповіді:


79

Тепло пропоную вам прочитати «Сумну трагедію театру мікрооптимізації» Джефа Етвуда.

Він розглядає просту конкатенацію проти StringBuilder проти інших методів.

Тепер, якщо ви хочете побачити деякі цифри та графіки, перейдіть за посиланням;)


+1 за так ! Час, витрачений на турботи з цього приводу, - це час, витрачений не на те, щоб зробити щось, що може насправді мати значення.
Грег Д,

8
Однак ваше читання помилкове: це не має значення у багатьох випадках, коли не йдеться про циклічність, в інших випадках це може бути БАГАТО
Пітер,

1
Я видалив редагування, оскільки в прийнятій відповіді це була просто неправильна інформація.
Пітер

2
і просто, щоб показати, наскільки це важливо, зі статті, на яку ви посилаєтесь: "У більшості мов, зібраних сміттям, рядки є незмінними: коли ви додаєте два рядки, вміст обох копіюється. По мірі того, як ви продовжуєте додавати до результату в цьому циклі , щоразу виділяється все більше і більше пам'яті. Це призводить безпосередньо до жахливої ​​квадратичної продуктивності n2 "
Пітер,

2
Чому це прийнята відповідь. Я не думаю, що просто кинути посилання і сказати "іди читати це" є гарною відповіддю
Каньйон

45

Але якщо я хочу поєднати 2 рядки, то я припускаю, що краще (швидше) це зробити без StringBuilder. Це правильно?

Це справді правильно, ви можете зрозуміти, чому саме це дуже добре пояснили на:

http://www.yoda.arachsys.com/csharp/stringbuilder.html

Підсумовуємо: якщо ви можете поєднати рядки за один раз, як

var result = a + " " + b  + " " + c + ..

вам краще без StringBuilder, оскільки зроблено лише копію (довжина отриманого рядка обчислюється заздалегідь.);

Для структури на зразок

var result = a;
result  += " ";
result  += b;
result  += " ";
result  += c;
..

щоразу створюються нові об'єкти, тому там слід розглянути StringBuilder.

Наприкінці статті підсумовуються такі основні правила:

Емпіричні правила

Отже, коли слід використовувати StringBuilder, а коли - оператори конкатенації рядків?

  • Однозначно використовуйте StringBuilder, коли ви об'єднуєтеся в нетривіальний цикл - особливо, якщо ви точно не знаєте (під час компіляції), скільки ітерацій ви зробите через цикл. Наприклад, читання файлу за символом, створення рядка під час використання оператора + = - це потенційно самогубство.

  • Однозначно використовуйте оператор конкатенації, коли ви можете (читабельно) вказати все, що потрібно об’єднати в одному операторі. (Якщо у вас є масив об’єднаних речей, розгляньте можливість прямого виклику String.Concat - або String.Join, якщо вам потрібен роздільник.)

  • Не бійтеся розбивати літерали на кілька об’єднаних бітів - результат буде однаковим. Ви можете полегшити читабельність, наприклад, розбивши довгий літерал на кілька рядків, не завдаючи шкоди продуктивності.

  • Якщо вам потрібні проміжні результати конкатенації для чогось іншого, крім подання наступної ітерації конкатенації, StringBuilder не допоможе вам. Наприклад, якщо ви створите повне ім'я з імені та прізвища, а потім додасте третю інформацію (можливо, псевдонім) до кінця, ви отримаєте вигоду від використання StringBuilder, якщо цього не зробите. потрібен рядок (ім’я + прізвище) для інших цілей (як це робиться у прикладі, який створює об’єкт Person).

  • Якщо у вас є лише кілька об’єднань, і ви дійсно хочете зробити їх окремими висловлюваннями, не має значення, яким шляхом ви підете. Який спосіб є більш ефективним, залежатиме від кількості об’єднань, розмірів задіяних рядків та порядку їх об’єднання. Якщо ви дійсно вважаєте, що цей фрагмент коду є вузьким місцем для продуктивності, сформулюйте його або порівняйте в обох напрямках.


14

System.String є незмінним об'єктом - це означає, що всякий раз, коли ви змінюєте його вміст, він виділяє новий рядок, і це вимагає часу (і пам'яті?) За допомогою StringBuilder ви змінюєте фактичний вміст об’єкта, не виділяючи нового.

Тому використовуйте StringBuilder, коли вам потрібно внести багато змін у рядок.


8

Не дуже ... вам слід використовувати StringBuilder, якщо ви об'єднуєте великі рядки або у вас багато конкатенацій, як у циклі.


1
Це неправильно. Ви повинні використовувати, StringBuilderлише якщо цикл або конкатенація є проблемою продуктивності специфікацій.
Алекс Баньоліні, 01

2
@Alex: Хіба це не завжди так? ;) Ні, серйозно, я завжди використовував StringBuilder для конкатенації всередині циклу ... однак, усі мої цикли мають більше 1 к ітерацій ... @ Binary: Зазвичай це слід компілювати string s = "abcd", принаймні це останнє Я чув ... хоча, зі змінними це був би, швидше за все, Concat.
Боббі

1
Справа в тому, що це майже ЗАВЖДИ НЕ так. Я завжди користувався рядковими операторами a + "hello" + "somethingelse"і ніколи про це не турбувався. Якщо це стане проблемою, я буду використовувати StringBuilder. Але я не переживав з цього приводу і витрачав менше часу на його написання.
Алекс Баньоліні,

3
Великі рядки абсолютно не виграють від продуктивності - лише з багатьма об’єднаннями.
Конрад Рудольф

1
@Konrad: Ви впевнені, що жодної переваги від продуктивності? Кожного разу, коли ви об'єднуєте великі рядки, ви копіюєте великий обсяг даних; Кожного разу, коли ви об’єднуєте невеликі рядки, ви копіюєте лише невелику кількість даних.
LukeH

6
  • Якщо ви об'єднуєте рядки в цикл, вам слід розглянути можливість використання StringBuilder замість звичайного рядка
  • Якщо це єдине об’єднання, ви можете взагалі не побачити різниці у часі виконання

Ось простий тестовий додаток, щоб довести думку:

class Program
{
    static void Main(string[] args)
    {
        const int testLength = 30000;
        var StartTime = DateTime.Now;

        //TEST 1 - String
        StartTime = DateTime.Now;
        String tString = "test string";
        for (int i = 0; i < testLength; i++)
        {
            tString += i.ToString();
        }
        Console.WriteLine((DateTime.Now - StartTime).TotalMilliseconds.ToString());
        //result: 2000 ms

        //TEST 2 - StringBuilder
        StartTime = DateTime.Now;
        StringBuilder tSB = new StringBuilder("test string");
        for (int i = 0; i < testLength; i++)
        {
            tSB.Append(i.ToString());
        }
        Console.WriteLine((DateTime.Now - StartTime).TotalMilliseconds.ToString());
        //result: 4 ms

        Console.ReadLine();
    }
}

Результати:

  • 30 000 ітерацій

    • Рядок - 2000 мс
    • StringBuilder - 4 мс
  • 1000 ітерацій

    • Рядок - 2 мс
    • StringBuilder - 1 мс
  • 500 ітерацій

    • Рядок - 0 мс
    • StringBuilder - 0 мс

5

Перефразовуючи

Тоді ти будеш рахувати до трьох, ні більше, ні менше. Три буде числом, яке ти порахуєш, а число підрахунком буде три. Чотири ти не вважатимеш, ані два, за винятком того, що потім перейдеш до трьох. Як тільки число три, яке є третім числом, буде досягнуто, тоді пролобіруй свою Антиохійську гранату

Я зазвичай використовую конструктор рядків для будь-якого блоку коду, що призведе до об'єднання трьох або більше рядків.


Це залежить: Concetanation робить лише одну копію: "Russell" + "" + Steen + ".", Зробить лише одну копію, оскільки попередньо обчислює довжину рядка. Тільки тоді, коли вам доведеться розділити конкатенацію, ви повинні почати думати про будівельника
Петро

4

Однозначної відповіді немає, є лише принципові правила. Мої власні особисті правила звучать приблизно так:

  • Якщо об'єднується в циклі, завжди використовуйте a StringBuilder.
  • Якщо рядки великі, завжди використовуйте a StringBuilder.
  • Якщо код конкатенації охайний і читається на екрані, це, мабуть, нормально.
    Якщо це не так, використовуйте StringBuilder.

Я знаю, що це стара тема, але я знаю лише навчання і хотів би знати, що ви вважаєте "великою струною"?
MatthewD

4

Але якщо я хочу об'єднати 2 рядки, то я припускаю, що це краще і швидше робити без StringBuilder. Це правильно?

Так. Але що ще важливіше, набагато зручніше використовувати ваніль Stringу таких ситуаціях. З іншого боку, використання його в циклі має сенс, а також може бути таким же читабельним, як конкатенація.

Я б з обережністю ставився до емпіричних правил, які вказують конкретну кількість об’єднань як поріг. Використання його в циклах (і лише циклах), мабуть, настільки ж корисне, легше запам’ятовується і має більше сенсу.


"Я б з обережністю ставився до емпіричних правил, які вказують конкретну кількість об'єднань як поріг" <це. Крім того, після застосування здорового глузду, подумайте про те, хто повернеться до вашого коду через 6 місяців.
Phil Cooper

4

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

Я виявив, що використання рядків невеликого розміру, а не використання i.ToString (), змінює час відгуку (видно в малих циклах).

Тест використовує різні послідовності ітерацій, щоб зберегти вимірювання часу в помітно порівнянних діапазонах.

Я скопіюю код наприкінці, щоб ви могли спробувати самостійно (results.Charts ... Dump () не працюватиме поза LINQPad).

Вихідні дані (Вісь X: Кількість перевірених ітерацій, Вісь Y: Час, відведений на галочки):

Послідовність ітерацій: 2, 3, 4, 5, 6, 7, 8, 9, 10 Послідовність ітерацій: 2, 3, 4, 5, 6, 7, 8, 9, 10

Послідовність ітерацій: 10, 20, 30, 40, 50, 60, 70, 80 Послідовність ітерацій: 10, 20, 30, 40, 50, 60, 70, 80

Послідовність ітерацій: 100, 200, 300, 400, 500 Послідовність ітерацій: 100, 200, 300, 400, 500

Код (написаний за допомогою LINQPad 5):

void Main()
{
    Test(2, 3, 4, 5, 6, 7, 8, 9, 10);
    Test(10, 20, 30, 40, 50, 60, 70, 80);
    Test(100, 200, 300, 400, 500);
}

void Test(params int[] iterationsCounts)
{
    $"Iterations sequence: {string.Join(", ", iterationsCounts)}".Dump();

    int testStringLength = 10;
    RandomStringGenerator.Setup(testStringLength);
    var sw = new System.Diagnostics.Stopwatch();
    var results = new Dictionary<int, TimeSpan[]>();

    // This call before starting to measure time removes initial overhead from first measurement
    RandomStringGenerator.GetRandomString(); 

    foreach (var iterationsCount in iterationsCounts)
    {
        TimeSpan elapsedForString, elapsedForSb;

        // string
        sw.Restart();
        var str = string.Empty;

        for (int i = 0; i < iterationsCount; i++)
        {
            str += RandomStringGenerator.GetRandomString();
        }

        sw.Stop();
        elapsedForString = sw.Elapsed;


        // string builder
        sw.Restart();
        var sb = new StringBuilder(string.Empty);

        for (int i = 0; i < iterationsCount; i++)
        {
            sb.Append(RandomStringGenerator.GetRandomString());
        }

        sw.Stop();
        elapsedForSb = sw.Elapsed;

        results.Add(iterationsCount, new TimeSpan[] { elapsedForString, elapsedForSb });
    }


    // Results
    results.Chart(r => r.Key)
    .AddYSeries(r => r.Value[0].Ticks, LINQPad.Util.SeriesType.Line, "String")
    .AddYSeries(r => r.Value[1].Ticks, LINQPad.Util.SeriesType.Line, "String Builder")
    .DumpInline();
}

static class RandomStringGenerator
{
    static Random r;
    static string[] strings;

    public static void Setup(int testStringLength)
    {
        r = new Random(DateTime.Now.Millisecond);

        strings = new string[10];
        for (int i = 0; i < strings.Length; i++)
        {
            strings[i] = Guid.NewGuid().ToString().Substring(0, testStringLength);
        }
    }

    public static string GetRandomString()
    {
        var indx = r.Next(0, strings.Length);
        return strings[indx];
    }
}

3

Поки ви можете фізично ввести кількість об'єднань (a + b + c ...), це не повинно мати великої різниці. N у квадраті (при N = 10) - це уповільнення в 100 разів, що не повинно бути дуже погано.

Велика проблема полягає в тому, що ви об'єднуєте сотні рядків. При N = 100 ви отримуєте уповільнення в 10000 разів. Що досить погано.


2

Я не думаю, що існує чітка грань між тим, коли використовувати чи коли не. Якщо, звичайно, хтось не провів кілька обширних випробувань, щоб отримати золоті умови.

Для мене я не буду використовувати StringBuilder, якщо просто об'єднаю 2 величезні рядки. Якщо є цикл з недетермінованим рахунком, я, швидше за все, маю, навіть якщо цикл може бути невеликим.


Справді, було б абсолютно неправильно використовувати StringBuilder для конкатування 2 рядків, але це не має нічого спільного з perf. тестування - тобто просто використовувати його для неправильної речі.
Марк Гравелл

1

Окреме об’єднання не варто використовувати StringBuilder. Зазвичай я використовую 5 конкатенацій як правило.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.