Najlepsze rozwiązania dotyczące porównywania ciągów na platformie .NET

Platforma .NET zapewnia szeroką obsługę tworzenia zlokalizowanych i zglobalizowanych aplikacji oraz ułatwia stosowanie konwencji bieżącej kultury lub określonej kultury podczas wykonywania typowych operacji, takich jak sortowanie i wyświetlanie ciągów. Jednak sortowanie lub porównywanie ciągów nie zawsze jest operacją wrażliwą na kulturę. Na przykład ciągi używane wewnętrznie przez aplikację powinny być obsługiwane identycznie we wszystkich kulturach. Gdy dane ciągów niezależne kulturowo, takie jak tagi XML, tagi HTML, nazwy użytkowników, ścieżki plików i nazwy obiektów systemowych, są interpretowane tak, jakby były wrażliwe na kulturę, kod aplikacji może podlegać subtelnym usterce, niskiej wydajności i, w niektórych przypadkach, problemom z zabezpieczeniami.

Ten artykuł analizuje metody sortowania, porównywania i wielkości liter ciągów na platformie .NET, przedstawia zalecenia dotyczące wybierania odpowiedniej metody obsługi ciągów i zawiera dodatkowe informacje na temat metod obsługi ciągów.

Rekomendacje na potrzeby użycia ciągów

Podczas opracowywania za pomocą platformy .NET postępuj zgodnie z tymi zaleceniami podczas porównywania ciągów.

Napiwek

Różne metody związane z ciągami wykonują porównanie. Przykłady obejmują String.Equals, , String.IndexOfString.Comparei String.StartsWith.

Unikaj następujących rozwiązań podczas porównywania ciągów:

  • Nie używaj przeciążeń, które nie jawnie ani niejawnie nie określają reguł porównania ciągów dla operacji ciągów.
  • W większości przypadków nie używaj operacji na ciągach StringComparison.InvariantCulture . Jedną z niewielu wyjątków jest utrwalanie danych lingwistycznych, ale nieznajdujących się kulturowo.
  • Nie używaj przeciążenia String.Compare metody lub CompareTo i testuj wartość zwracaną zero, aby określić, czy dwa ciągi są równe.

Jawne określanie porównań ciągów

Większość metod manipulowania ciągami na platformie .NET jest przeciążona. Zazwyczaj co najmniej jedno przeciążenie akceptuje ustawienia domyślne, podczas gdy inne nie akceptują żadnych wartości domyślnych i zamiast tego definiują dokładny sposób porównywania ciągów lub manipulowania nimi. Większość metod, które nie opierają się na wartościach domyślnych, obejmuje parametr typu StringComparison, który jest wyliczeniem, które jawnie określa reguły porównywania ciągów według kultury i wielkości liter. W poniższej StringComparison tabeli opisano elementy członkowskie wyliczenia.

Element członkowski StringComparison opis
CurrentCulture Wykonuje porównanie z uwzględnieniem wielkości liter przy użyciu bieżącej kultury.
CurrentCultureIgnoreCase Wykonuje porównanie bez uwzględniania wielkości liter przy użyciu bieżącej kultury.
InvariantCulture Wykonuje porównanie uwzględniające wielkość liter przy użyciu niezmiennej kultury.
InvariantCultureIgnoreCase Wykonuje porównanie bez uwzględniania wielkości liter przy użyciu niezmiennej kultury.
Ordinal Wykonuje porównanie porządkowe.
OrdinalIgnoreCase Wykonuje porównanie porządkowe bez uwzględniania wielkości liter.

Na przykład IndexOf metoda, która zwraca indeks podciągów w String obiekcie, który pasuje do znaku lub ciągu, ma dziewięć przeciążeń:

