Поделиться через


Рекомендации по сравнению строк в .NET

.NET предоставляет широкую поддержку для разработки локализованных и глобальных приложений и упрощает применение соглашений как текущей культуры, так и определённой культуры при выполнении общих операций, таких как сортировка и отображение строк. Но сортировка или сравнение строк не всегда является операцией с учетом культурных особенностей. Например, строки, используемые внутри приложения, обычно обрабатываются одинаково во всех культурах. Если строковые данные, не зависящие от культурных особенностей, такие как XML-теги, теги HTML, имена пользователей, пути к файлам и имена системных объектов, интерпретируются как чувствительные к языковым и региональным параметрам, код приложения может столкнуться с тонкими ошибками, сниженной производительностью и, в некоторых случаях, проблемами безопасности.

В этой статье рассматриваются методы сортировки строк, сравнения и регистра в .NET, представлены рекомендации по выбору соответствующего метода обработки строк и дополнительные сведения о методах обработки строк.

Рекомендации по использованию строк

При разработке с помощью .NET следуйте этим рекомендациям при сравнении строк.

Подсказка

Различные методы, связанные со строками, выполняют сравнение. String.EqualsНапример, , String.Compare, String.IndexOfи String.StartsWith.

  • Используйте перегрузки, которые явно указывают правила сравнения строк для строковых операций. Как правило, это включает вызов перегрузки метода, которая имеет параметр типа StringComparison.
  • Используйте StringComparison.Ordinal или StringComparison.OrdinalIgnoreCase для сравнения в качестве безопасного значения по умолчанию для сопоставления строк, не зависящих от языка и региональных параметров.
  • Используйте сравнения с StringComparison.Ordinal или StringComparison.OrdinalIgnoreCase для повышения производительности.
  • Используйте строковые операции, которые основаны на StringComparison.CurrentCulture, когда отображаете выходные данные пользователю.
  • Используйте значения StringComparison.Ordinal или StringComparison.OrdinalIgnoreCase вместо строковых операций, основанных на CultureInfo.InvariantCulture, когда сравнение не имеет лингвистического значения (например, символическое).
  • String.ToUpperInvariant Используйте метод вместо String.ToLowerInvariant метода при нормализации строк для сравнения.
  • Используйте перегрузку метода String.Equals, чтобы проверить, равны ли две строки.
  • Используйте методы String.Compare и String.CompareTo для сортировки строк, а не для проверки равенства.
  • Используйте форматирование с учетом языка и региональных параметров для отображения нестроковых данных, таких как числа и даты, в пользовательском интерфейсе. Используйте форматирование с инвариантной культурой для сохранения нестроковых данных в строковой форме.

Избегайте следующих методик при сравнении строк:

  • Не используйте перегрузки, которые не указывают явно или неявно правила сравнения строк для строковых операций.
  • Не используйте операции со строками, основанные на StringComparison.InvariantCulture, в большинстве случаев. Одно из немногих исключений заключается в сохранении лингвистически значимых, но не зависящих от культуры данных.
  • Не используйте перегрузку метода String.Compare или CompareTo и не проверяйте возвращаемое значение на наличие нуля, чтобы определить, равны ли две строки.

Подсказка

Правила анализа кода CA1307, CA1309 и CA1310 помогают определить сайты вызовов, где лингвистическое сравнение используется непреднамеренно. Чтобы включить их и выявить нарушения в качестве ошибок сборки, задайте следующие свойства в файле проекта:

<PropertyGroup>
  <AnalysisMode>All</AnalysisMode>
  <WarningsAsErrors>$(WarningsAsErrors);CA1307;CA1309;CA1310</WarningsAsErrors>
</PropertyGroup>

Явное указание сравнений строк

Большинство методов обработки строк в .NET перегружены. Как правило, одна или несколько перегрузок принимают параметры по умолчанию, в то время как другие не принимают значения по умолчанию и вместо этого определяют точный способ сравнения или обработки строк. Большинство методов, которые не полагаются на значения по умолчанию, включают параметр типа StringComparison, представляющий перечисление, задающее правила сравнения строк по культуре и регистру. В следующей StringComparison таблице описаны элементы перечисления.

ЧленStringComparison Описание
CurrentCulture Выполняет сравнение с учетом регистра, используя текущую языковую среду.
CurrentCultureIgnoreCase Выполняет сравнение без учета регистра с помощью текущей культуры.
InvariantCulture Выполняет сравнение с учетом регистра с помощью инвариантной культуры.
InvariantCultureIgnoreCase Выполняет нечувствительное к регистру сравнение с помощью инвариантной культуры.
Ordinal Выполняет порядковое сравнение.
OrdinalIgnoreCase Выполняет порядковое сравнение строк без учета регистра.

Например, IndexOf метод, возвращающий индекс подстроки в String объекте, который соответствует символу или строке, имеет девять перегрузок:

Рекомендуем выбрать перегрузку, не использующую значения по умолчанию, по следующим причинам:

  • Некоторые перегрузки с параметрами по умолчанию (те, которые ищут Char в экземпляре строки) выполняют порядковое сравнение, в то время как другие (те, которые ищут строку в строковом экземпляре) чувствительны к языку и региональным настройкам. Трудно помнить, какой метод использует значение по умолчанию, и легко запутать перегрузки.

  • Намерение кода, использующее значения по умолчанию для вызовов методов, не ясно. В следующем примере, который зависит от значений по умолчанию, трудно знать, предназначен ли разработчик фактически порядковый номер или лингвистическое сравнение двух строк, или может ли разница между url.Scheme "https" и "https" привести к проверке на равенство false.

    Uri url = new("https://learn.microsoft.com/");
    
    // Incorrect
    if (string.Equals(url.Scheme, "https"))
    {
        // ...Code to handle HTTPS protocol.
    }
    
    Dim url As New Uri("https://learn.microsoft.com/")
    
    ' Incorrect
    If String.Equals(url.Scheme, "https") Then
        ' ...Code to handle HTTPS protocol.
    End If
    

Как правило, рекомендуется вызывать метод, который не зависит от значений по умолчанию, так как он делает намерение кода однозначно. Это, в свою очередь, делает код более удобочитаемым и упрощает отладку и обслуживание. В следующем примере рассматриваются вопросы, возникающие в предыдущем примере. Это означает, что используется порядковое сравнение и что различия в случае игнорируются.

Uri url = new("https://learn.microsoft.com/");

// Correct
if (string.Equals(url.Scheme, "https", StringComparison.OrdinalIgnoreCase))
{
    // ...Code to handle HTTPS protocol.
}
Dim url As New Uri("https://learn.microsoft.com/")

' Incorrect
If String.Equals(url.Scheme, "https", StringComparison.OrdinalIgnoreCase) Then
    ' ...Code to handle HTTPS protocol.
End If

Сведения о сравнении строк

Сравнение строк является основой многих операций со строками, особенно сортировки и проверки на равенство. Строки сортируются в определенном порядке: если "my" находится перед "string" в отсортированном списке строк, то "my" должно быть меньше или равно "string". Кроме того, сравнение неявно определяет равенство. Операция сравнения возвращает ноль для строк, которые она считает равной. Хорошее толкование заключается в том, что ни одна строка не меньше другой. Большинство значимых операций, связанных с строками, включают одну или обе эти процедуры: сравнение с другой строкой и выполнение четко определенной операции сортировки.

Примечание.

Вы можете скачать таблицы веса сортировки, набор текстовых файлов, содержащих сведения о весах символов, используемых в операциях сортировки и сравнения для операционных систем Windows, а также таблицу элементов сортировки Юникода по умолчанию, последнюю версию таблицы веса сортировки для Linux и macOS. Конкретная версия таблицы веса сортировки в Linux и macOS зависит от версии международных компонентов библиотек Юникода, установленных в системе. Сведения о версиях ICU и версиях Юникода, которые они реализуют, см. в разделе "Скачивание ICU".

