Udostępnij za pośrednictwem


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.

Zalecenia dotyczące użycia ciągów

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

Wskazówka

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

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

  • Nie używaj przeciążeń, które nie określają wprost ani pośrednio reguł porównania ciągów dla operacji ciągów.
  • W większości przypadków nie używaj operacji na ciągach opartych na StringComparison.InvariantCulture. Jednym z niewielu wyjątków jest persystowanie danych lingwistycznych, ale niezależnych od kultury.
  • Nie używaj przeciążeń metod String.Compare lub CompareTo i nie testuj wartości zwracanej 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.

Członek 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 leksykograficzne 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 w wystąpieniu ciągu) wykonują porównanie porządkowe, natomiast inne (te, które wyszukują ciąg w wystąpieniu ciągu) uwzględniają ustawienia regionalne. Trudno jest zapamiętać, która metoda używa której wartości domyślnej, i łatwo pomylić te 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 stwierdzić, czy deweloper rzeczywiście zamierzał porównanie porządkowe czy językowe dwóch ciągów, czy różnica wielkości liter między url.Scheme a "https" może spowodować, że test równości zwróci false wartość.

    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 są sortowane w określonej kolejności: jeśli ciąg "my" pojawia się przed "ciągiem" w posortowanej liście ciągów, "my" musi być mniejszy lub równy "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