Zalecamy wybranie przeciążenia, które nie używa wartości domyślnych z następujących powodów:

  • Niektóre przeciążenia z parametrami domyślnymi (te, które wyszukują Char element w wystąpieniu ciągu) wykonują porównanie porządkowe, natomiast inne (te, które wyszukują ciąg w wystąpieniu ciągu) są wrażliwe na kulturę. Trudno jest pamiętać, która metoda używa wartości domyślnej i łatwo mylić przeciążenia.

  • Intencja kodu, który opiera się na wartościach domyślnych wywołań metod, nie jest jasna. W poniższym przykładzie, który opiera się na wartościach domyślnych, trudno jest wiedzieć, czy deweloper rzeczywiście zamierzał porządkowy, czy językowy porównanie dwóch ciągów, czy też różnica wielkości liter między url.Scheme i "https" może spowodować, że test równości zwróci falsewartość .

    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
    

Ogólnie rzecz biorąc, zalecamy wywołanie metody, która nie opiera się na wartościach domyślnych, ponieważ sprawia, że intencja kodu jest jednoznaczna. To z kolei sprawia, że kod jest bardziej czytelny i łatwiejszy do debugowania i konserwacji. W poniższym przykładzie przedstawiono odpowiedzi na pytania dotyczące poprzedniego przykładu. Jasno widać, że jest używane porównanie porządkowe i że różnice w przypadku są ignorowane.

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

Szczegóły porównania ciągów

Porównanie ciągów jest sercem wielu operacji związanych z ciągami, szczególnie sortowania i testowania pod kątem równości. Ciągi sortowane w określonej kolejności: jeśli wyrażenie "my" pojawi się przed ciągiem w posortowanej liście ciągów, "my" musi porównać wartość mniejszą niż lub równą "ciągowi". Ponadto porównanie niejawnie definiuje równość. Operacja porównania zwraca zero dla ciągów, które uznaje za równe. Dobrą interpretacją jest to, że żaden ciąg nie jest mniejszy niż drugi. Najbardziej znaczące operacje obejmujące ciągi obejmują jedną lub obie z tych procedur: porównywanie z innym ciągiem i wykonywanie dobrze zdefiniowanej operacji sortowania.

Uwaga

Tabele wagi sortowania, zestaw plików tekstowych zawierających informacje o wagach znaków używanych w operacjach sortowania i porównywania dla systemów operacyjnych Windows oraz domyślną tabelę elementów sortowania Unicode, najnowszą wersję tabeli wagi sortowania dla systemów Linux i macOS. Określona wersja tabeli wagi sortowania w systemach Linux i macOS zależy od wersji międzynarodowych składników bibliotek Unicode zainstalowanych w systemie. Aby uzyskać informacje na temat wersji ICU i implementowanych wersji Unicode, zobacz Pobieranie ICU.

Jednak ocena dwóch ciągów dla równości lub kolejności sortowania nie daje jednego, poprawnego wyniku; wynik zależy od kryteriów używanych do porównywania ciągów. W szczególności porównania ciągów, które są porządkowe lub oparte na konwencji wielkości liter i sortowania bieżącej kultury lub niezmiennej kultury (kultury niezależnej od ustawień regionalnych na podstawie języka angielskiego) mogą generować różne wyniki.

Ponadto porównania ciągów przy użyciu różnych wersji platformy .NET lub platformy .NET w różnych systemach operacyjnych lub wersjach systemu operacyjnego mogą zwracać różne wyniki. Aby uzyskać więcej informacji, zobacz Ciągi i Standard Unicode.

Porównania ciągów używające bieżącej kultury

Jednym z kryteriów jest użycie konwencji bieżącej kultury podczas porównywania ciągów. Porównania oparte na bieżącej kulturze używają bieżącej kultury wątku lub ustawień regionalnych. Jeśli kultura nie jest ustawiana przez użytkownika, domyślnie jest to ustawienie systemu operacyjnego. Zawsze należy używać porównań opartych na bieżącej kulturze, gdy dane są istotne lingwistyczne i kiedy odzwierciedlają interakcję z użytkownikami wrażliwymi na kulturę.

