НейроАгент

Как сделать Contains() нечувствительным к регистру в C#

Узнайте несколько способов сделать string.Contains() нечувствительным к регистру в C# с использованием StringComparison, методов расширения и лучших практик. Избегайте проблем с i18n с помощью эффективного сравнения строк без учета регистра.

Вопрос

Как сделать string.Contains() нечувствительным к регистру в C#

Есть ли способ сделать так, чтобы следующий код возвращал true?

csharp
string title = "ASTRINGTOTEST";
title.Contains("string");

Похоже, нет перегрузки, которая позволила бы установить чувствительность к регистру. В настоящее время я преобразую обе строки в ВЕРХНИЙ РЕГИСТР, но это просто глупо (я имею в виду проблемы i18n, связанные с преобразованием в верхний и нижний регистр).

НейроАгент

Метод string.Contains() в C# не поддерживает сравнение без учета регистра напрямую, но вы можете добиться этого, используя параметр StringComparison, реализовав метод расширения или преобразовав обе строки в один и тот же регистр с помощью методов сравнения, учитывающих культурные особенности. Рекомендуемый подход — использование StringComparison.OrdinalIgnoreCase, который обрабатывает сравнение без учета регистра более эффективно, чем преобразование обеих строк в верхний или нижний регистр.

Содержание


Базовое решение с StringComparison

Метод string.Contains() не имеет встроенной перегрузки для сравнения без учета регистра, но вы можете обойти это ограничение, используя метод IndexOf(), который принимает параметр StringComparison:

csharp
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() без учета регистра:

csharp
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, соответствующий вашему первоначальному намерению, при этом сохраняя правильное поведение сравнения без учета регистра.

Расширенный метод расширения с несколькими вариантами

Вы можете расширить это дальше, чтобы поддерживать разные режимы сравнения:

csharp
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) Работает с исходными строками Ниже Безопасен
Метод расширения Минимальные накладные расходы Ниже Безопасен
csharp
// Пример теста производительности
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() могут привести к неверным результатам в международных приложениях.

Проблемы простого преобразования регистра

csharp
// Проблемные примеры
string german = "STRASSE"; // Немецкое слово "улица"
string turkish = "I";      // Турецкая точка I

// Эти могут не работать в разных культурах
bool germanToUpper = german.ToUpper() == "STRASSE".ToUpper(); // Работает
bool turkishIssue = turkish.ToUpper() == "i".ToUpper();       // Проблематично

Подходы, учитывающие культурные особенности

csharp
// Использование 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

csharp
// Для внутренней обработки данных
bool contains = data.Contains("search", StringComparison.OrdinalIgnoreCase);

// Для текста, видимого пользователю
bool displayContains = userInput.Contains("search", StringComparison.CurrentCultureIgnoreCase);

// Для операций с файловой системой
bool fileExists = fileName.Contains("temp", StringComparison.Ordinal);

2. Учитывайте производительность в циклах

csharp
// Кэшируйте StringComparison для повторяющихся операций
var comparison = StringComparison.OrdinalIgnoreCase;
foreach (var item in items)
{
    if (item.Value.Contains("search", comparison))
    {
        // Обработка совпадения
    }
}

3. Безопасно обрабатывайте null и пустые строки

csharp
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. Документируйте вашу стратегию сравнения

csharp
/// <summary>
/// Ищет текст без учета регистра с использованием порядкового сравнения.
/// Подходит для внутренней обработки данных и критичных к производительности сценариев.
/// </summary>
public bool ContainsSearchTerm(string searchTerm)
{
    return Content.IndexOf(searchTerm, StringComparison.OrdinalIgnoreCase) >= 0;
}

Расширенные сценарии

Регулярные выражения с игнорированием регистра

Для сложного сопоставления шаблонов регулярные выражения предоставляют мощные варианты без учета регистра:

csharp
using System.Text.RegularExpressions;

string title = "ASTRINGTOTEST";
bool contains = Regex.IsMatch(title, "string", RegexOptions.IgnoreCase);

Пользовательское сравнение с IEqualityComparer

csharp
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);

Методы поиска по нескольким строкам

csharp
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 и их соответствующие случаи использования в вашем приложении.