Однако оценка двух строк для равенства или порядка сортировки не дает единого правильного результата; Результат зависит от критериев, используемых для сравнения строк. В частности, сравнения строк, которые являются порядковыми или основанными на соглашениях о регистре и сортировке текущей культуры или инвариантной культуры (языковой стандарт, не зависящий от конкретной локации и основанный на английском языке), могут привести к различным результатам.

Кроме того, сравнения строк с использованием разных версий .NET или .NET в разных операционных системах или версиях операционной системы могут возвращать разные результаты. .NET использует библиотеку Международных компонентов для Юникода (ICU) для лингвистических сравнений строк на всех поддерживаемых платформах. Дополнительные сведения см. в разделе Строки и стандарт Юникода и глобализация .NET и ICU.

Сравнения строк, использующие текущий язык и региональные параметры

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

Однако поведение сравнения и регистра в .NET изменяется при изменении культурных параметров. Это происходит, когда приложение выполняется на компьютере с другим языком и региональными параметрами, чем на компьютере, на котором было разработано приложение, или при изменении языка и региональных параметров выполнения потока. Это поведение намеренно, но оно остается неясным для многих разработчиков. В следующем примере показаны различия в порядке сортировки в культурах США (английский) ("en-US") и шведской ("sv-SE"). Обратите внимание, что слова "ångström", "Windows" и "Visual Studio" отображаются в разных позициях в отсортированных массивах строк.

using System.Globalization;

// Words to sort
string[] values= { "able", "ångström", "apple", "Æble",
                    "Windows", "Visual Studio" };

// Current culture
Array.Sort(values);
DisplayArray(values);

// Change culture to Swedish (Sweden)
string originalCulture = CultureInfo.CurrentCulture.Name;
Thread.CurrentThread.CurrentCulture = new CultureInfo("sv-SE");
Array.Sort(values);
DisplayArray(values);

// Restore the original culture
Thread.CurrentThread.CurrentCulture = new CultureInfo(originalCulture);

static void DisplayArray(string[] values)
{
    Console.WriteLine($"Sorting using the {CultureInfo.CurrentCulture.Name} culture:");
    
    foreach (string value in values)
        Console.WriteLine($"   {value}");

    Console.WriteLine();
}

// The example displays the following output:
//     Sorting using the en-US culture:
//        able
//        Æble
//        ångström
//        apple
//        Visual Studio
//        Windows
//
//     Sorting using the sv-SE culture:
//        able
//        apple
//        Visual Studio
//        Windows
//        ångström
//        Æble
Imports System.Globalization
Imports System.Threading

Module Program
    Sub Main()
        ' Words to sort
        Dim values As String() = {"able", "ångström", "apple", "Æble",
                                  "Windows", "Visual Studio"}

        ' Current culture
        Array.Sort(values)
        DisplayArray(values)

        ' Change culture to Swedish (Sweden)
        Dim originalCulture As String = CultureInfo.CurrentCulture.Name
        Thread.CurrentThread.CurrentCulture = New CultureInfo("sv-SE")
        Array.Sort(values)
        DisplayArray(values)

        ' Restore the original culture
        Thread.CurrentThread.CurrentCulture = New CultureInfo(originalCulture)
    End Sub

    Sub DisplayArray(values As String())
        Console.WriteLine($"Sorting using the {CultureInfo.CurrentCulture.Name} culture:")

        For Each value As String In values
            Console.WriteLine($"   {value}")
        Next

        Console.WriteLine()
    End Sub
End Module

' The example displays the following output:
'     Sorting using the en-US culture:
'        able
'        Æble
'        ångström
'        apple
'        Visual Studio
'        Windows
'
'     Sorting using the sv-SE culture:
'        able
'        apple
'        Visual Studio
'        Windows
'        ångström
'        Æble

Текущие сравнения без учёта регистра, использующие настройки текущей культуры, аналогичны сравнениям с учётом культурных особенностей, за исключением того, что они игнорируют регистр в соответствии с текущими параметрами потока. Это поведение также может проявляться в порядке сортировки.

Сравнения, использующие семантику текущей культуры, являются значениями по умолчанию для следующих методов.

В любом случае рекомендуется вызвать перегрузку, которая имеет StringComparison параметр, чтобы сделать намерение вызова метода ясным.

Тонкие и явные ошибки могут возникать, когда нелингвистические строковые данные интерпретируются лингвистически или когда строковые данные из определённой культуры интерпретируются с помощью соглашений другой культуры. Канонический пример — это проблема Turkish-I.

Для почти всех латинских алфавитов, включая английский язык США, символ "i" (\u0069) является нижней версией символа "I" (\u0049). Это правило регистра быстро становится стандартным для программиста в такой культуре. Тем не менее, турецкий ("tr-TR") алфавит включает символ "I с точкой" — "İ" (\u0130), который является заглавной версией "i". Турецкий также включает строчный символ "i без точки", "ı" (\u0131), который превращается в заглавную "I". Это поведение также происходит в культуре Азербайджана ("az").

Поэтому предположения, сделанные о прописной букве "i" или нижней части "I" не являются допустимыми среди всех культур. Если вы используете перегрузки по умолчанию для процедур сравнения строк, они будут подвержены различиям между культурами. Если сравниваемые данные являются нелингвистическими, использование перегрузок по умолчанию может привести к нежелательным результатам, как демонстрирует следующая попытка выполнить сравнение строк "bill" и "BILL" без учета регистра символов.

using System.Globalization;

string name = "Bill";

Thread.CurrentThread.CurrentCulture = new CultureInfo("en-US");
Console.WriteLine($"Culture = {Thread.CurrentThread.CurrentCulture.DisplayName}");
Console.WriteLine($"   Is 'Bill' the same as 'BILL'? {name.Equals("BILL", StringComparison.OrdinalIgnoreCase)}");
Console.WriteLine($"   Does 'Bill' start with 'BILL'? {name.StartsWith("BILL", true, null)}");
Console.WriteLine();

Thread.CurrentThread.CurrentCulture = new CultureInfo("tr-TR");
Console.WriteLine($"Culture = {Thread.CurrentThread.CurrentCulture.DisplayName}");
Console.WriteLine($"   Is 'Bill' the same as 'BILL'? {name.Equals("BILL", StringComparison.OrdinalIgnoreCase)}");
Console.WriteLine($"   Does 'Bill' start with 'BILL'? {name.StartsWith("BILL", true, null)}");

//' The example displays the following output:
//'
//'     Culture = English (United States)
//'        Is 'Bill' the same as 'BILL'? True
//'        Does 'Bill' start with 'BILL'? True
//'     
//'     Culture = Turkish (Türkiye)
//'        Is 'Bill' the same as 'BILL'? True
//'        Does 'Bill' start with 'BILL'? False
Imports System.Globalization
Imports System.Threading

Module Program
    Sub Main()
        Dim name As String = "Bill"

        Thread.CurrentThread.CurrentCulture = New CultureInfo("en-US")
        Console.WriteLine($"Culture = {Thread.CurrentThread.CurrentCulture.DisplayName}")
        Console.WriteLine($"   Is 'Bill' the same as 'BILL'? {name.Equals("BILL", StringComparison.OrdinalIgnoreCase)}")
        Console.WriteLine($"   Does 'Bill' start with 'BILL'? {name.StartsWith("BILL", True, Nothing)}")
        Console.WriteLine()

        Thread.CurrentThread.CurrentCulture = New CultureInfo("tr-TR")
        Console.WriteLine($"Culture = {Thread.CurrentThread.CurrentCulture.DisplayName}")
        Console.WriteLine($"   Is 'Bill' the same as 'BILL'? {name.Equals("BILL", StringComparison.OrdinalIgnoreCase)}")
        Console.WriteLine($"   Does 'Bill' start with 'BILL'? {name.StartsWith("BILL", True, Nothing)}")
    End Sub

End Module

' The example displays the following output:
'
'     Culture = English (United States)
'        Is 'Bill' the same as 'BILL'? True
'        Does 'Bill' start with 'BILL'? True
'     
'     Culture = Turkish (Türkiye)
'        Is 'Bill' the same as 'BILL'? True
'        Does 'Bill' start with 'BILL'? False

