Sdílet prostřednictvím


Osvědčené postupy pro porovnávání řetězců v .NET

.NET poskytuje rozsáhlou podporu pro vývoj lokalizovaných a globalizovaných aplikací a usnadňuje použití konvencí aktuální jazykové verze nebo konkrétní jazykové verze při provádění běžných operací, jako je řazení a zobrazování řetězců. Ale řazení nebo porovnávání řetězců není vždy citlivé na kulturní kontext. Například řetězce, které aplikace používá interně, by se obvykle měly zpracovávat stejně napříč všemi jazykovými verzemi. Pokud jsou řetězcová data nezávislá na kulturních specifikách, jako jsou značky XML, značky HTML, uživatelská jména, cesty k souborům a názvy systémových objektů, interpretována jako by byly kulturně závislá, může kód aplikace podléhat drobným chybám, špatnému výkonu a v některých případech i bezpečnostním problémům.

Tento článek zkoumá metody řazení, porovnávání a práci s velikostí písmen v prostředí .NET, obsahuje doporučení pro výběr vhodné metody zpracování řetězců a poskytuje dodatečné informace o těchto metodách.

Doporučení pro použití řetězců

Při vývoji pomocí .NET při porovnávání řetězců postupujte podle těchto doporučení.

Návod

Porovnání provádí různé metody související s řetězci. Mezi příklady patří String.Equals, String.Compare, String.IndexOfa String.StartsWith.

Při porovnávání řetězců se vyhněte následujícím postupům:

  • Nepoužívejte přetížení, která explicitně ani implicitně nezadávají pravidla porovnání řetězců pro řetězcové operace.
  • Ve většině případů nepoužívejte řetězcové operace založené na StringComparison.InvariantCulture. Jednou z několika výjimek je, když budete uchovávat lingvisticky smysluplná, ale kulturně nezávislá data.
  • Nepoužívejte přetíženou metodu String.Compare nebo CompareTo a otestujte, zda je návratová hodnota nulová, abyste zjistili, zda jsou dva řetězce stejné.

Jasné zadání porovnání řetězců

Většina metod manipulace s řetězci v .NET je přetížená. Jedno nebo více přetížení obvykle přijímají výchozí nastavení, zatímco ostatní přijímají žádné výchozí hodnoty a místo toho definují přesný způsob, jakým se mají řetězce porovnávat nebo manipulovat. Většina metod, které nespoléhá na výchozí hodnoty, zahrnují parametr typu StringComparison, což je výčet, který explicitně určuje pravidla pro porovnání řetězců podle jazykové verze a případu. Následující tabulka popisuje členy výčtového typu StringComparison.

StringComparison – člen Popis
CurrentCulture Provede porovnání s rozlišováním velkých a malých písmen pomocí aktuálního kulturního nastavení.
CurrentCultureIgnoreCase Provede porovnání nerozlišující malá a velká písmena pomocí aktuální kultury.
InvariantCulture Provádí porovnání s rozlišováním velkých a malých písmen pomocí invariantní kultury.
InvariantCultureIgnoreCase Provede porovnání bez rozlišování velkých a malých písmen použitím invariantní kultury.
Ordinal Provede řadové porovnání.
OrdinalIgnoreCase Provádí pořadové porovnávání bez rozlišování malých a velkých písmen.

Například metoda IndexOf, která vrací index podřetězce v objektu String, který odpovídá znaku nebo řetězci, má devět přetížení:

Doporučujeme vybrat přetížení, které nepoužívá výchozí hodnoty, z následujících důvodů:

  • Některá přetížení s výchozími parametry (ta, která hledají Char v instanci řetězce) provádějí porovnání podle pořadí, zatímco jiná (ta, která hledají řetězec v dané instanci) jsou závislá na kulturním kontextu. Je obtížné si zapamatovat, která metoda používá jakou výchozí hodnotu, a přetížení je snadné zaměnit.

  • Záměr kódu, který spoléhá na výchozí hodnoty volání metody, není jasný. V následujícím příkladu, který spoléhá na výchozí hodnoty, je obtížné zjistit, jestli vývojář skutečně zamýšlel pořadové nebo lingvistické porovnání dvou řetězců, nebo zda rozdíl mezi případem mezi url.Scheme a "https" může způsobit, že test rovnosti vrátí 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
    

