Рекомендации по сравнению строк в .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 метод и проверьте возвращаемое значение нуля, чтобы определить, равны ли две строки.

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

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

Элемент StringComparison Description
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. Кроме того, неявное сравнение определяет равенство. Операция сравнения возвращает 0 для строк, которые она считает равными. Правильно интерпретировать это следующим образом: ни одна из строк не меньше другой. Наиболее значимые операции со строками включают обе следующие процедуры или хотя бы одну из них: сравнение с другой строкой и выполнение правильно определенной операции сортировки.

Примечание.

Можно скачать таблицы коэффициентов сортировки — набор текстовых файлов, которые содержат сведения о весовых коэффициентах символов, используемых в операциях сортировки и сравнения для операционных систем Windows, а также последнюю версию таблицы параметров сортировки по умолчанию для элементов Юникод — таблицу весовых коэффициентов сортировки для Linux и macOS. Конкретная версия таблицы коэффициентов сортировки в Linux и macOS зависит от установленной в системе версии библиотек International Components for Unicode (ICU). Сведения о версиях ICU и реализуемых в них версиях Юникода см. на странице Downloading ICU (Скачивание ICU).

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

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

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

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

Однако поведение сравнения и использования регистра в .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 , чтобы сделать назначение вызова метода очевидным.

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

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

Поэтому предположения, сделанные о прописной букве "i" или нижней части "I" не являются допустимыми среди всех культур. При использовании перегрузок по умолчанию для сравнения строк они будут меняться в зависимости от языков и региональных параметров. Если сравниваемые данные являются нелингвистическими, использование перегрузок по умолчанию может привести к нежелательным результатам, так как следующая попытка выполнить нечувствительное сравнение строк "счет" и "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. Используйте такое сравнение, когда контекст определяет, что строки должны точно совпадать, или требует использования консервативной политики соответствия. Кроме того, порядковое сравнение — это самая быстрая операция сравнения, потому что при расчете результата не применяются лингвистические правила.

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

Внимание

Несмотря на то что в методах сравнения строк не учитываются внедренные символы null, методы поиска строк, такие как String.Contains, String.EndsWith, String.IndexOf, String.LastIndexOfи String.StartsWith , эти символы учитывают.

В следующем примере выполняется сравнение с учетом языка и региональных параметров строки Aa с аналогичной строкой, содержащей несколько внедренных символов NULL между А и а, и показано, почему две строки рассматриваются как равные:

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 .

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

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

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

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

InvariantCulture: a + ̊ = å

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

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

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
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:

Data Поведение Соответствующее сравнение System.StringComparison

значение
Внутренние идентификаторы с учетом регистра

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

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

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

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

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

среды.

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

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

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

–или–

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

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

–или–

CurrentCultureIgnoreCase

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

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

String.Compare

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

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

Класс System.Globalization.CompareInfo , возвращаемый свойством CultureInfo.CompareInfo , также включает метод Compare , предоставляющий большое количество соответствующих параметров (порядковый, игнорирование пробела, игнорирование типа каны и т. д.) посредством перечисления флага 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.ToLower(Char) Методы Char.ToUpper(Char) работают аналогично String.ToUpper() методам, String.ToLower() описанным в предыдущем разделе.

String.StartsWith и String.EndsWith

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

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

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 тип.

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

Некоторые нестроковые методы, основным назначением которых является сравнение строк, используют тип 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

См. также