Это сравнение может привести к значительным проблемам, если культура непреднамеренно используется в контекстах, связанных с безопасностью, как показано в следующем примере. Вызов метода IsFileURI("file:") возвращает true, если текущая культура — английский (США), и false, если текущая культура — турецкий. Таким образом, в турецких системах кто-то может обойти меры безопасности, которые блокируют доступ к нечувствительным URI регистра, начинающимся с file:.

public static bool IsFileURI(string path) =>
    path.StartsWith("FILE:", true, null);
Public Shared Function IsFileURI(path As String) As Boolean
    Return path.StartsWith("FILE:", True, Nothing)
End Function

В этом случае, поскольку "file:" предназначен для интерпретации как нелингвистического идентификатора, не зависящего от языка и культуры, код должен быть написан, как показано в следующем примере:

public static bool IsFileURI(string path) =>
    path.StartsWith("FILE:", StringComparison.OrdinalIgnoreCase);
Public Shared Function IsFileURI(path As String) As Boolean
    Return path.StartsWith("FILE:", StringComparison.OrdinalIgnoreCase)
End Function

Порядковые строковые операции

Указание StringComparison.Ordinal или StringComparison.OrdinalIgnoreCase значение в вызове метода означает нелингвистическое сравнение, при котором функции естественных языков игнорируются. Методы, вызываемые этими StringComparison значениями, определяют операции со строками на основе простых операций сравнения байтов вместо таблиц регистра или эквивалентности, параметризованных культурными настройками. В большинстве случаев этот подход лучше всего подходит для предполагаемой интерпретации строк, делая код более быстрым и надежным.

Порядковые сравнения — это сравнения строк, в которых каждый байт каждой строки сравнивается без лингвистической интерпретации; Например, "windows" не соответствует "Windows". Это, по сути, вызов функции среды выполнения strcmp C. Используйте это сравнение, если контекст диктует соответствие строк точно или требует консервативной политики сопоставления. Кроме того, порядковое сравнение является самой быстрой операцией сравнения, так как она не применяет лингвистические правила при определении результата.

Средство OrdinalIgnoreCase сравнения по-прежнему работает на основе char-by-char, но устраняет различия случаев при выполнении операции. OrdinalIgnoreCase Под сравнивателем пары символов 'd' и 'D' сравниваются как равные, как и пары символов 'á' и 'Á'. Но неакцентированный символ 'a' сравнивается как не равный акцентированному символу 'á'.

Некоторые примеры этого приведены в следующей таблице:

Строка 1 Строка 2 Ordinal Сравнение OrdinalIgnoreCase Сравнение
"dog" "dog" равный равный
"dog" "Dog" не равно равный
"resume" "résumé" не равно не равно

Юникод также позволяет строкам иметь несколько различных представлений в памяти. Например, é (e с острым ударением) может представляться в двух вариантах.

  • Один литеральный 'é' символ (также написанный как '\u00E9').
  • Литеральный без акцента 'e' символ, за которым следует символ акцентного модификатора '\u0301'.

Это означает, что следующие четыре строки отображаются как "résumé", даже если их составляющие части отличаются. Строки используют сочетание буквальных 'é' символов или буквально без акцента 'e' символов, а также модификатор комбинирования акцента '\u0301'.

  • "r\u00E9sum\u00E9"
  • "r\u00E9sume\u0301"
  • "re\u0301sum\u00E9"
  • "re\u0301sume\u0301"

При порядковом сравнении ни одна из этих строк не является равной другой. Это связано с тем, что все они содержат разные базовые последовательности символов, даже если при отображении на экране они выглядят одинаково.

При выполнении string.IndexOf(..., StringComparison.Ordinal) операции среда выполнения ищет точное соответствие подстроки. Результаты приведены следующим образом.

Console.WriteLine("resume".IndexOf('e', StringComparison.Ordinal)); // "resume": prints '1'
Console.WriteLine("r\u00E9sum\u00E9".IndexOf('e', StringComparison.Ordinal)); // "résumé": prints '-1'
Console.WriteLine("r\u00E9sume\u0301".IndexOf('e', StringComparison.Ordinal)); // "résumé": prints '5'
Console.WriteLine("re\u0301sum\u00E9".IndexOf('e', StringComparison.Ordinal)); // "résumé": prints '1'
Console.WriteLine("re\u0301sume\u0301".IndexOf('e', StringComparison.Ordinal)); // "résumé": prints '1'
Console.WriteLine("resume".IndexOf('e', StringComparison.OrdinalIgnoreCase)); // "resume": prints '1'
Console.WriteLine("r\u00E9sum\u00E9".IndexOf('e', StringComparison.OrdinalIgnoreCase)); // "résumé": prints '-1'
Console.WriteLine("r\u00E9sume\u0301".IndexOf('e', StringComparison.OrdinalIgnoreCase)); // "résumé": prints '5'
Console.WriteLine("re\u0301sum\u00E9".IndexOf('e', StringComparison.OrdinalIgnoreCase)); // "résumé": prints '1'
Console.WriteLine("re\u0301sume\u0301".IndexOf('e', StringComparison.OrdinalIgnoreCase)); // "résumé": prints '1'
Sub IndexOfExample()
    Console.WriteLine("resume".IndexOf("e"c, StringComparison.Ordinal)) ' "resume": prints '1'
    Console.WriteLine(("r" & ChrW(&HE9) & "sum" & ChrW(&HE9)).IndexOf("e"c, StringComparison.Ordinal)) ' "résumé": prints '-1'
    Console.WriteLine(("r" & ChrW(&HE9) & "sume" & ChrW(&H301)).IndexOf("e"c, StringComparison.Ordinal)) ' "résumé": prints '5'
    Console.WriteLine(("re" & ChrW(&H301) & "sum" & ChrW(&HE9)).IndexOf("e"c, StringComparison.Ordinal)) ' "résumé": prints '1'
    Console.WriteLine(("re" & ChrW(&H301) & "sume" & ChrW(&H301)).IndexOf("e"c, StringComparison.Ordinal)) ' "résumé": prints '1'
    Console.WriteLine("resume".IndexOf("e"c, StringComparison.OrdinalIgnoreCase)) ' "resume": prints '1'
    Console.WriteLine(("r" & ChrW(&HE9) & "sum" & ChrW(&HE9)).IndexOf("e"c, StringComparison.OrdinalIgnoreCase)) ' "résumé": prints '-1'
    Console.WriteLine(("r" & ChrW(&HE9) & "sume" & ChrW(&H301)).IndexOf("e"c, StringComparison.OrdinalIgnoreCase)) ' "résumé": prints '5'
    Console.WriteLine(("re" & ChrW(&H301) & "sum" & ChrW(&HE9)).IndexOf("e"c, StringComparison.OrdinalIgnoreCase)) ' "résumé": prints '1'
    Console.WriteLine(("re" & ChrW(&H301) & "sume" & ChrW(&H301)).IndexOf("e"c, StringComparison.OrdinalIgnoreCase)) ' "résumé": prints '1'
End Sub

Порядковый поиск и процедуры сравнения никогда не влияют на параметры языка и региональных параметров текущего потока.

Строки в .NET могут содержать внедренные пустые символы (и другие непечатаемые символы). Одно из самых четких различий между порядковым и учитывающим культурные особенности сравнением (включая сравнения, использующие язык-инвариант и культуру) касается обработки внедренных нулевых символов в строке. Эти символы игнорируются при использовании методов String.Compare и String.Equals для выполнения сравнения с учетом культурных особенностей (включая сравнения, использующие инвариантные культурные параметры). В результате строки, содержащие внедренные символы NULL, можно считать равными строкам, которые их не содержат. Встроенные символы, не выводимые на печать, могут пропускаться для целей методов сравнения строк, таких как String.StartsWith.

Это важно

Хотя методы сравнения строк игнорируют внедренные символы NULL, методы поиска строк, такие как String.Contains, String.EndsWith, String.IndexOf, String.LastIndexOf и String.StartsWith, не делают.