Jednak porównanie i zachowanie wielkości liter na platformie .NET zmienia się po zmianie kultury. Dzieje się tak, gdy aplikacja jest wykonywana na komputerze, który ma inną kulturę niż komputer, na którym utworzono aplikację, lub gdy wątek wykonujący zmienia jego kulturę. To zachowanie jest zamierzone, ale nie jest oczywiste dla wielu deweloperów. W poniższym przykładzie przedstawiono różnice w kolejności sortowania między kulturami języka angielskiego (en-US) i szwedzkiego ("sv-SE"). Należy pamiętać, że wyrazy "ångström", "Windows" i "Visual Studio" są wyświetlane w różnych pozycjach w posortowanych tablicach ciągów.

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

Porównania bez uwzględniania wielkości liter, które używają bieżącej kultury, są takie same jak porównania wrażliwe na kulturę, z tą różnicą, że ignorują wielkość liter zgodnie z bieżącą kulturą wątku. To zachowanie może również manifestować się w kolejności sortowania.

Porównania korzystające z semantyki bieżącej kultury są domyślne dla następujących metod:

W każdym razie zalecamy wywołanie przeciążenia, które ma StringComparison parametr w celu wyczyszczenia intencji wywołania metody.

Subtelne i nie tak subtelne błędy mogą pojawić się, gdy dane ciągów nielingwistycznych są interpretowane lingwistyczne lub gdy dane ciągów z określonej kultury są interpretowane przy użyciu konwencji innej kultury. Przykładem kanonicznym jest problem Turecki-I.

W przypadku prawie wszystkich alfabetów łacińskich, w tym angielskiego w Stanach Zjednoczonych, znak "i" (\u0069) jest małą wersją znaku "I" (\u0049). Ta reguła wielkości liter szybko staje się wartością domyślną dla kogoś programistycznego w takiej kulturze. Jednak alfabet turecki ("tr-TR") zawiera znak "I z kropką" "İ" (\u0130), czyli wersję kapitacyjną "i". Język turecki zawiera również małe litery "i bez kropki", "ı" (\u0131), które wielką literą to "I". To zachowanie występuje również w kulturze Azerbejdżanu ("az").

W związku z tym założenia dotyczące wielkości liter "i" lub małych liter "I" nie są prawidłowe we wszystkich kulturach. Jeśli używasz domyślnych przeciążeń dla procedur porównywania ciągów, będą one podlegać wariancji między kulturami. Jeśli dane do porównania nie są językowe, użycie domyślnych przeciążeń może spowodować niepożądane wyniki, ponieważ poniższa próba wykonania porównania bez uwzględniania wielkości liter ciągów "bill" i "BILL" ilustruje.

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

To porównanie może spowodować znaczne problemy, jeśli kultura jest przypadkowo używana w ustawieniach poufnych zabezpieczeń, jak w poniższym przykładzie. Wywołanie metody, takie jak IsFileURI("file:") zwraca true , jeśli bieżąca kultura jest angielska w USA, ale false jeśli bieżąca kultura jest turecka. W związku z tym w tureckich systemach ktoś może obejść środki bezpieczeństwa, które blokują dostęp do identyfikatorów URI bez uwzględniania wielkości liter, które zaczynają się od "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

W takim przypadku, ponieważ "plik:" ma być interpretowany jako identyfikator nielingwistyczny, niewrażliwy na kulturę, kod powinien być zamiast tego napisany, jak pokazano w poniższym przykładzie:

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

Operacje porządkowych ciągów

Określenie StringComparison.Ordinal wartości lub StringComparison.OrdinalIgnoreCase w wywołaniu metody oznacza porównanie nielingwistyczne, w którym funkcje języków naturalnych są ignorowane. Metody wywoływane przy użyciu tych StringComparison wartości podstawowych decyzji dotyczących operacji ciągów w prostych porównaniach bajtów zamiast tabel wielkości liter lub równoważności, które są sparametryzowane przez kulturę. W większości przypadków takie podejście najlepiej pasuje do zamierzonej interpretacji ciągów przy jednoczesnym szybszym i bardziej niezawodnym kodzie.

