Что такое пул строк (String Pool) в .NET?

Ответ

Пул строк (String Pool, или Intern Pool) — это механизм оптимизации в CLR (.NET), который хранит в памяти единственный экземпляр каждого уникального строкового литерала для предотвращения избыточных аллокаций.

Как это работает?

  1. Во время компиляции: Все строковые литералы, найденные в коде сборки, автоматически помещаются в пул.
  2. Во время выполнения: Создание строки с одинаковым литеральным значением приводит к возврату ссылки на уже существующий объект из пула.

Демонстрация:

string s1 = "Hello";
string s2 = "Hello";
string s3 = new string('H', 5); // "Hello", но создаётся через конструктор

// Сравнение ссылок
Console.WriteLine(ReferenceEquals(s1, s2)); // True: обе ссылаются на один объект в пуле
Console.WriteLine(ReferenceEquals(s1, s3)); // False: s3 — новый объект в куче

// Явное интернирование строки
string s4 = String.Intern(s3); // Принудительно помещаем s3 в пул
Console.WriteLine(ReferenceEquals(s1, s4)); // True: теперь s4 ссылается на объект из пула

// Проверка наличия в пуле
string interned = String.IsInterned("Hello"); // Вернёт "Hello" (ссылка на объект в пуле)
string notInterned = String.IsInterned(Guid.NewGuid().ToString()); // Вернёт null

Ключевые моменты и практические советы:

  • Что попадает в пул автоматически? Только строковые литералы из кода.
  • Динамические строки (результаты конкатенации, чтения из файла, ввода пользователя) НЕ интернируются автоматически. Для них нужно явно вызывать String.Intern().
  • Пул живёт в куче и не собирается GC до выгрузки домена приложения. Злоупотребление интернированием для уникальных динамических строк (например, GUID) может привести к утечке памяти.
  • Когда использовать String.Intern? Крайне редко. Возможный сценарий — обработка большого количества повторяющихся строковых значений, полученных динамически (например, парсинг XML/JSON с часто повторяющимися именами тегов). В 99% случаев доверяйте компилятору и CLR.
  • Сравнение строк: Благодаря пулу, сравнение строковых литералов через == (которое проверяет ссылки) может быть очень быстрым, но для надёжности всегда используйте String.Equals с указанием StringComparison (например, Ordinal).

Ответ 18+ 🔞

А, пул строк, ну это классика! Слушай, представь себе такую картину: ты в баре, и каждый раз, когда кто-то заказывает "пиво", бармен не бежит на завод за новой бутылкой. Он просто тянется на полку, где уже стоит одна бутылка с надписью "пиво", и даёт её. Вот этот бар — это и есть пул строк, или String Pool, если по-умному.

Как эта хрень работает, если по-простому?

  1. Когда код компилируется, все твои строчки, которые ты в коде написал в кавычках (типа "Привет"), автоматом кладутся на эту самую "полку" в памяти. Один раз.
  2. Когда программа работает, и ты в десяти местах пишешь "Привет", то все переменные просто тычут пальцем в одну и ту же бутылку на полке. Новую не создают — зачем, если уже есть?

Смотри, как это выглядит в деле:

string s1 = "Hello";
string s2 = "Hello"; // Тут CLR такая: "О, 'Hello'? Да он у меня уже на полке валяется!" — и даёт ссылку на ту же самую строку.
string s3 = new string('H', 5); // А вот тут ты явно говоришь: "Создай мне НОВУЮ строку 'Hello', блядь!" И её создают отдельно, в куче.

// Проверяем, это один и тот же объект в памяти или нет?
Console.WriteLine(ReferenceEquals(s1, s2)); // True: это одна и та же бутылка с "полки".
Console.WriteLine(ReferenceEquals(s1, s3)); // False: s3 — это отдельная, только что с завода, бутылка. Хотя на вкус то же самое "пиво".

// Но если очень хочется, можно и новую бутылку запихнуть на общую полку (явное интернирование)
string s4 = String.Intern(s3); // Силой засовываем s3 в пул.
Console.WriteLine(ReferenceEquals(s1, s4)); // True: теперь и s4 смотрит на ту же самую, первую бутылку.

// А можно спросить: "Эй, а эта строка у тебя на полке уже есть?"
string interned = String.IsInterned("Hello"); // Вернёт "Hello" — да, есть, вот ссылка.
string notInterned = String.IsInterned(Guid.NewGuid().ToString()); // Вернёт null — нет, такой хуйни на полке ещё не стояло.

Важные моменты, чтобы не обжечься:

  • Автоматом в пул летят только строковые литералы — то, что прямо в коде в кавычках написано. Всё.
  • Всё, что создаётся на ходу (склеилось, приехало из сети, из файла, пользователь ввёл) — по умолчанию НЕ попадает в пул. Для этого надо вызывать String.Intern(), но это очень острый нож.
  • Пул — это такая память, которую сборщик мусора (GC) не трогает до самой выгрузки программы. Представь: если ты начнёшь в него силком пихать миллион уникальных GUID'ов — они там навечно останутся. Это гарантированная утечка памяти, ёпта. Овердохуища места займут.
  • Когда вообще это использовать? Да почти никогда. Серьёзно. Разве что у тебя парсится какой-нибудь огромный JSON, где одни и те же названия полей повторяются миллион раз, и ты хочешь сэкономить память. В 99.9% случаев CLR сама всё отлично оптимизирует, не лезь со своим Intern().
  • Про сравнение строк: Да, из-за пула сравнение == для литералов может быть мгновенным (просто проверка ссылок). Но это ненадёжно, чувак. Всегда, блядь, используй String.Equals с явным указанием StringComparison.Ordinal или другого правила. Так надёжнее, и спать спокойнее будешь.