В следующем примере выполняется сравнение строки с учетом культурных особенностей с аналогичной строкой, содержащей несколько внедренных нулевых символов между «A» и «a», и показывается, как две строки считаются равными.

string str1 = "Aa";
string str2 = "A" + new string('\u0000', 3) + "a";

Thread.CurrentThread.CurrentCulture = System.Globalization.CultureInfo.GetCultureInfo("en-us");

Console.WriteLine($"Comparing '{str1}' ({ShowBytes(str1)}) and '{str2}' ({ShowBytes(str2)}):");
Console.WriteLine("   With String.Compare:");
Console.WriteLine($"      Current Culture: {string.Compare(str1, str2, StringComparison.CurrentCulture)}");
Console.WriteLine($"      Invariant Culture: {string.Compare(str1, str2, StringComparison.InvariantCulture)}");
Console.WriteLine("   With String.Equals:");
Console.WriteLine($"      Current Culture: {string.Equals(str1, str2, StringComparison.CurrentCulture)}");
Console.WriteLine($"      Invariant Culture: {string.Equals(str1, str2, StringComparison.InvariantCulture)}");

string ShowBytes(string value)
{
   string hexString = string.Empty;
   for (int index = 0; index < value.Length; index++)
   {
      string result = Convert.ToInt32(value[index]).ToString("X4");
      result = string.Concat(" ", result.Substring(0,2), " ", result.Substring(2, 2));
      hexString += result;
   }
   return hexString.Trim();
}

// The example displays the following output:
//     Comparing 'Aa' (00 41 00 61) and 'Aa' (00 41 00 00 00 00 00 00 00 61):
//        With String.Compare:
//           Current Culture: 0
//           Invariant Culture: 0
//        With String.Equals:
//           Current Culture: True
//           Invariant Culture: True

Module Program
    Sub Main()
        Dim str1 As String = "Aa"
        Dim str2 As String = "A" & New String(Convert.ToChar(0), 3) & "a"

        Console.WriteLine($"Comparing '{str1}' ({ShowBytes(str1)}) and '{str2}' ({ShowBytes(str2)}):")
        Console.WriteLine("   With String.Compare:")
        Console.WriteLine($"      Current Culture: {String.Compare(str1, str2, StringComparison.CurrentCulture)}")
        Console.WriteLine($"      Invariant Culture: {String.Compare(str1, str2, StringComparison.InvariantCulture)}")
        Console.WriteLine("   With String.Equals:")
        Console.WriteLine($"      Current Culture: {String.Equals(str1, str2, StringComparison.CurrentCulture)}")
        Console.WriteLine($"      Invariant Culture: {String.Equals(str1, str2, StringComparison.InvariantCulture)}")
    End Sub

    Function ShowBytes(str As String) As String
        Dim hexString As String = String.Empty

        For ctr As Integer = 0 To str.Length - 1
            Dim result As String = Convert.ToInt32(str.Chars(ctr)).ToString("X4")
            result = String.Concat(" ", result.Substring(0, 2), " ", result.Substring(2, 2))
            hexString &= result
        Next

        Return hexString.Trim()
    End Function

    ' The example displays the following output:
    '     Comparing 'Aa' (00 41 00 61) and 'Aa' (00 41 00 00 00 00 00 00 00 61):
    '        With String.Compare:
    '           Current Culture: 0
    '           Invariant Culture: 0
    '        With String.Equals:
    '           Current Culture: True
    '           Invariant Culture: True
End Module

Однако строки не считаются равными при использовании порядкового сравнения, как показано в следующем примере:

string str1 = "Aa";
string str2 = "A" + new String('\u0000', 3) + "a";

Console.WriteLine($"Comparing '{str1}' ({ShowBytes(str1)}) and '{str2}' ({ShowBytes(str2)}):");
Console.WriteLine("   With String.Compare:");
Console.WriteLine($"      Ordinal: {string.Compare(str1, str2, StringComparison.Ordinal)}");
Console.WriteLine("   With String.Equals:");
Console.WriteLine($"      Ordinal: {string.Equals(str1, str2, StringComparison.Ordinal)}");

string ShowBytes(string str)
{
    string hexString = string.Empty;
    for (int ctr = 0; ctr < str.Length; ctr++)
    {
        string result = Convert.ToInt32(str[ctr]).ToString("X4");
        result = " " + result.Substring(0, 2) + " " + result.Substring(2, 2);
        hexString += result;
    }
    return hexString.Trim();
}

// The example displays the following output:
//    Comparing 'Aa' (00 41 00 61) and 'A   a' (00 41 00 00 00 00 00 00 00 61):
//       With String.Compare:
//          Ordinal: 97
//       With String.Equals:
//          Ordinal: False
Module Program
    Sub Main()
        Dim str1 As String = "Aa"
        Dim str2 As String = "A" & New String(Convert.ToChar(0), 3) & "a"

        Console.WriteLine($"Comparing '{str1}' ({ShowBytes(str1)}) and '{str2}' ({ShowBytes(str2)}):")
        Console.WriteLine("   With String.Compare:")
        Console.WriteLine($"      Ordinal: {String.Compare(str1, str2, StringComparison.Ordinal)}")
        Console.WriteLine("   With String.Equals:")
        Console.WriteLine($"      Ordinal: {String.Equals(str1, str2, StringComparison.Ordinal)}")
    End Sub

    Function ShowBytes(str As String) As String
        Dim hexString As String = String.Empty

        For ctr As Integer = 0 To str.Length - 1
            Dim result As String = Convert.ToInt32(str.Chars(ctr)).ToString("X4")
            result = String.Concat(" ", result.Substring(0, 2), " ", result.Substring(2, 2))
            hexString &= result
        Next

        Return hexString.Trim()
    End Function

    ' The example displays the following output:
    '    Comparing 'Aa' (00 41 00 61) and 'A   a' (00 41 00 00 00 00 00 00 00 61):
    '       With String.Compare:
    '          Ordinal: 97
    '       With String.Equals:
    '          Ordinal: False
End Module

Порядковые сравнения без учета регистра — это следующий по степени консерватизма подход. Эти сравнения игнорируют различия в написании; например, "windows" соответствует "Windows". При работе с символами ASCII эта политика эквивалентна StringComparison.Ordinal, за исключением того, что она игнорирует обычный регистр ASCII. Таким образом, любой символ в [A, Z] (\u0041-\u005A) соответствует соответствующему символу в [a,z] (\u0061-\007A). Регистры за пределами диапазона ASCII используют таблицы инвариантной культуры. Поэтому следующее сравнение:

string.Compare(strA, strB, StringComparison.OrdinalIgnoreCase);
String.Compare(strA, strB, StringComparison.OrdinalIgnoreCase)

эквивалентно сравнению (но быстрее).

string.Compare(strA.ToUpperInvariant(), strB.ToUpperInvariant(), StringComparison.Ordinal);
String.Compare(strA.ToUpperInvariant(), strB.ToUpperInvariant(), StringComparison.Ordinal)

Эти сравнения по-прежнему очень быстры.

Оба StringComparison.Ordinal и StringComparison.OrdinalIgnoreCase используют двоичные значения напрямую и лучше всего подходят для сопоставления. Если вы не уверены в параметрах сравнения, используйте одно из этих двух значений. Тем не менее, поскольку они выполняют сравнение байт за байтом, они не сортируются по лингвистическому порядку сортировки (например, как словарь английского языка), а по двоичному порядку сортировки. Результаты могут выглядеть нечетно в большинстве контекстов, если они отображаются пользователям.

По умолчанию используется порядковая семантика для String.Equals перегрузок, которые не включают StringComparison аргумент (включая оператор равенства). В любом случае рекомендуется вызвать перегрузку с параметром StringComparison .

Сравнение строковых данных в лингвистическом контексте

Лингвистические процедуры поиска и сравнения разлагают строку на элементы сортировки и выполняют поиск или сравнение этих элементов. Между символами строки и её составляющими элементами сортировки не всегда существует прямое соответствие. Например, строка длины 2 может состоять только из одного элемента сортировки. При сравнении двух строк в лингвистическом режиме средство сравнения проверяет, имеют ли элементы сортировки двух строк одинаковые семантические значения, даже если литеральные символы строки отличаются.