Obecně doporučujeme volat metodu, která nespoléhá na výchozí hodnoty, protože je záměr kódu jednoznačný. Díky tomu je kód čitelnější a snadněji se ladí a udržuje. Následující příklad řeší otázky vyvolané v předchozím příkladu. Je zřejmé, že se používá řadové porovnání a že rozdíly v případě ignorovány.

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

Podrobnosti o porovnání řetězců

Porovnávání řetězců je klíčovým prvkem mnoha operací souvisejících s řetězci, zejména řazení a ověřování rovnosti. Řetězce jsou seřazeny v určeném pořadí: Pokud se "my" objeví před "řetězec" v seřazeném seznamu řetězců, musí "my" být menší nebo se rovnat "řetězci". Porovnání navíc implicitně definuje rovnost. Operace porovnání vrátí nulu pro řetězce, které považuje za stejné. Dobrou interpretací je, že ani jeden řetězec není menší než druhý. Nejvýstižnější operace zahrnující řetězce zahrnují jeden nebo oba tyto postupy: porovnávání s jiným řetězcem a provádění dobře definované operace řazení.

Poznámka:

Můžete si stáhnout Tabulky váhového řazení, sadu textových souborů, které obsahují informace o váhách znaků používaných při řazení a porovnávání v operačních systémech Windows, a Výchozí tabulka kolace Unicode, nejnovější verzi tabulky váhového řazení pro Linux a macOS. Konkrétní verze tabulky priorit řazení v Linuxu a macOS závisí na verzi knihoven International Components for Unicode nainstalovaných na systému. Informace o verzích ICU a verzích Unicode, které implementují, naleznete v oddílu Stažení ICU.

Vyhodnocení dvou řetězců pro rovnost nebo pořadí řazení však nepřináší jediný, správný výsledek; výsledek závisí na kritériích použitých k porovnání řetězců. Konkrétně porovnání řetězců, která jsou ordinační nebo která jsou založena na konvencích rozlišování velikosti písmen a řazení aktuální kultury nebo invariantní kultury (kultury nezávislé na národním prostředí na základě anglického jazyka), mohou způsobit různé výsledky.

Kromě toho porovnání řetězců s různými verzemi .NET nebo .NET na různých operačních systémech nebo verzích operačního systému může vracet různé výsledky. Pro více informací viz Řetězce a standard Unicode.

Porovnání řetězců, která používají aktuální kulturu

Jedním z kritérií je použití konvencí aktuální kultury při porovnávání řetězců. Porovnání založená na aktuální jazykové verzi používají aktuální jazykovou verzi nebo národní prostředí vlákna. Pokud uživatel kulturu nenastaví, použije se jako výchozí nastavení operačního systému. Vždy byste měli použít porovnání založená na aktuální jazykové verzi, pokud jsou data lingvisticky relevantní, a když odráží interakci uživatele citlivé na jazykovou verzi.

Když se změní kultura, změní se v .NET chování porovnání a velkých a malých písmen. K tomu dochází, když se aplikace spustí v počítači, který má jinou jazykovou verzi než počítač, na kterém byla aplikace vyvinuta, nebo když provádění vlákna změní jeho jazykovou verzi. Toto chování je záměrné, ale pro mnoho vývojářů to není zřejmé. Následující příklad znázorňuje rozdíly v pořadí řazení mezi jazykovými verzemi USA ("en-US") a švédštinou ("sv-SE"). Všimněte si, že slova "ångström", "Windows" a "Visual Studio" se objevují na různých pozicích v seřazených polích řetězců.

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

Porovnání nerozlišující malá a velká písmena, která používají aktuální jazykovou verzi, jsou stejná jako porovnání citlivá na jazykovou verzi, s tím rozdílem, že ignorují malá a velká písmena podle aktuální jazykové verze vlákna. Toto chování se může projevit také v pořadí řazení.

Porovnání, která používají sémantiku aktuální jazykové verze, jsou výchozí pro následující metody:

V každém případě doporučujeme zavolat dané přetížení, které má parametr StringComparison, aby byl jasný záměr volání metody.

Drobné a ne tak drobné chyby se mohou objevit, když se jazykově interpretují nejazyčná řetězcová data nebo když se řetězcová data z konkrétní jazykové verze interpretují pomocí konvencí jiné jazykové verze. Kanonický příklad je problém Turkish-I.