Porównania porządkowe to porównania ciągów, w których każdy bajt każdego ciągu jest porównywany bez interpretacji językowej; na przykład "windows" nie pasuje do "Windows". Jest to zasadniczo wywołanie funkcji środowiska uruchomieniowego strcmp języka C. Użyj tego porównania, gdy kontekst określa, że ciągi powinny być dokładnie dopasowane lub wymaga konserwatywnych zasad dopasowania. Ponadto porównywanie porządkowe jest najszybszą operacją porównania, ponieważ nie stosuje żadnych reguł językowych podczas określania wyniku.

Ciągi na platformie .NET mogą zawierać osadzone znaki null (i inne znaki niedrukowane). Jedną z najczystszych różnic między porównaniem porządkowym i uwzględniającym kulturę (w tym porównaniami używającymi niezmiennej kultury) jest obsługa osadzonych znaków null w ciągu. Te znaki są ignorowane podczas używania String.Compare metod i String.Equals do przeprowadzania porównań wrażliwych na kulturę (w tym porównań używających niezmiennej kultury). W związku z tym ciągi zawierające osadzone znaki null mogą być traktowane jako równe ciągom, które nie są. Osadzone znaki niedrukowane mogą być pomijane w celu porównywania ciągów, takich jak String.StartsWith.

Ważne

Mimo że metody porównania ciągów ignorują osadzone znaki null, metody wyszukiwania ciągów, takie jak String.Contains, String.EndsWith, String.IndexOf, String.LastIndexOfi String.StartsWith nie.

Poniższy przykład wykonuje porównanie z uwzględnieniem kultury ciągu "Aa" z podobnym ciągiem zawierającym kilka osadzonych znaków null między znakami "A" i "a" i pokazuje, jak dwa ciągi są traktowane jako równe:

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

Jednak ciągi nie są traktowane jako równe w przypadku użycia porównania porządkowego, jak pokazano w poniższym przykładzie:

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

Porównania porządkowe bez uwzględniania wielkości liter to następne najbardziej konserwatywne podejście. Te porównania ignorują większość wielkości liter; na przykład "windows" pasuje do "Windows". W przypadku czynienia z znakami ASCII ta zasada jest równoważna StringComparison.Ordinal, z tą różnicą, że ignoruje zwykłe wielkości liter ASCII. W związku z tym każdy znak w [A, Z] (\u0041-\u005A) pasuje do odpowiedniego znaku w [a,z] (\u0061-\007A). Wielkość liter poza zakresem ASCII używa niezmiennych tabel kultury. W związku z tym następujące porównanie:

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

jest równoważne (ale szybciej niż) to porównanie:

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

Te porównania są nadal bardzo szybkie.

Zarówno StringComparison.Ordinal , jak i StringComparison.OrdinalIgnoreCase używają wartości binarnych bezpośrednio i najlepiej nadają się do dopasowywania. Jeśli nie masz pewności co do ustawień porównania, użyj jednej z tych dwóch wartości. Jednak ze względu na to, że wykonują porównanie bajtów po bajtach, nie są sortowane według kolejności sortowania językowego (na przykład słownika angielskiego), ale według kolejności sortowania binarnego. Wyniki mogą wyglądać dziwnie w większości kontekstów, jeśli są wyświetlane użytkownikom.

Semantyka porządkowa jest wartością domyślną dla String.Equals przeciążeń, które nie zawierają argumentu StringComparison (w tym operatora równości). W każdym razie zalecamy wywołanie przeciążenia, które ma StringComparison parametr .

Operacje na ciągach używające niezmiennej kultury