Рассмотрим строку "résumé" и четыре различных представления, описанные в предыдущем разделе. В следующей таблице показано каждое представление, разбитое на его элементы сортировки.

String Как элементы сортировки
"r\u00E9sum\u00E9" "r" + "\u00E9" + "s" + "u" + "m" + "\u00E9"
"r\u00E9sume\u0301" "r" + "\u00E9" + "s" + "u" + "m" + "e\u0301"
"re\u0301sum\u00E9" "r" + "e\u0301" + "s" + "u" + "m" + "\u00E9"
"re\u0301sume\u0301" "r" + "e\u0301" + "s" + "u" + "m" + "e\u0301"

Элемент сортировки слабо соответствует тому, что читатели считают одним символом или кластером символов. Он концептуально похож на кластер графем, но охватывает более широкий диапазон.

В лингвистическом сравнении точные совпадения не нужны. Вместо этого элементы сортировки сравниваются на основе их семантического значения. Например, лингвистическое сравнение обрабатывает подстроки "\u00E9" и "e\u0301" как равные, так как они обоим семантически соответствуют "строчной букве e с острым акцентом". Это позволяет методу IndexOf сопоставлять подстроку "e\u0301" в более крупной строке, содержащей эквивалентную по семантике подстроку "\u00E9", как показано в следующем примере кода.

Console.WriteLine("r\u00E9sum\u00E9".IndexOf("e")); // "résumé": prints '-1' (not found)
Console.WriteLine("r\u00E9sum\u00E9".IndexOf("\u00E9")); // "résumé": prints '1'
Console.WriteLine("\u00E9".IndexOf("e\u0301")); // prints '0'
Sub IndexOfStringExample()
    Console.WriteLine(("r" & ChrW(&HE9) & "sum" & ChrW(&HE9)).IndexOf("e")) ' "résumé": prints '-1' (not found)
    Console.WriteLine(("r" & ChrW(&HE9) & "sum" & ChrW(&HE9)).IndexOf(ChrW(&HE9).ToString())) ' "résumé": prints '1'
    Console.WriteLine(ChrW(&HE9).ToString().IndexOf("e" & ChrW(&H301))) ' prints '0'
End Sub

В результате этого две строки разной длины могут сравниться как равные, если используется лингвистическое сравнение. Функции, которые вызываются, не должны создавать специальную логику для обработки длины строки в таких сценариях.

Процедуры поиска и сравнения с учетом языка и культуры — это специальная форма лингвистических процедур поиска и сравнения. В средстве сравнения, учитывающем особенности культуры, концепция элемента сортировки расширяется, чтобы включить сведения, относящиеся к указанной культуре.

Например, в венгерском алфавите, когда два символа <dz> идут подряд, они считаются собственной уникальной буквой, отличной от <d> или <z>. Это означает, что когда в строке встречается <dz>, учитывающий венгерскую культуру компаратор обрабатывает его как единый элемент сортировки.

String Как элементы сортировки Замечания
"endz" "e" + "n" + "d" + "z" (с помощью стандартного языкового сравнителя)
"endz" "e" + "n" + "dz" (с помощью средства сравнения с учетом венгерской культурной специфики)

При использовании венгерского сравнения с учётом культуры строка "endz"не заканчивается на подстроку "z", так как <dz> и <z> считаются элементами сортировки с разными семантическими значениями.

// Set thread culture to Hungarian
CultureInfo.CurrentCulture = CultureInfo.GetCultureInfo("hu-HU");
Console.WriteLine("endz".EndsWith("z")); // Prints 'False'

// Set thread culture to invariant culture
CultureInfo.CurrentCulture = CultureInfo.InvariantCulture;
Console.WriteLine("endz".EndsWith("z")); // Prints 'True'
' Set thread culture to Hungarian
CultureInfo.CurrentCulture = CultureInfo.GetCultureInfo("hu-HU")
Console.WriteLine("endz".EndsWith("z")) ' Prints 'False'

' Set thread culture to invariant culture
CultureInfo.CurrentCulture = CultureInfo.InvariantCulture
Console.WriteLine("endz".EndsWith("z")) ' Prints 'True'

Примечание.

  • Поведение: Сравнители, учитывающие языковые и культурные особенности, могут время от времени претерпевать поведенческие изменения. И ICU, и более старая подсистема NLS Windows обновляются, чтобы учитывать изменения в мировых языках. Для получения дополнительной информации см. запись в блоге Изменение данных локали (культуры). Поведение Ordinal сравнивателя никогда не изменится, так как он выполняет точный побитовый поиск и сравнение. Однако поведение сравнения OrdinalIgnoreCase может измениться по мере роста Юникода, чтобы охватывать больше наборов символов и исправлять упущения в существующих данных регистра.
  • Использование: компараторы StringComparison.InvariantCulture и StringComparison.InvariantCultureIgnoreCase являются лингвистическими сравнивателями, которые не учитывают культуру. То есть эти компараторы понимают такие понятия, как акцентированный символ é с несколькими возможными базовыми представлениями, и что все такие представления должны рассматриваться как равные. Но лингвистические сравнения, не учитывающие культурные особенности, не будут содержать специальную обработку для <dz>, отличающуюся от <d> или <z>, как показано выше. Они также не будут обрабатывать специальные символы, такие как немецкий Eszett (ß).

.NET также предлагает инвариантный режим глобализации. Этот режим опции отключает кодовые маршруты, которые относятся к лингвистическому поиску и процедурам сравнения. В этом режиме все операции используют Ordinal или OrdinalIgnoreCase поведение, независимо от того, какой аргумент CultureInfo или StringComparison предоставляет вызывающий объект. Дополнительные сведения см. в разделе "Параметры конфигурации среды выполнения" для глобализации и инвариантного режима глобализации .NET Core.

Строковые операции, использующие инвариантный язык и региональные параметры

Сравнения с инвариантной культурой используют свойство CompareInfo, возвращаемое статическим свойством CultureInfo.InvariantCulture. Это поведение одинаково для всех систем; он преобразует любые символы вне своего диапазона в то, что он считает эквивалентными инвариантными символами. Эта политика может быть полезной для поддержания одного набора поведения строк в различных культурах, но часто это дает неожиданные результаты.

Сравнения без учета регистра с инвариантным языком и региональными параметрами используют статическое CompareInfo свойство, возвращаемое статическим CultureInfo.InvariantCulture свойством для сведений о сравнении, а также. Любые различия между переведенными символами игнорируются.

Сравнения, которые используют StringComparison.InvariantCulture и StringComparison.Ordinal работают одинаково в строках ASCII. StringComparison.InvariantCulture Однако принимает лингвистические решения, которые могут не соответствовать строкам, которые должны быть интерпретированы как набор байтов. Объект CultureInfo.InvariantCulture.CompareInfo делает Compare метод интерпретируемым определенными наборами символов как эквивалентные. Например, следующая эквивалентность допустима в инвариантном языке и региональных параметров:

InvariantCulture: a + ̊ = å

ЛАТИНСКАЯ НЕБОЛЬШАЯ БУКВА СИМВОЛ "a" (\u0061), когда рядом с символом "+ " ̊" (\u030a), интерпретируется как ЛАТИНСКАЯ НЕБОЛЬШАЯ БУКВА С КОЛЬЦОМ ВЫШЕ символ "å" (\u00e5). Как показано в следующем примере, это поведение отличается от порядкового сравнения.

string separated = "\u0061\u030a";
string combined = "\u00e5";

Console.WriteLine($"Equal sort weight of {separated} and {combined} using InvariantCulture: {string.Compare(separated, combined, StringComparison.InvariantCulture) == 0}");

Console.WriteLine($"Equal sort weight of {separated} and {combined} using Ordinal: {string.Compare(separated, combined, StringComparison.Ordinal) == 0}");