Pro téměř všechny latinky, včetně angličtiny v USA, je znak "i" (\u0069) malými písmeny znaku "I" (\u0049). Toto pravidlo psaní velkých písmen se rychle stane výchozím nastavením pro někoho, kdo programuje v takovém prostředí. Nicméně, turečtina ("tr-TR") abeceda obsahuje "I s tečkou" znak "İ" (\u0130), což je hlavní verze "i". Turečtina obsahuje také malé písmeno 'i bez tečky', znak 'ı' (\u0131), které se používá jako velké písmeno 'I'. K tomuto chování dochází také v ázerbájdžánské kultuře ("az").

Proto předpoklady týkající se velkých písmen "i" nebo nižších písmen "I" nejsou platné mezi všemi kulturami. Pokud použijete výchozí přetížení pro rutiny porovnání řetězců, budou podléhat různosti mezi kulturami. Pokud jsou porovnávaná data nejazyková, použití výchozích přetížení může vést k nežádoucím výsledkům, jak ukazuje následující pokus o provedení porovnání řetězců "bill" a "BILL" bez rozlišování velikosti písmen.

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

Toto porovnání může způsobit významné problémy, pokud je kultura neúmyslně používána v prostředích citlivých na zabezpečení, jako v následujícím příkladu. Volání metody, jako je IsFileURI("file:"), vrátí true, pokud je aktuální kulturní prostředí angličtina (USA), ale false, pokud je aktuální kulturní prostředí turečtina. V tureckých systémech by tedy někdo mohl obejít bezpečnostní opatření, která blokují přístup k URI bez rozlišování velkých a malých písmen, které začínají „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

V tomto případě, protože "file:" je určen k interpretaci jako nejazyčný identifikátor nerozlišující jazykovou verzi, měl by být kód napsán, jak je znázorněno v následujícím příkladu:

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

Operace řadových řetězců

Určení StringComparison.Ordinal nebo StringComparison.OrdinalIgnoreCase hodnoty ve volání metody označuje nejazyčné porovnání, ve kterém jsou vlastnosti přirozeného jazyka ignorovány. Metody, které jsou vyvolány s těmito StringComparison hodnotami, zakládají rozhodování o operacích na řetězcích na jednoduchých porovnáváních bajtů namísto tabulek velikostí písmen nebo ekvivalence, které jsou parametrizovány kulturními zvyklostmi. Ve většině případů je tento přístup nejvhodnější pro zamýšlenou interpretaci řetězců a zároveň rychlejší a spolehlivější kód.

Řadové porovnání jsou porovnání řetězců, ve kterých se každý bajt každého řetězce porovnává bez jazykové interpretace; Například "windows" neodpovídá "Windows". Jedná se v podstatě o volání funkce strcmp běhového prostředí jazyka C. Toto porovnání použijte, když kontext vyžaduje, aby se řetězce přesně shodovaly a vyžaduje konzervativní politiku srovnávání. Kromě toho je pořadové porovnání nejrychlejší operací porovnání, protože při určování výsledku nepoužívá žádná jazyková pravidla.

Řetězce v .NET můžou obsahovat vložené znaky null (a další netiskné znaky). Jedním z nejjasnějších rozdílů mezi ordinálním a kulturu zohledňujícím porovnáním (včetně porovnání, která používají invariantní kulturní verzi) je zpracování vložených nulových znaků v textovém řetězci. Tyto znaky jsou ignorovány při použití metod String.Compare a String.Equals k provádění porovnání citlivých na kulturní rozdíly (včetně porovnání, která používají neutrální jazykovou verzi). Výsledkem je, že řetězce, které obsahují vložené znaky null, mohou být považovány za rovné řetězcům, které je neobsahují. Vložené netisknutné znaky mohou být vynechány pro účely metod porovnání řetězců, například String.StartsWith.

Důležité

I když metody porovnání řetězců ignorují vložené znaky null, metody vyhledávání řetězců, jako jsou String.Contains, String.EndsWith, String.IndexOf, String.LastIndexOfa String.StartsWith ne.

