Почему обычный .Contains() вас подведёт (и как это исправить)
Представьте: вы пишете поиск по каталогу книг на C#. Пользователь вводит «Толстой», а в базе хранится «толстой» с маленькой буквы. Обычный string.Contains("Толстой") вернёт false. Знакомая боль? Этот вопрос набрал тысячи просмотров на Stack Overflow — и не зря. В 2025 году, когда .NET предлагает мощные инструменты для работы со строками, использовать старый подход — значит терять пользователей и производительность.
В этой статье мы разберём 5 лучших способов сделать .Contains() регистронезависимым в C#. Вы узнаете, какой метод выбрать для максимальной скорости, а какой — для корректной работы с разными языками. Поехали!
Проблема: почему .Contains() по умолчанию чувствителен к регистру?
В C# метод string.Contains() по умолчанию использует Ordinal сравнение. Это значит, что символы сравниваются побайтово: 'A' (0x41) не равен 'a' (0x61). Для англоязычного контента это часто нормально, но в мультиязычных приложениях (русский, немецкий, турецкий) такой подход ломает логику поиска.
Например, в турецком языке буква 'i' в верхнем регистре — 'İ', а не 'I'. Обычное ToLower() может дать неверный результат. Поэтому просто вызывать text.ToLower().Contains(pattern.ToLower()) — плохая практика. Давайте посмотрим на современные решения.
Способ 1: StringComparison.OrdinalIgnoreCase — ваш новый стандарт
Начиная с .NET 5 (и отлично работает в .NET 8/9), самый быстрый и безопасный способ — использовать перегрузку Contains с параметром StringComparison:
string text = "Война и мир — Лев Толстой";
string searchTerm = "толстой";
bool result = text.Contains(searchTerm, StringComparison.OrdinalIgnoreCase);
Console.WriteLine(result); // True
Почему это лучший выбор?
- Скорость: OrdinalIgnoreCase работает на уровне байтов, но игнорирует регистр. Это в 2-3 раза быстрее, чем CurrentCultureIgnoreCase.
- Предсказуемость: Не зависит от культуры пользователя. "ß" (немецкая Eszett) не будет равна "ss", если вы этого не хотите.
- Поддержка: Доступен во всех современных версиях .NET (Core 2.1+).
Используйте этот метод, когда вам нужно простое сравнение без учёта регистра, и вы не работаете с лингвистическими особенностями (сортировка, склонения).
Способ 2: IndexOf() с StringComparison — если нужно больше контроля
Иногда вам нужно не просто проверить наличие, но и узнать позицию вхождения. Или вы работаете с более старыми версиями .NET Framework, где Contains с StringComparison отсутствует. Тогда на помощь приходит IndexOf():
string text = "C# — лучший язык для backend";
string searchTerm = "Лучший";
bool result = text.IndexOf(searchTerm, StringComparison.OrdinalIgnoreCase) >= 0;
Console.WriteLine(result); // True
// А если нужно получить позицию:
int index = text.IndexOf(searchTerm, StringComparison.OrdinalIgnoreCase);
Console.WriteLine(index); // 4 (позиция, с которой начинается слово)
Когда использовать:
- В legacy-проектах (например, .NET Framework 4.x).
- Когда нужно найти индекс вхождения, а не просто проверить наличие.
- Для более сложных сценариев (например, поиск с пропуском символов).
Важно: IndexOf возвращает -1, если подстрока не найдена. Всегда проверяйте на >= 0, а не на > 0.
Способ 3: CultureInfo.CurrentCultureIgnoreCase — для лингвистически корректного поиска
Если ваше приложение должно учитывать правила конкретного языка (например, немецкий умлаут или турецкая 'i'), используйте CurrentCultureIgnoreCase или InvariantCultureIgnoreCase:
using System.Globalization;
string text = "Straße"; // Немецкое слово "улица"
string searchTerm = "STRASSE";
// OrdinalIgnoreCase не сработает:
bool ordinalResult = text.Contains(searchTerm, StringComparison.OrdinalIgnoreCase);
Console.WriteLine(ordinalResult); // False (потому что ß != ss)
// А CurrentCultureIgnoreCase — сработает в немецкой культуре:
bool cultureResult = text.Contains(searchTerm, StringComparison.CurrentCultureIgnoreCase);
Console.WriteLine(cultureResult); // True (в культуре de-DE)
Минусы:
- Медленнее: CurrentCultureIgnoreCase может быть в 5-10 раз медленнее OrdinalIgnoreCase.
- Зависимость от культуры: Результат может меняться в зависимости от настроек сервера или пользователя.
Совет: Используйте InvariantCultureIgnoreCase как компромисс — он быстрее, чем CurrentCulture, и всё ещё учитывает некоторые лингвистические правила (например, для английского).
Способ 4: Регулярные выражения (Regex) — для сложных шаблонов
Если вам нужно не просто игнорировать регистр, но и искать по шаблону (например, "все слова, начинающиеся с 'толст'"), используйте Regex.IgnoreCase:
using System.Text.RegularExpressions;
string text = "Толстой написал 'Войну и мир'. Толстые книги — это здорово.";
string pattern = @"\bтолст\w*"; // Слова, начинающиеся с "толст"
bool result = Regex.IsMatch(text, pattern, RegexOptions.IgnoreCase);
Console.WriteLine(result); // True (найдёт "Толстой", "толстые")
// А если нужно получить все совпадения:
MatchCollection matches = Regex.Matches(text, pattern, RegexOptions.IgnoreCase);
foreach (Match match in matches)
{
Console.WriteLine(match.Value); // Выведет: "Толстой", "Толстые"
}
Когда нужно:
- Поиск по сложным шаблонам (регулярные выражения).
- Поиск с игнорированием регистра + другие опции (многострочный режим и т.д.).
Внимание: Regex медленнее обычных строковых методов. Для простого поиска всегда предпочитайте Contains с StringComparison.
Способ 5: Кастомный экстеншн-метод (для старых .NET Framework)
Если вы застряли на .NET Framework 4.6.2 или ниже, где нет перегрузки Contains с StringComparison, напишите свой extension-метод