// The example displays the following output:
//     Equal sort weight of a° and å using InvariantCulture: True
//     Equal sort weight of a° and å using Ordinal: False
Module Program
    Sub Main()
        Dim separated As String = ChrW(&H61) & ChrW(&H30A)
        Dim combined As String = ChrW(&HE5)

        Console.WriteLine("Equal sort weight of {0} and {1} using InvariantCulture: {2}",
                          separated, combined,
                          String.Compare(separated, combined, StringComparison.InvariantCulture) = 0)

        Console.WriteLine("Equal sort weight of {0} and {1} using Ordinal: {2}",
                          separated, combined,
                          String.Compare(separated, combined, StringComparison.Ordinal) = 0)

        ' The example displays the following output:
        '     Equal sort weight of a° and å using InvariantCulture: True
        '     Equal sort weight of a° and å using Ordinal: False
    End Sub
End Module

При интерпретации имен файлов, файлов cookie или других элементов, где может отображаться сочетание, например "å", порядковые сравнения по-прежнему обеспечивают наиболее прозрачное и подходящее поведение.

В целом инвариантная культура обладает лишь немногими свойствами, которые делают её полезной для сравнения. Он сравнивает лингвистически релевантный способ, который препятствует гарантии полной символьной эквивалентности, но это не выбор для отображения в любом языке и региональных параметрах. Одной из немногих причин использования StringComparison.InvariantCulture для сравнения является сохранение упорядоченных данных, чтобы обеспечить их идентичное отображение в разных культурах. Например, если большой файл данных, содержащий список отсортированных идентификаторов для отображения, сопровождает приложение, добавление в этот список потребует вставки с инвариантной сортировкой по стилю.

Как выбрать элемент StringComparison

В следующей таблице описывается сопоставление из контекста семантической строки с элементом StringComparison перечисления:

Данные Поведение Соответствующий System.StringComparison

ценность
Внутренние чувствительные к регистру идентификаторы.

Идентификаторы с учетом регистра в таких стандартах, как XML и HTTP.

Параметры, связанные с безопасностью с учетом регистра.
Нелингвистические идентификаторы, где точно совпадают байты. Ordinal
Внутренние идентификаторы без учета регистра.

Идентификаторы, нечувствительные к регистру, в таких стандартах, как XML и HTTP.

Пути к файлам.

Ключи и значения реестра.

переменные окружения.

Идентификаторы ресурсов (например, дескриптор имен).

Настройки, связанные с безопасностью, без учета регистра символов.
Нелингвистический идентификатор, в котором регистр не имеет значения. OrdinalIgnoreCase
Некоторые сохраненные, лингвистически релевантные данные.

Отображение лингвистических данных, требующих фиксированного порядка сортировки.
Не зависящие от культуры данные, которые по-прежнему лингвистически актуальны. InvariantCulture

-или-

InvariantCultureIgnoreCase
Данные, отображаемые пользователю.

Большинство пользовательских вводов.
Данные, требующие местных лингвистических обычаев. CurrentCulture

-или-

CurrentCultureIgnoreCase

Последствия для безопасности

Если приложение использует строковые API для фильтрации или управления доступом, используйте порядковые сравнения. Лингвистические сравнения на основе текущей культуры могут приводить к непредвиденным результатам, которые зависят от платформы и локализации. Такие шаблоны кода могут быть подвержены эксплойтам безопасности:

//
// THIS SAMPLE CODE IS INCORRECT.
// DO NOT USE IT IN PRODUCTION.
//
bool ContainsHtmlSensitiveCharacters(string input)
{
    if (input.IndexOf("<") >= 0) { return true; }
    if (input.IndexOf("&") >= 0) { return true; }
    return false;
}
'
' THIS SAMPLE CODE IS INCORRECT.
' DO NOT USE IT IN PRODUCTION.
'
Function ContainsHtmlSensitiveCharacters(input As String) As Boolean
    If input.IndexOf("<") >= 0 Then Return True
    If input.IndexOf("&") >= 0 Then Return True
    Return False
End Function

string.IndexOf(string) Поскольку метод использует лингвистический поиск по умолчанию, строка может содержать литерал '<' или символ '&', а string.IndexOf(string) возвратит -1, указывая, что подстрока поиска не найдена. Правила анализа кода CA1307 и CA1309 помечают такие сайты вызовов и предупреждают разработчика о возможной проблеме.

Распространенные методы сравнения строк в .NET

В следующих разделах описываются методы, которые чаще всего используются для сравнения строк.

String.Compare

Интерпретация по умолчанию: StringComparison.CurrentCulture.

Как операция, основополагающая для интерпретации строк, все вызовы данных методов должны быть проанализированы, чтобы определить, следует ли интерпретировать строки в соответствии с текущими языковыми и региональными стандартами или рассматривать их как независимые от этих стандартов (в символическом смысле). Обычно это так, и вместо этого следует использовать сравнение с StringComparison.Ordinal.

Класс System.Globalization.CompareInfo, возвращаемый свойством CultureInfo.CompareInfo, также включает Compare метод, предоставляющий большое количество параметров сопоставления (порядковый, игнорируя пробелы и тип kana и т. д.) с помощью перечисления флагов CompareOptions.

String.CompareTo

Интерпретация по умолчанию: StringComparison.CurrentCulture.

В настоящее время этот метод не предлагает перегрузку, указывающую StringComparison тип. Обычно этот метод можно преобразовать в рекомендуемую String.Compare(String, String, StringComparison) форму.

Типы, которые реализуют интерфейсы IComparable и IComparable<T>, реализуют этот метод. Так как отсутствует возможность указать параметр StringComparison, реализация типов часто разрешает определить StringComparer в их конструкторе. В следующем примере определяется FileName класс, конструктор которого содержит StringComparer параметр. Затем этот StringComparer объект используется в методе FileName.CompareTo .

class FileName : IComparable
{
    private readonly StringComparer _comparer;

    public string Name { get; }

    public FileName(string name, StringComparer? comparer)
    {
        if (string.IsNullOrEmpty(name)) throw new ArgumentNullException(nameof(name));

        Name = name;

        if (comparer != null)
            _comparer = comparer;
        else
            _comparer = StringComparer.OrdinalIgnoreCase;
    }

    public int CompareTo(object? obj)
    {
        if (obj == null) return 1;

        if (obj is not FileName)
            return _comparer.Compare(Name, obj.ToString());
        else
            return _comparer.Compare(Name, ((FileName)obj).Name);
    }
}
Class FileName
    Implements IComparable

    Private ReadOnly _comparer As StringComparer

    Public ReadOnly Property Name As String

    Public Sub New(name As String, comparer As StringComparer)
        If (String.IsNullOrEmpty(name)) Then Throw New ArgumentNullException(NameOf(name))

        Me.Name = name

        If comparer IsNot Nothing Then
            _comparer = comparer
        Else
            _comparer = StringComparer.OrdinalIgnoreCase
        End If
    End Sub

    Public Function CompareTo(obj As Object) As Integer Implements IComparable.CompareTo
        If obj Is Nothing Then Return 1

        If TypeOf obj IsNot FileName Then
            Return _comparer.Compare(Name, obj.ToString())
        Else
            Return _comparer.Compare(Name, DirectCast(obj, FileName).Name)
        End If
    End Function
End Class

String.Equals

Интерпретация по умолчанию: StringComparison.Ordinal.

Класс String позволяет проверить равенство путем вызова перегрузки статических или экземплярных Equals методов или с помощью оператора статического равенства. Перегрузки и операторы используют порядковое сравнение по умолчанию. Однако мы по-прежнему рекомендуем вызывать перегрузку, которая явно указывает StringComparison тип, даже если требуется выполнить порядковое сравнение; это упрощает поиск кода для определенной интерпретации строк.

String.ToUpper и String.ToLower.

Интерпретация по умолчанию: StringComparison.CurrentCulture.

Будьте осторожны при использовании методов String.ToUpper() и String.ToLower(), так как приведение строки к верхнему или нижнему регистру часто используется как небольшая нормализация для сравнения строк независимо от регистра. В этом случае рекомендуется использовать сравнение строк без учета регистра.