Následující příklad provádí kultuře přizpůsobené porovnání řetězce "Aa" s podobným řetězcem, který obsahuje několik vložených nulových znaků mezi "A" a "a", a ukazuje, jak jsou tyto dva řetězce považovány za stejné.

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

Při použití řadového porovnání se však řetězce nepovažují za stejné, jak ukazuje následující příklad:

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

Porovnání podle pořadí, která nerozlišují velká a malá písmena, jsou dalším nejkonzervativnějším přístupem. Tato porovnání ignorují většinu případů; Například "windows" odpovídá "Windows". Při práci se znaky ASCII je tato zásada ekvivalentní k StringComparison.Ordinal, s tím rozdílem, že ignoruje obvyklou velikost písmen ASCII. Proto libovolný znak v [A, Z] (\u0041-\u005A) odpovídá odpovídajícímu znaku v [a;z] (\u0061-\007A). Použití velkých a malých písmen mimo rozsah ASCII využívá invariantní tabulky kultury. Proto následující porovnání:

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

je ekvivalentní (ale rychlejší než) toto porovnání:

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

Tato porovnání jsou stále velmi rychlá.

StringComparison.Ordinal i StringComparison.OrdinalIgnoreCase používají binární hodnoty přímo a jsou nejvhodnější pro porovnávání. Pokud si nejste jistí nastavením porovnání, použijte jednu z těchto dvou hodnot. Vzhledem k tomu, že ale provádějí porovnání bajtů po bajtech, neřadí se podle lingvistického pořadí řazení (například anglického slovníku), ale podle binárního pořadí řazení. Výsledky můžou být ve většině kontextů liché, pokud se zobrazí uživatelům.

Ordinální sémantika je standardní pro přetížení String.Equals, které nezahrnují argument StringComparison (včetně operátoru rovnosti). V každém případě doporučujeme volat přetížení, které má parametr StringComparison.

Řetězcové operace, které používají invariantní jazykovou verzi

Porovnání s invariantní kulturou používá vlastnost CompareInfo, kterou vrací statická vlastnost CultureInfo.InvariantCulture. Toto chování je stejné ve všech systémech; překládá všechny znaky mimo rozsah do toho, co se domnívá, že jsou ekvivalentní invariantní znaky. Tato zásada může být užitečná pro udržování konsistentního chování řetězců napříč kulturami, ale často poskytuje neočekávané výsledky.

Porovnání nerozlišující malá a velká písmena s invariantní jazykovou verzí používají také statickou vlastnost CompareInfo, která je vrácena statickou vlastností CultureInfo.InvariantCulture, pro potřeby porovnávání. Všechny rozdíly mezi těmito přeloženými znaky jsou ignorovány.

Porovnání, která používají StringComparison.InvariantCulture a StringComparison.Ordinal fungují shodně s řetězci ASCII. StringComparison.InvariantCulture však provádí lingvistická rozhodnutí, která nemusí být vhodná pro řetězce, které musí být interpretovány jako sada bajtů. Objekt CultureInfo.InvariantCulture.CompareInfo způsobí, aby metoda Compare interpretovala určité sady znaků jako rovnocenné. Například následující ekvivalence je platná v rámci invariantní kulturní verze.

InvariantCulture: a + ̊ = å

Malé písmeno latinky "a" (\u0061), je-li vedle znaku SLOUČENÍ PRSTENU NAD "̊" (\u030a), je interpretováno jako malé písmeno latinky "a" s prstencem nad "å" (\u00e5). Jak ukazuje následující příklad, toto chování se liší od řadového porovnání.

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

Při interpretaci názvů souborů, souborů cookie nebo čehokoli jiného, kde se může objevit kombinace jako "å", nabízejí pořadová porovnání stále nejtransparentnější a nejvhodnější chování.

Celkově má neproměnná kultura několik vlastností, díky kterým je užitečná pro porovnání. Porovnává způsobem, který má jazykový význam, což brání zajistit úplnou symbolickou shodu, ale není to vhodná volba pro zobrazení v žádné kultuře. Jedním z mála důvodů použití StringComparison.InvariantCulture pro porovnání je zachování seřazených dat pro kulturně jednotné zobrazení. Pokud například velký datový soubor, který obsahuje seznam seřazených identifikátorů pro zobrazení, doprovází aplikaci, přidání do tohoto seznamu by vyžadovalo vložení s řazením ve stylu invariant.