Porównania z niezmienną kulturą używają CompareInfo właściwości zwracanej przez właściwość statyczną CultureInfo.InvariantCulture . To zachowanie jest takie samo we wszystkich systemach; Tłumaczy wszystkie znaki poza zakresem na to, co uważa za równoważne niezmienne znaki. Te zasady mogą być przydatne do obsługi jednego zestawu zachowań ciągów w różnych kulturach, ale często zapewniają nieoczekiwane wyniki.

Porównania bez uwzględniania wielkości liter z niezmienną kulturą używają właściwości statycznej zwracanej przez właściwość statyczną CompareInfo na potrzeby informacji porównawczych CultureInfo.InvariantCulture . Wszelkie różnice wielkości liter między tymi przetłumaczonymi znakami są ignorowane.

Porównania, które używają StringComparison.InvariantCulture ciągów ASCII i StringComparison.Ordinal działają identycznie. Jednak podejmuje decyzje językowe, StringComparison.InvariantCulture które mogą nie być odpowiednie dla ciągów, które muszą być interpretowane jako zestaw bajtów. Obiekt sprawia, CultureInfo.InvariantCulture.CompareInfoCompare że metoda interpretuje niektóre zestawy znaków jako równoważne. Na przykład następująca równoważność jest prawidłowa w niezmiennej kulturze:

InvariantCulture: a + ̊ = å

MAŁA LITERA ŁACIŃSKA znak "a" (\u0061), gdy znajduje się obok znaku "+ "+ " ̊" (\u030a), jest interpretowany jako MAŁA LITERA ŁACIŃSKA A Z PIERŚCIENIEM POWYŻEJ znaku "å" (\u00e5). Jak pokazano w poniższym przykładzie, to zachowanie różni się od porównania porządkowego.

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

Podczas interpretowania nazw plików, plików cookie lub innych elementów, w których może pojawić się kombinacja, taka jak "å", porównania porządkowe nadal oferują najbardziej przejrzyste i dopasowane zachowanie.

W równowadze niezmienna kultura ma kilka właściwości, które ułatwiają porównywanie. Porównuje się w sposób językowy, co uniemożliwia zagwarantowanie pełnej równoważności symbolicznej, ale nie jest to wybór do wyświetlania w żadnej kulturze. Jednym z niewielu powodów do porównania StringComparison.InvariantCulture jest utrwalanie uporządkowanych danych dla wyświetlacza identycznego między kulturowo. Jeśli na przykład duży plik danych zawierający listę posortowanych identyfikatorów do wyświetlania towarzyszy aplikacji, dodanie do tej listy wymaga wstawiania z sortowaniem w stylu niezmiennym.

Wybieranie elementu członkowskiego StringComparison dla wywołania metody

W poniższej tabeli przedstawiono mapowanie z kontekstu ciągu semantycznego na składową StringComparison wyliczenia:

Data Zachowanie Odpowiedni plik System.StringComparison

wartość
Identyfikatory wewnętrzne z uwzględnieniem wielkości liter.

Identyfikatory z uwzględnieniem wielkości liter w standardach, takich jak XML i HTTP.

Ustawienia związane z zabezpieczeniami uwzględniające wielkość liter.
Identyfikator nielingwistyczny, w którym bajty są dokładnie zgodne. Ordinal
Identyfikatory wewnętrzne bez uwzględniania wielkości liter.

Identyfikatory bez uwzględniania wielkości liter w standardach, takich jak XML i HTTP.

Ścieżki plików.

Klucze rejestru i wartości.

Zmienne środowiskowe.

Identyfikatory zasobów (na przykład obsługują nazwy).

Ustawienia związane z zabezpieczeniami bez uwzględniania wielkości liter.
Identyfikator nielingwistyczny, gdzie wielkość liter jest nieistotna. OrdinalIgnoreCase
Niektóre utrwalone, językowe dane.

Wyświetlanie danych językowych, które wymagają stałego porządku sortowania.
Dane niezależne od kultury, które nadal mają znaczenie językowe. InvariantCulture

— lub —