Доступны String.ToUpperInvariant и String.ToLowerInvariant методы. ToUpperInvariant — это стандартный способ нормализации регистра. Сравнения, сделанные с помощью StringComparison.OrdinalIgnoreCase, поведенчески представляют собой состав двух вызовов: вызов ToUpperInvariant для обоих строковых аргументов и сравнение с помощью StringComparison.Ordinal.

Перегрузки также доступны для преобразования в верхний и нижний регистр в определенном языке и региональных параметрах, передав CultureInfo объект, представляющий этот язык и региональные параметры в метод.

Char.ToUpper и Char.ToLower.

Интерпретация по умолчанию: StringComparison.CurrentCulture.

Методы Char.ToUpper(Char) и Char.ToLower(Char) работают аналогично методам String.ToUpper() и String.ToLower(), описанным в предыдущем разделе.

String.StartsWith и String.EndsWith.

Интерпретация по умолчанию: StringComparison.CurrentCulture (если первый параметр является параметром string) или StringComparison.Ordinal (когда первый параметр является параметром char).

Существует несоответствие в том, как перегрузки по умолчанию этих методов выполняют сравнения. Перегрузки, принимающие char параметр, выполняют порядковое сравнение, но перегрузки, принимающие string параметр, выполняют сравнение, чувствительное к культуре, и могут игнорировать непечатные символы.

String.IndexOf и String.LastIndexOf.

Интерпретация по умолчанию: StringComparison.CurrentCulture.

Отсутствует согласованность в том, как эти методы выполняют сравнения при перегрузках по умолчанию. Все String.IndexOf и String.LastIndexOf методы, включающие Char параметр, выполняют порядковое сравнение, но методы по умолчанию String.IndexOf и String.LastIndexOf, включающие String параметр, выполняют сравнение с учетом культурных и региональных параметров.

При вызове метода String.IndexOf(String) или String.LastIndexOf(String) и передачи строки для поиска в нем в текущем экземпляре рекомендуется вызвать перегрузку, явно указывающую тип StringComparison. Перегрузки, включающие Char аргумент, не позволяют указывать StringComparison тип.

String.Contains

Интерпретация по умолчанию: StringComparison.Ordinal.

В отличие от String.IndexOf, метод String.Contains использует порядковое сравнение по умолчанию для обеих перегрузок char и string. Однако вы по-прежнему должны передавать явный аргумент, если важна точность намерения, чтобы сделать поведение понятным в месте вызова StringComparison.

и MemoryExtensions.AsSpan.IndexOfAny и тип SearchValues<T>

.NET 8 представил SearchValues<T> тип, который предоставляет оптимизированное решение для поиска определенных наборов символов или байтов в пределах диапазона.

Если вы сравниваете строку с фиксированным набором известных значений многократно, попробуйте использовать SearchValues<T>.Contains(T) метод вместо цепных сравнений или подходов на основе LINQ. SearchValues<T> может предварительно вычислять внутренние таблицы поиска и оптимизировать логику сравнения на основе предоставленных значений. Чтобы просмотреть преимущества производительности, создайте и кэшируйте SearchValues<string> экземпляр один раз, а затем повторно используйте его для сравнения:

using System.Buffers;

namespace ExampleCode;

internal partial class DemoCode
{
    private static readonly SearchValues<string> Commands =
        SearchValues.Create(
            ["start", "run", "go", "begin", "commence"],
            StringComparison.OrdinalIgnoreCase);

    void ProcessCommand(string command)
    {
        if (Commands.Contains(command))
        {
            // ...
        }
    }
}
Imports System.Buffers

Namespace ExampleCode
    Partial Friend Class DemoCode

        Private Shared ReadOnly Commands As SearchValues(Of String) =
            SearchValues.Create(
                {"start", "run", "go", "begin", "commence"},
                StringComparison.OrdinalIgnoreCase)

        Sub ProcessCommand(command As String)
            If Commands.Contains(command) Then
                ' ...
            End If
        End Sub

    End Class
End Namespace

В .NET 9 возможность SearchValues была расширена для поддержки поиска подстрок в более крупной строке. Пример см. в разделе SearchValues "Расширение".

Методы, которые выполняют сравнение строк косвенно

Некоторые нестроковые методы, имеющие сравнение строк в качестве центральной операции, используют StringComparer тип. Класс StringComparer включает шесть статических свойств, возвращающих StringComparer экземпляры, методы которых StringComparer.Compare выполняют следующие типы сравнений строк:

  • Сравнение строк с учетом текущих культурных и региональных параметров. Этот StringComparer объект возвращается свойством StringComparer.CurrentCulture .
  • Сравнения без учета регистра с использованием текущей культуры. Этот StringComparer объект возвращается свойством StringComparer.CurrentCultureIgnoreCase .
  • Сравнения без учета культурных особенностей с использованием правил сравнения слов инвариантной культуры. Этот StringComparer объект возвращается свойством StringComparer.InvariantCulture .
  • Сравнение без учета регистра и культуры с использованием правил сравнения слов инвариантной культуры. Этот StringComparer объект возвращается свойством StringComparer.InvariantCultureIgnoreCase .
  • Порядковое сравнение. Этот StringComparer объект возвращается свойством StringComparer.Ordinal .
  • Сравнение порядковых значений без учета регистра. Этот StringComparer объект возвращается свойством StringComparer.OrdinalIgnoreCase .

Array.Sort и Array.BinarySearch.

Интерпретация по умолчанию: StringComparison.CurrentCulture.

При хранении любых данных в коллекции или считывании сохранённых данных из файла или базы данных в коллекцию, переключение текущей культуры может привести к нарушению инвариантов в коллекции. Метод Array.BinarySearch предполагает, что элементы в массиве, которые будут искать, уже отсортированы. Чтобы отсортировать любой строковый элемент в массиве, Array.Sort метод вызывает String.Compare метод для упорядочивания отдельных элементов. Использование культурно-чувствительного средства сравнения может быть опасным, если культура изменяется между сортировкой массива и его поиском. Например, в следующем коде хранилище и извлечение работают с компаратором, который неявно предоставляется свойством Thread.CurrentThread.CurrentCulture. Если культура может меняться между вызовами StoreNames и DoesNameExist, и особенно если содержимое массива сохраняется где-либо между двумя вызовами метода, двоичный поиск может завершиться ошибкой.

// Incorrect
string[] _storedNames;

public void StoreNames(string[] names)
{
    _storedNames = new string[names.Length];

    // Copy the array contents into a new array
    Array.Copy(names, _storedNames, names.Length);

    Array.Sort(_storedNames); // Line A
}

public bool DoesNameExist(string name) =>
    Array.BinarySearch(_storedNames, name) >= 0; // Line B
' Incorrect
Dim _storedNames As String()

Sub StoreNames(names As String())
    ReDim _storedNames(names.Length - 1)

    ' Copy the array contents into a new array
    Array.Copy(names, _storedNames, names.Length)

    Array.Sort(_storedNames) ' Line A
End Sub

Function DoesNameExist(name As String) As Boolean
    Return Array.BinarySearch(_storedNames, name) >= 0 ' Line B
End Function

Рекомендуемый вариант отображается в следующем примере, который использует один и тот же порядковый метод сравнения (язык и региональные значения) как для сортировки, так и для поиска массива. Код изменения отражается в строках, помеченных Line A и Line B в двух примерах.

// Correct
string[] _storedNames;

public void StoreNames(string[] names)
{
    _storedNames = new string[names.Length];

    // Copy the array contents into a new array
    Array.Copy(names, _storedNames, names.Length);

    Array.Sort(_storedNames, StringComparer.Ordinal); // Line A
}

public bool DoesNameExist(string name) =>
    Array.BinarySearch(_storedNames, name, StringComparer.Ordinal) >= 0; // Line B
' Correct
Dim _storedNames As String()

Sub StoreNames(names As String())
    ReDim _storedNames(names.Length - 1)

    ' Copy the array contents into a new array
    Array.Copy(names, _storedNames, names.Length)

    Array.Sort(_storedNames, StringComparer.Ordinal) ' Line A
End Sub