Volba člena StringComparison pro volání metody

Následující tabulka popisuje mapování ze sémantického kontextu řetězce na člen výčtu StringComparison.

Údaje Chování Odpovídající System.StringComparison

hodnota
Interní identifikátory citlivé na malá a velká písmena.

Případově citlivé identifikátory ve standardech, jako je XML a HTTP.

Nastavení související se zabezpečením, která rozlišují velká a malá písmena.
Nejazyčný identifikátor, kde se bajty přesně shodují. Ordinal
Interní identifikátory nerozlišující malá a velká písmena.

Identifikátory nerozlišující malá a velká písmena ve standardech, jako je XML a HTTP.

Cesty k souborům

Klíče a hodnoty registru.

Proměnné prostředí.

Identifikátory prostředků (například názvy úchytů)

Nastavení související se zabezpečením nezávisle na velikosti písmen.
Nejazyčný identifikátor, pokud je případ irelevantní. OrdinalIgnoreCase
Některá trvalá, lingvisticky relevantní data.

Zobrazení lingvistických dat, která vyžadují pevné pořadí řazení
Kulturní nezávislá data, která jsou stále lingvisticky relevantní. InvariantCulture

nebo

InvariantCultureIgnoreCase
Data zobrazená uživateli

Většina uživatelských vstupů.
Data, která vyžadují místní lingvistické zvyky. CurrentCulture

nebo

CurrentCultureIgnoreCase

Běžné metody porovnání řetězců v .NET

Následující části popisují metody, které se nejčastěji používají pro porovnání řetězců.

String.Compare

Výchozí interpretace: StringComparison.CurrentCulture.

Vzhledem k tomu, že jde o nejdůležitější operaci pro interpretaci řetězců, všechny instance volání těchto metod by měly být zkoumány, aby bylo možné určit, zda řetězce mají být interpretovány podle aktuální kultury, nebo mají být od kultury symbolicky odděleny. Obvykle se jedná o posledně jmenovanou možnost a místo toho by se mělo použít porovnání StringComparison.Ordinal.

Třída System.Globalization.CompareInfo, vrácená vlastností CultureInfo.CompareInfo, zahrnuje také metodu Compare, která poskytuje velký počet odpovídajících kritérií (řazení podle pořadí, ignorování prázdného místa, ignorování typu kana atd.) pomocí výčtu příznaků CompareOptions.

String.CompareTo

Výchozí interpretace: StringComparison.CurrentCulture.

Tato metoda v současné době nenabízí přetížení, které určuje typ StringComparison. Obvykle je možné tuto metodu převést na doporučený formulář String.Compare(String, String, StringComparison).

Tuto metodu implementují typy, které implementují rozhraní IComparable a IComparable<T>. Vzhledem k tomu, že nenabízí možnost parametru StringComparison, implementace typů často umožňuje uživateli zadat StringComparer ve svém konstruktoru. Následující příklad definuje FileName třídy, jejíž konstruktor třídy obsahuje StringComparer parametr. Tento objekt StringComparer se pak použije v metodě 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

Výchozí interpretace: StringComparison.Ordinal.

Třída String umožňuje testovat rovnost voláním statických nebo instančních přetížení metod Equals, nebo pomocí statického operátoru rovnosti. Přetížení a operátor ve výchozím nastavení používají řadové porovnání. Přesto však doporučujeme volat přetížení, které explicitně určuje typ StringComparison, i pokud chcete provést řadové porovnání; to usnadňuje vyhledávání určité interpretace řetězců v kódu.

String.ToUpper a String.ToLower

Výchozí interpretace: StringComparison.CurrentCulture.

Při použití metod String.ToUpper() a String.ToLower() buďte opatrní, protože převod řetězce na velká nebo malá písmena se často používá jako dílčí normalizace pro porovnávání řetězců bez ohledu na velikost písmen. Pokud ano, zvažte použití porovnání ignorujícího velikost písmen.

K dispozici jsou také metody String.ToUpperInvariant a String.ToLowerInvariant. ToUpperInvariant je standardní způsob normalizace případů. Porovnání provedená pomocí StringComparison.OrdinalIgnoreCase mají podobu složení dvou volání: zavoláním ToUpperInvariant na oba řetězcové argumenty a porovnáním pomocí StringComparison.Ordinal.

