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


Рекомендации по сравнению строк в .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 Описание
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 изменяется при изменении культурных параметров. Это происходит, когда приложение выполняется на компьютере с другим языком и региональными параметрами, чем на компьютере, на котором было разработано приложение, или при изменении языка и региональных параметров выполнения потока. Это поведение намеренно, но оно остается неясным для многих разработчиков. В следующем примере показаны различия в порядке сортировки в культурах США (английский) ("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. Используйте это сравнение, если контекст диктует соответствие строк точно или требует консервативной политики сопоставления. Кроме того, порядковое сравнение является самой быстрой операцией сравнения, так как она не применяет лингвистические правила при определении результата.

Строки в .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 .

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

Сравнения с инвариантной культурой используют свойство 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

Распространенные методы сравнения строк в .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.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

См. также