InvariantCultureIgnoreCase
Dane wyświetlane użytkownikowi.

Większość danych wejściowych użytkownika.
Dane, które wymagają lokalnych zwyczajów językowych. CurrentCulture

— lub —

CurrentCultureIgnoreCase

Typowe metody porównywania ciągów na platformie .NET

W poniższych sekcjach opisano metody, które są najczęściej używane do porównywania ciągów.

Funkcja String.Compare

Interpretacja domyślna: StringComparison.CurrentCulture.

Ponieważ najbardziej centralna operacja do interpretacji ciągu, należy zbadać wszystkie wystąpienia tych wywołań metody w celu określenia, czy ciągi powinny być interpretowane zgodnie z bieżącą kulturą, czy też odsunięty od kultury (symbolicznie). Zazwyczaj jest to drugie, a StringComparison.Ordinal zamiast tego należy użyć porównania.

Klasa System.Globalization.CompareInfo , która jest zwracana przez CultureInfo.CompareInfo właściwość, zawiera również metodę Compare , która udostępnia dużą liczbę pasujących opcji (porządkowych, ignorujących biały znak, ignorując typ kana itd.) za pomocą CompareOptions wyliczenia flagi.

Funkcja String.CompareTo

Interpretacja domyślna: StringComparison.CurrentCulture.

Ta metoda nie oferuje obecnie przeciążenia określającego StringComparison typ. Zazwyczaj można przekonwertować tę metodę na zalecany String.Compare(String, String, StringComparison) formularz.

Typy implementujące IComparable interfejsy i IComparable<T> implementują tę metodę. Ponieważ nie oferuje opcji parametru StringComparison , implementacja typów często pozwala użytkownikowi określić StringComparer element w konstruktorze. W poniższym przykładzie zdefiniowano klasę FileName , której konstruktor klasy zawiera StringComparer parametr. Ten StringComparer obiekt jest następnie używany w metodzie 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

Funkcja String.Equals

Interpretacja domyślna: StringComparison.Ordinal.

Klasa String umożliwia testowanie pod kątem równości przez wywołanie przeciążeń metody statycznej lub wystąpienia Equals lub przy użyciu operatora równości statycznej. Przeciążenia i operator domyślnie używają porównania porządkowego. Jednak nadal zalecamy wywołanie przeciążenia, które jawnie określa StringComparison typ, nawet jeśli chcesz wykonać porównanie porządkowe. Ułatwia to wyszukiwanie kodu dla określonej interpretacji ciągu.

String.ToUpper i String.ToLower

Interpretacja domyślna: StringComparison.CurrentCulture.

Należy zachować ostrożność podczas używania String.ToUpper() metod i String.ToLower() , ponieważ wymuszanie ciągu na wielkie lub małe litery jest często używane jako mała normalizacja do porównywania ciągów niezależnie od wielkości liter. Jeśli tak, rozważ użycie porównania bez uwzględniania wielkości liter.

Dostępne String.ToUpperInvariant są również metody i String.ToLowerInvariant . ToUpperInvariant to standardowy sposób normalizacji wielkości liter. Porównania wykonywane przy użyciu StringComparison.OrdinalIgnoreCase są behawioralnie kompozycją dwóch wywołań: wywoływanie obu argumentów ciągów i porównywanie ToUpperInvariant przy użyciu metody StringComparison.Ordinal.

Przeciążenia są również dostępne do konwersji na wielkie i małe litery w określonej kulturze, przekazując CultureInfo obiekt reprezentujący tę kulturę do metody.

Char.ToUpper i Char.ToLower

Interpretacja domyślna: StringComparison.CurrentCulture.

Metody Char.ToUpper(Char) i Char.ToLower(Char) działają podobnie do String.ToUpper() metod i String.ToLower() opisanych w poprzedniej sekcji.

String.StartsWith i String.EndsWith

Interpretacja domyślna: StringComparison.CurrentCulture.

