Как сделать string.Contains() нечувствительным к регистру в C#
Есть ли способ сделать так, чтобы следующий код возвращал true?
string title = "ASTRINGTOTEST";
title.Contains("string");
Похоже, нет перегрузки, которая позволила бы установить чувствительность к регистру. В настоящее время я преобразую обе строки в ВЕРХНИЙ РЕГИСТР, но это просто глупо (я имею в виду проблемы i18n, связанные с преобразованием в верхний и нижний регистр).
Метод string.Contains() в C# не поддерживает сравнение без учета регистра напрямую, но вы можете добиться этого, используя параметр StringComparison, реализовав метод расширения или преобразовав обе строки в один и тот же регистр с помощью методов сравнения, учитывающих культурные особенности. Рекомендуемый подход — использование StringComparison.OrdinalIgnoreCase, который обрабатывает сравнение без учета регистра более эффективно, чем преобразование обеих строк в верхний или нижний регистр.
Содержание
- Базовое решение с StringComparison
- Подход с методом расширения
- Сравнение производительности
- Рассмотрения интернационализации
- Лучшие практики
- Расширенные сценарии
Базовое решение с StringComparison
Метод string.Contains() не имеет встроенной перегрузки для сравнения без учета регистра, но вы можете обойти это ограничение, используя метод IndexOf(), который принимает параметр StringComparison:
string title = "ASTRINGTOTEST";
bool contains = title.IndexOf("string", StringComparison.OrdinalIgnoreCase) >= 0;
Этот подход напрямую решает вашу задачу, выполняя поиск без учета регистра. Значение перечисления StringComparison.OrdinalIgnoreCase указывает, что сравнение должно игнорировать различия в регистре, поэтому “ASTRINGTOTEST” содержит “string” вернет true.
Почему IndexOf() вместо Contains()?
Класс string предоставляет несколько методов сравнения с разными перегрузками:
- Contains(): Принимает только строковый параметр
- IndexOf(): Принимает как строковый, так и параметр StringComparison
- StartsWith(): Имеет перегрузки с StringComparison
- EndsWith(): Имеет перегрузки с StringComparison
Эта несогласованность в .NET Framework является причиной того, что разработчикам часто приходится обходить это ограничение с помощью IndexOf().
Подход с методом расширения
Для более чистого и читаемого кода вы можете создать метод расширения, предоставляющий функциональность Contains() без учета регистра:
public static class StringExtensions
{
public static bool ContainsIgnoreCase(this string source, string value)
{
if (string.IsNullOrEmpty(source) || string.IsNullOrEmpty(value))
return false;
return source.IndexOf(value, StringComparison.OrdinalIgnoreCase) >= 0;
}
}
// Использование
string title = "ASTRINGTOTEST";
bool contains = title.ContainsIgnoreCase("string");
Этот метод расширения предоставляет более интуитивный API, соответствующий вашему первоначальному намерению, при этом сохраняя правильное поведение сравнения без учета регистра.
Расширенный метод расширения с несколькими вариантами
Вы можете расширить это дальше, чтобы поддерживать разные режимы сравнения:
public static bool Contains(this string source, string value, StringComparison comparisonType)
{
if (string.IsNullOrEmpty(source) || string.IsNullOrEmpty(value))
return false;
return source.IndexOf(value, comparisonType) >= 0;
}
// Использование
string title = "ASTRINGTOTEST";
bool containsOrdinal = title.Contains("string", StringComparison.Ordinal);
bool containsIgnoreCase = title.Contains("string", StringComparison.OrdinalIgnoreCase);
bool containsCurrentCulture = title.Contains("string", StringComparison.CurrentCultureIgnoreCase);
Сравнение производительности
Разные подходы имеют различные характеристики производительности. Вот сравнение распространенных методов:
| Метод | Примечания по производительности | Использование памяти | Потокобезопасность |
|---|---|---|---|
ToUpper().Contains() |
Создает новые строковые объекты | Выше | Безопасен |
ToLower().Contains() |
Создает новые строковые объекты | Выше | Безопасен |
IndexOf(StringComparison) |
Работает с исходными строками | Ниже | Безопасен |
| Метод расширения | Минимальные накладные расходы | Ниже | Безопасен |
// Пример теста производительности
var sw = Stopwatch.StartNew();
for (int i = 0; i < 1000000; i++)
{
bool result = title.IndexOf("string", StringComparison.OrdinalIgnoreCase) >= 0;
}
sw.Stop();
Console.WriteLine($"IndexOf с StringComparison: {sw.ElapsedMilliseconds}мс");
Подход IndexOf() с StringComparison обычно является наиболее эффективным, потому что он:
- Не выделяет новые строковые объекты
- Использует оптимизированные нативные процедуры сравнения
- Работает напрямую с исходными данными строки
Рассмотрения интернационализации
Ваше беспокойство о проблемах i18n при преобразовании регистра обосновано. Разные культуры обрабатывают преобразование регистра по-разному, и простые методы ToUpper() или ToLower() могут привести к неверным результатам в международных приложениях.
Проблемы простого преобразования регистра
// Проблемные примеры
string german = "STRASSE"; // Немецкое слово "улица"
string turkish = "I"; // Турецкая точка I
// Эти могут не работать в разных культурах
bool germanToUpper = german.ToUpper() == "STRASSE".ToUpper(); // Работает
bool turkishIssue = turkish.ToUpper() == "i".ToUpper(); // Проблематично
Подходы, учитывающие культурные особенности
// Использование CurrentCulture (подходит для текста, видимого пользователю)
bool containsCurrent = title.Contains("string", StringComparison.CurrentCultureIgnoreCase);
// Использование InvariantCulture (подходит для обработки данных)
bool containsInvariant = title.Contains("string", StringComparison.InvariantCultureIgnoreCase);
// Использование Ordinal (лучше для производительности, когда регистр - единственный вопрос)
bool containsOrdinal = title.Contains("string", StringComparison.OrdinalIgnoreCase);
Когда использовать каждый StringComparison
- StringComparison.Ordinal: Лучшая производительность, подходит для внутренних идентификаторов, путей к файлам и обработки данных
- StringComparison.OrdinalIgnoreCase: Хороший баланс производительности и сопоставления без учета регистра
- StringComparison.CurrentCulture: Подходит для текста, видимого пользователю, и отображения
- StringComparison.InvariantCulture: Подходит для постоянных данных и кросс-культурных сценариев
Лучшие практики
1. Выбирайте подходящий StringComparison
// Для внутренней обработки данных
bool contains = data.Contains("search", StringComparison.OrdinalIgnoreCase);
// Для текста, видимого пользователю
bool displayContains = userInput.Contains("search", StringComparison.CurrentCultureIgnoreCase);
// Для операций с файловой системой
bool fileExists = fileName.Contains("temp", StringComparison.Ordinal);
2. Учитывайте производительность в циклах
// Кэшируйте StringComparison для повторяющихся операций
var comparison = StringComparison.OrdinalIgnoreCase;
foreach (var item in items)
{
if (item.Value.Contains("search", comparison))
{
// Обработка совпадения
}
}
3. Безопасно обрабатывайте null и пустые строки
public static bool SafeContainsIgnoreCase(this string source, string value)
{
if (source == null || value == null)
return false;
if (source.Length == 0 || value.Length == 0)
return source.Length == value.Length;
return source.IndexOf(value, StringComparison.OrdinalIgnoreCase) >= 0;
}
4. Документируйте вашу стратегию сравнения
/// <summary>
/// Ищет текст без учета регистра с использованием порядкового сравнения.
/// Подходит для внутренней обработки данных и критичных к производительности сценариев.
/// </summary>
public bool ContainsSearchTerm(string searchTerm)
{
return Content.IndexOf(searchTerm, StringComparison.OrdinalIgnoreCase) >= 0;
}
Расширенные сценарии
Регулярные выражения с игнорированием регистра
Для сложного сопоставления шаблонов регулярные выражения предоставляют мощные варианты без учета регистра:
using System.Text.RegularExpressions;
string title = "ASTRINGTOTEST";
bool contains = Regex.IsMatch(title, "string", RegexOptions.IgnoreCase);
Пользовательское сравнение с IEqualityComparer
public class CaseInsensitiveComparer : IEqualityComparer<string>
{
public bool Equals(string x, string y)
{
return string.Equals(x, y, StringComparison.OrdinalIgnoreCase);
}
public int GetHashCode(string obj)
{
return obj?.ToLower().GetHashCode() ?? 0;
}
}
// Использование
var comparer = new CaseInsensitiveComparer();
bool contains = new List<string> { "ASTRINGTOTEST" }.Contains("string", comparer);
Методы поиска по нескольким строкам
public static class StringSearch
{
public static bool ContainsAny(this string source, IEnumerable<string> values, StringComparison comparison = StringComparison.OrdinalIgnoreCase)
{
return values.Any(value => source.Contains(value, comparison));
}
public static bool ContainsAll(this string source, IEnumerable<string> values, StringComparison comparison = StringComparison.OrdinalIgnoreCase)
{
return values.All(value => source.Contains(value, comparison));
}
}
// Использование
string text = "The quick brown fox";
bool hasAny = text.ContainsAny(new[] { "QUICK", "FOX" });
bool hasAll = text.ContainsAll(new[] { "QUICK", "BROWN" });
Для полной документации по методам сравнения строк в .NET обращайтесь к официальной документации Microsoft .NET.
Заключение
- Используйте IndexOf() с StringComparison.OrdinalIgnoreCase как основное решение для операций Contains со строками без учета регистра в C#
- Создавайте методы расширения для более чистого и читаемого кода, соответствующего вашему первоначальному намерению
- Учитывайте влияние на производительность - подход с IndexOf более эффективен, чем преобразование обеих строк в верхний или нижний регистр
- Учитывайте интернационализацию - используйте подходящие значения StringComparison в зависимости от вашего случая использования (Ordinal для производительности, CurrentCulture для текста, видимого пользователю)
- Реализуйте методы, безопасные для null, для обработки граничных случаев в производственном коде
- Выбирайте правильную стратегию сравнения в зависимости от того, имеете ли дело с внутренней обработкой данных, текстом, видимым пользователю, или хранением постоянных данных
Функциональность Contains() без учета регистра может быть легко реализована, как только вы поймете доступные варианты StringComparison и их соответствующие случаи использования в вашем приложении.