Přetížení jsou také k dispozici pro převod na velká a malá písmena v konkrétní kultuře tím, že metodě předáte objekt CultureInfo, který tuto kulturu představuje.

Char.ToUpper a Char.ToLower

Výchozí interpretace: StringComparison.CurrentCulture.

Metody Char.ToUpper(Char) a Char.ToLower(Char) fungují podobně jako metody String.ToUpper() a String.ToLower() popsané v předchozí části.

String.StartsWith a String.EndsWith

Výchozí interpretace: StringComparison.CurrentCulture.

Ve výchozím nastavení obě tyto metody provádějí porovnání citlivé na kulturu. Konkrétně mohou ignorovat netisknutné znaky.

String.IndexOf a String.LastIndexOf

Výchozí interpretace: StringComparison.CurrentCulture.

Existuje nedostatek konzistence v tom, jak výchozí přetížení těchto metod provádí porovnání. Všechny metody String.IndexOf a String.LastIndexOf, které zahrnují parametr Char, provádějí porovnání řadových hodnot, ale výchozí String.IndexOf a String.LastIndexOf metody, které obsahují parametr String, provádějí porovnání citlivé na jazykovou verzi.

Pokud zavoláte metodu String.IndexOf(String) nebo String.LastIndexOf(String) a předáte jí řetězec pro vyhledání v aktuální instanci, doporučujeme volat přetížení, které explicitně určuje typ StringComparison. Přetížení zahrnující argument Char vám neumožňují zadat typ StringComparison.

Metody, které provádějí porovnání řetězců nepřímo

Některé neřetězcové metody, které mají porovnání řetězců jako centrální operace, používají typ StringComparer. Třída StringComparer obsahuje šest statických vlastností, které vracejí StringComparer instance, jejichž StringComparer.Compare metody provádějí následující typy porovnání řetězců:

Array.Sort a Array.BinarySearch

Výchozí interpretace: StringComparison.CurrentCulture.

Při ukládání jakýchkoli dat do kolekce nebo při čtení uložených dat ze souboru či databáze do kolekce může přepnutí aktuální jazykové verze narušit platnost invariantů v kolekci. Metoda Array.BinarySearch předpokládá, že prvky v poli, které mají být prohledány, jsou již seřazeny. Chcete-li seřadit libovolný řetězcový prvek v poli, metoda Array.Sort volá metodu String.Compare k uspořádání jednotlivých prvků. Použití porovnávače citlivého na jazykovou verzi může být nebezpečné, pokud se jazyková verze změní mezi časem, kdy je pole seřazeno, a časem, kdy se prohledává jeho obsah. Například v následujícím kódu se ukládání a načítání provádí s porovnávačem, který je implicitně určen vlastností Thread.CurrentThread.CurrentCulture. Pokud se může kultura změnit mezi voláními StoreNames a DoesNameExist, a zejména pokud je obsah pole uložen někde mezi voláními těchto dvou metod, binární vyhledávání může selhat.

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

V následujícím příkladu se zobrazí doporučená varianta, která používá stejnou metodu porovnání bez rozlišení jazykové verze k seřazení i vyhledávání v poli. Kód změny se projeví v řádcích označených Line A a Line B v obou příkladech.

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

Pokud jsou tato data uložena a přenesena mezi kulturami a řazení se používá k prezentaci těchto dat uživateli, můžete zvážit použití StringComparison.InvariantCulture, která funguje lingvisticky pro lepší uživatelský výstup, ale není ovlivněno změnami v kultuře. Následující příklad upravuje dva předchozí příklady tak, aby používaly invariantní jazykovou verzi pro řazení a hledání v poli.

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

Příklad kolekcí: Konstruktor Hashtable

Hashování řetězců představuje druhý příklad operace, která je ovlivněna způsobem porovnání řetězců.

Následující příklad vytvoří instanci objektu Hashtable předáním objektu StringComparer, který vrací vlastnost StringComparer.OrdinalIgnoreCase. Vzhledem k tomu, že třída StringComparer odvozená z StringComparer implementuje rozhraní IEqualityComparer, jeho GetHashCode metoda se používá k výpočtu hash kódu řetězců v tabulce hash.

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

Viz také