Function DoesNameExist(name As String) As Boolean
    Return Array.BinarySearch(_storedNames, name, StringComparer.Ordinal) >= 0 ' Line B
End Function

Если эти данные сохраняются и перемещаются по языкам и региональным параметрам, а сортировка используется для представления этих данных пользователю, вы можете рассмотреть возможность использования StringComparison.InvariantCulture, которая работает лингвистически для улучшения выходных данных пользователей, но не влияет на изменения языка и региональных параметров. Следующий пример изменяет два предыдущих примера для использования инвариантной культуры при сортировке и поиске в массиве.

// Correct
string[] _storedNames;

public void StoreNames(string[] names)
{
    _storedNames = new string[names.Length];

    // Copy the array contents into a new array
    Array.Copy(names, _storedNames, names.Length);

    Array.Sort(_storedNames, StringComparer.InvariantCulture); // Line A
}

public bool DoesNameExist(string name) =>
    Array.BinarySearch(_storedNames, name, StringComparer.InvariantCulture) >= 0; // Line B
' Correct
Dim _storedNames As String()

Sub StoreNames(names As String())
    ReDim _storedNames(names.Length - 1)

    ' Copy the array contents into a new array
    Array.Copy(names, _storedNames, names.Length)

    Array.Sort(_storedNames, StringComparer.InvariantCulture) ' Line A
End Sub

Function DoesNameExist(name As String) As Boolean
    Return Array.BinarySearch(_storedNames, name, StringComparer.InvariantCulture) >= 0 ' Line B
End Function

Пример коллекций: Hashtable конструктор

Хэширование строк предоставляет второй пример операции, затронутой способом сравнения строк.

В следующем примере создается объект Hashtable путем передачи ему объекта StringComparer, который возвращается свойством StringComparer.OrdinalIgnoreCase. Поскольку класс StringComparer, производный от StringComparer, реализует интерфейс IEqualityComparer, его метод GetHashCode используется для вычисления хэш-кода строк в хэш-таблице.

using System.IO;
using System.Collections;

const int InitialCapacity = 100;

Hashtable creationTimeByFile = new(InitialCapacity, StringComparer.OrdinalIgnoreCase);
string directoryToProcess = Directory.GetCurrentDirectory();

// Fill the hash table
PopulateFileTable(directoryToProcess);

// Get some of the files and try to find them with upper cased names
foreach (var file in Directory.GetFiles(directoryToProcess))
    PrintCreationTime(file.ToUpper());


void PopulateFileTable(string directory)
{
    foreach (string file in Directory.GetFiles(directory))
        creationTimeByFile.Add(file, File.GetCreationTime(file));
}

void PrintCreationTime(string targetFile)
{
    object? dt = creationTimeByFile[targetFile];

    if (dt is DateTime value)
        Console.WriteLine($"File {targetFile} was created at time {value}.");
    else
        Console.WriteLine($"File {targetFile} does not exist.");
}
Imports System.IO

Module Program
    Const InitialCapacity As Integer = 100

    Private ReadOnly s_creationTimeByFile As New Hashtable(InitialCapacity, StringComparer.OrdinalIgnoreCase)
    Private ReadOnly s_directoryToProcess As String = Directory.GetCurrentDirectory()

    Sub Main()
        ' Fill the hash table
        PopulateFileTable(s_directoryToProcess)

        ' Get some of the files and try to find them with upper cased names
        For Each File As String In Directory.GetFiles(s_directoryToProcess)
            PrintCreationTime(File.ToUpper())
        Next
    End Sub

    Sub PopulateFileTable(directoryPath As String)
        For Each file As String In Directory.GetFiles(directoryPath)
            s_creationTimeByFile.Add(file, IO.File.GetCreationTime(file))
        Next
    End Sub

    Sub PrintCreationTime(targetFile As String)
        Dim dt As Object = s_creationTimeByFile(targetFile)

        If TypeOf dt Is Date Then
            Console.WriteLine($"File {targetFile} was created at time {DirectCast(dt, Date)}.")
        Else
            Console.WriteLine($"File {targetFile} does not exist.")
        End If
    End Sub
End Module

Пример коллекций: SortedSet<T> и List<T>.Sort

Та же проблема с чувствительностью локали возникает при создании экземпляра отсортированного набора строк или сортировке существующей коллекции строк. Всегда указывайте явный компаратор:

// Words to sort
string[] values = [ "able", "ångström", "apple", "Æble",
            "Windows", "Visual Studio" ];

//
// Potentially incorrect code - behavior might vary based on locale.
//
SortedSet<string> mySet = [.. values]; // No comparer specified

List<string> list = [.. values];
list.Sort(); // No comparer specified

//
// Corrected code - uses ordinal sorting; doesn't vary by locale.
//
SortedSet<string> mySet2 = new(values, StringComparer.Ordinal);

List<string> list2 = [.. values];
list2.Sort(StringComparer.Ordinal);
' Words to sort
Dim values As String() = {"able", "ångström", "apple", "Æble",
                          "Windows", "Visual Studio"}

'
' Potentially incorrect code - behavior might vary based on locale.
'
Dim mySet As New SortedSet(Of String)(values) ' No comparer specified

Dim list As New List(Of String)(values)
list.Sort() ' No comparer specified

'
' Corrected code - uses ordinal sorting; doesn't vary by locale.
'
Dim mySet2 As New SortedSet(Of String)(values, StringComparer.Ordinal)

Dim list2 As New List(Of String)(values)
list2.Sort(StringComparer.Ordinal)

Различия между .NET и .NET Framework

Платформа .NET и .NET Framework обрабатывают глобализацию по-разному. Платформа .NET Framework в Windows использует средство поддержки национальных языков (NLS) операционной системы для лингвистических сравнений строк. .NET использует библиотеку Международных компонентов для Юникода (ICU) для лингвистических сравнений строк на всех поддерживаемых платформах.

Так как ICU и NLS реализуют другую логику в лингвистических сравнениях, результаты строковых методов, использующих сравнение с учетом языка и региональных параметров, могут отличаться между .NET и .NET Framework. Это важно для любого метода, использующего лингвистическое сравнение по умолчанию, в том числе:

Примечание.

Это не исчерпывающий список затронутых API.

Одним из важных различий является обработка внедренных значений NULL и других символов управления. При использовании лингвистического сравнения в NLS некоторые управляющие символы, такие как null-символ (\0) могут рассматриваться как игнорируемые в определенных контекстах сравнения. В рамках ICU эти символы рассматриваются как фактические символы в строке. Это может привести string.IndexOf(string) к возврату различных результатов, если строка поиска содержит пустой символ.

Например, следующий код может создать другой ответ в зависимости от текущей среды выполнения:

const string greeting = "Hel\0lo";
Console.WriteLine($"{greeting.IndexOf("\0")}");

// The snippet prints:
//
// '3' when running on .NET Framework and .NET Core 2.x - 3.x (Windows)
// '0' when running on .NET 5 or later (Windows)
// '0' when running on .NET Core 2.x - 3.x or .NET 5 (non-Windows)
// '3' when running on .NET Core 2.x or .NET 5+ (in invariant mode)
Const greeting As String = "Hel" & vbNullChar & "lo"
Console.WriteLine($"{greeting.IndexOf(CStr(vbNullChar))}")

' The snippet prints:
'
' '3' when running on .NET Framework and .NET Core 2.x - 3.x (Windows)
' '0' when running on .NET 5 or later (Windows)
' '0' when running on .NET Core 2.x - 3.x or .NET 5 (non-Windows)
' '3' when running on .NET Core 2.x or .NET 5+ (in invariant mode)

Лучший способ избежать этих кроссплатформенных и кросс-реализационных сюрпризов — всегда передавать явный StringComparison аргумент в методы сравнения строк и использовать StringComparison.Ordinal или StringComparison.OrdinalIgnoreCase для нелингвистических сравнений.

Если вы переносите приложение из .NET Framework в .NET и используете устаревшие функции NLS в Windows, можно настроить приложение для использования NLS. Дополнительные сведения см. в статье о глобализации .NET и ICU.

См. также