Domyślnie obie te metody wykonują porównanie wrażliwe na kulturę. W szczególności mogą ignorować znaki niedrukowane.

String.IndexOf i String.LastIndexOf

Interpretacja domyślna: StringComparison.CurrentCulture.

Brak spójności w sposobie wykonywania porównań przez domyślne przeciążenia tych metod. Wszystkie String.IndexOf metody i String.LastIndexOf , które zawierają Char parametr, wykonują porównanie porządkowe, ale domyślne String.IndexOf i String.LastIndexOf metody, które zawierają String parametr, wykonują porównanie wrażliwe na kulturę.

Jeśli wywołasz metodę String.IndexOf(String) lub String.LastIndexOf(String) i przekażesz ciąg do lokalizacji w bieżącym wystąpieniu, zalecamy wywołanie przeciążenia, które jawnie określa StringComparison typ. Przeciążenia, które zawierają Char argument, nie umożliwiają określenia StringComparison typu.

Metody, które pośrednio wykonują porównanie ciągów

Niektóre metody inne niż ciągi, które mają porównanie ciągów jako centralna operacja, używają StringComparer typu . Klasa StringComparer zawiera sześć właściwości statycznych, które zwracają StringComparer wystąpienia, których StringComparer.Compare metody wykonują następujące typy porównań ciągów:

Array.Sort i Array.BinarySearch

Interpretacja domyślna: StringComparison.CurrentCulture.

W przypadku przechowywania jakichkolwiek danych w kolekcji lub odczytywania utrwalonych danych z pliku lub bazy danych do kolekcji przełączenie bieżącej kultury może unieważnić niezmienne elementy w kolekcji. Metoda Array.BinarySearch zakłada, że elementy w tablicy do przeszukania są już sortowane. Aby posortować dowolny element ciągu w tablicy, Array.Sort metoda wywołuje metodę String.Compare w celu uporządkowania poszczególnych elementów. Użycie porównania wrażliwego na kulturę może być niebezpieczne, jeśli kultura zmienia się między czasem sortowania tablicy a jej zawartością. Na przykład w poniższym kodzie magazyn i pobieranie działają na porównującym, który jest dostarczany niejawnie przez Thread.CurrentThread.CurrentCulture właściwość . Jeśli kultura może ulec zmianie między wywołaniami do i DoesNameExist, a zwłaszcza jeśli zawartość tablicy jest utrwalana gdzieś między dwoma wywołaniami StoreNames metody, wyszukiwanie binarne może zakończyć się niepowodzeniem.

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

Zalecana odmiana jest wyświetlana w poniższym przykładzie, która używa tej samej metody porównania porządkowej (niewrażliwej na kulturę) zarówno do sortowania, jak i do przeszukiwania tablicy. Kod zmiany jest odzwierciedlany w wierszach oznaczonych Line A etykietami i Line B w dwóch przykładach.

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

Jeśli te dane są utrwalane i przenoszone między kulturami, a sortowanie jest używane do prezentowania tych danych użytkownikowi, możesz rozważyć użycie elementu StringComparison.InvariantCulture, który działa językowo w celu uzyskania lepszych danych wyjściowych użytkownika, ale nie ma to wpływu na zmiany w kulturze. Poniższy przykład modyfikuje dwa poprzednie przykłady, aby używać niezmiennej kultury do sortowania i przeszukiwania tablicy.

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

Przykład kolekcji: konstruktor hashtable

Skróty ciągów udostępnia drugi przykład operacji, która ma wpływ na sposób porównywania ciągów.

Poniższy przykład tworzy wystąpienie Hashtable obiektu, przekazując go StringComparer do obiektu zwróconego StringComparer.OrdinalIgnoreCase przez właściwość . Ponieważ klasa StringComparer pochodząca z StringComparer implementuje IEqualityComparer interfejs, jego GetHashCode metoda służy do obliczania kodu skrótu ciągów w tabeli skrótów.

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

Zobacz też