Możesz pobrać 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, czyli najnowszą wersję tabeli wag 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ących 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 korzystają z bieżącej kultury wątku lub jego 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 mają znaczenie lingwistyczne i kiedy odzwierciedlają interakcję użytkownika uwzględniającą wrażliwość kulturową.

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 używającymi języka angielskiego w USA ("en-US") a kulturą szwedzką ("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ż przejawiać się w sposobie 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 parametr StringComparison, aby wyjaśnić intencję 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 Turkish-I.

W przypadku prawie wszystkich alfabetów łacińskich, w tym angielskiego w Usa, 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 turecki ("tr-TR") alfabet zawiera znak "I z kropką" "İ" (\u0130), czyli wielką literę "i". Język turecki zawiera również małą literę "i bez kropki", "ı" (\u0131), której wersją wielką jest "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 są niejęzykowe, użycie domyślnych przeciążeń może prowadzić do niepożądanych wyników, co ilustruje poniższa próba porównania bez rozróżniania wielkości liter ciągów "bill" 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

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 obecna kultura jest angloamerykańska, ale false, jeśli obecna 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 z tymi wartościami StringComparison opierają decyzje dotyczące operacji na ciągach znaków na prostych porównaniach bajtów danych, zamiast na tabelach wielkości liter lub równoważnościach sparametryzowanych 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 w .NET mogą zawierać wbudowane znaki zerowe (i inne znaki niedrukowane). Jedną z najbardziej wyraźnych różnic między porównaniem porządkowym a uwzględniającym kulturę (w tym porównaniami z użyciem niezmiennej kultury) jest obsługa osadzonych znaków zerowych w ciągu. Te znaki są ignorowane podczas używania metod String.Compare i String.Equals do przeprowadzania porównań uwzględniających różnice kulturowe (w tym porównań z użyciem 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 zawierają. Osadzone niedrukowane znaki mogą być pomijane w celu stosowania metod 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". Podczas pracy z znakami ASCII ta zasada jest równoważna StringComparison.Ordinal, z tą różnicą, że ignoruje standardowe wielkości liter ASCII. W związku z tym dowolny 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 stanowi domyślną wartość w przypadku przeciążeń String.Equals, które nie uwzględniają argumentu StringComparison (w tym operator równości). W każdym razie zalecamy wywołanie przeciążenia, które posiada parametr StringComparison.

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

Porównania z kulturą niezmienną używają właściwości CompareInfo, którą zwraca właściwość statyczna 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ą również wykorzystują statyczną właściwość CompareInfo zwróconą przez statyczną właściwość CultureInfo.InvariantCulture na potrzeby informacji porównawczych. Wszelkie różnice wielkości liter między tymi przetłumaczonymi znakami są ignorowane.

Porównania, które używają StringComparison.InvariantCulture i StringComparison.Ordinal, działają identycznie na ciągach ASCII. Jednak StringComparison.InvariantCulture podejmuje decyzje językowe, które mogą nie być odpowiednie dla ciągów znaków, które muszą być interpretowane jako zestaw bajtów. Obiekt CultureInfo.InvariantCulture.CompareInfo sprawia, że metoda Compare 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 "+ PIERŚCIEŃ POWYŻEJ" "+ " ̊" (\u030a), jest interpretowany jako MAŁA LITERA ŁACIŃSKA LITERA 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 {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

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.

Ogólnie rzecz biorąc, niezmienna kultura ma niewiele właściwości, które służą do porównywania. Dokonuje porównania w sposób istotny językowo, co uniemożliwia zagwarantowanie pełnej równoważności symbolicznej, ale nie nadaje się do wyświetlania w żadnej kulturze. Jednym z niewielu powodów do użycia StringComparison.InvariantCulture jako porównanie jest zachowanie uporządkowanych danych dla wyświetlania zgodnego międzykulturowo. Na przykład, jeśli dużemu plikowi danych zawierającemu listę posortowanych identyfikatorów do wyświetlania towarzyszy aplikacja, to dodanie do tej listy wymaga wstawienia z sortowaniem w niezmiennym stylu.

Wybieranie członka StringComparison dla wywołania metody

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

Dane Zachowanie Odpowiadający System.StringComparison

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

Identyfikatory rozróżniające wielkość liter w standardach takich jak XML i HTTP.

Ustawienia związane z bezpieczeństwem uwzględniające rozróżnianie wielkości liter.
Identyfikator nielingwistyczny, w którym bajty są dokładnie zgodne. Ordinal
Identyfikatory wewnętrzne niebiorące pod uwagę wielkości liter.

Identyfikatory niewrażliwe na wielkość 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 dane językowe, które pozostały istotne.

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.

PorównajCiągi

Interpretacja domyślna: StringComparison.CurrentCulture.

Jako operacja najbardziej związana z interpretacją ciągów, należy zbadać wszystkie wystąpienia tych wywołań metod w celu określenia, czy ciągi powinny być interpretowane zgodnie z tą bieżącą kulturą, czy też odsunięte 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 właściwość CultureInfo.CompareInfo, zawiera również metodę Compare, która udostępnia dużą liczbę opcji dopasowania (kolejności, ignorowania spacji, ignorowania typu kana itd.) za pomocą wyliczenia flag CompareOptions.

String.CompareTo

Interpretacja domyślna: StringComparison.CurrentCulture.

Ta metoda nie oferuje obecnie przeciążenia dla typu StringComparison. Zazwyczaj można przekonwertować tę metodę na zalecany String.Compare(String, String, StringComparison) formularz.

Typy, które implementują interfejsy IComparable i IComparable<T>, implementują tę metodę. Ponieważ nie daje możliwości użycia parametru StringComparison, implementacje typów często pozwalają użytkownikowi określić StringComparer 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

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 metody instancji Equals, lub przy użyciu statycznego operatora porównania. Przeciążenia i operator domyślnie używają porównania porządkowego. Jednak nadal zalecamy, aby wywołać przeciążenie, które jawnie określa typ StringComparison, nawet jeśli chcesz przeprowadzić porównanie porządkowe. Ułatwia to przeszukiwanie kodu w poszukiwaniu określonej interpretacji ciągu znaków.

Metoda String.ToUpper i metoda String.ToLower

Interpretacja domyślna: StringComparison.CurrentCulture.

Należy zachować ostrożność podczas używania metod String.ToUpper() i String.ToLower(), ponieważ zamiana ciągu znaków na wielkie lub małe litery jest często stosowana jako prosta 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 są również metody String.ToUpperInvariant i String.ToLowerInvariant. ToUpperInvariant to standardowy sposób normalizacji wielkości liter. Porównania wykonywane przy użyciu StringComparison.OrdinalIgnoreCase są zachowawczo kompozycją dwóch wywołań: wywoływanie ToUpperInvariant na obu argumentach ciągów, a następnie porównywanie przy użyciu StringComparison.Ordinal.

Aby skonwertować na wielkie i małe litery w określonej kulturze, można również skorzystać z przeciążeń, przekazując obiekt CultureInfo 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 metod String.ToUpper() 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.

Brakuje spójności w odniesieniu do sposobu, w jaki domyślne przeciążenia tych metod wykonują porównania. Wszystkie metody String.IndexOf i String.LastIndexOf, które zawierają parametr Char, wykonują porównanie porządkowe, natomiast domyślne metody String.IndexOf i String.LastIndexOf, które zawierają parametr String, wykonują porównanie uwzględniające 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 nie będące stringami, które mają porównanie ciągów jako centralną operację, 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 przechowywanie i pobieranie działają na komparatorze, który jest automatycznie dostarczany przez właściwość Thread.CurrentThread.CurrentCulture. Jeśli kultura może się zmienić między wywołaniami do StoreNames i DoesNameExist, a zwłaszcza jeśli zawartość tablicy jest przechowywana między dwoma wywołaniami tych metod, 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ć kultury niezmiennej 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

Haszowanie ciągów stanowi drugi przykład operacji, na którą wpływa 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 interfejs IEqualityComparer, jej metoda GetHashCode 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ż