Změny chování při porovnávání řetězců v .NET 5+

.NET 5 zavádí změnu chování modulu runtime, kde rozhraní API globalizace ve výchozím nastavení používají ICU ve všech podporovaných platformách. Jedná se o odchod z dřívějších verzí .NET Core a rozhraní .NET Framework, které při spouštění ve Windows využívají funkci podpory národních jazyků operačního systému (NLS). Další informace o těchtozměnách

Důvod změny

Tato změna byla zavedena ke sjednocení . Chování globalizace net ve všech podporovaných operačních systémech Poskytuje také možnost aplikacím seskupit vlastní knihovny globalizace, nikoli záviset na integrovaných knihovnách operačního systému. Další informace najdete v oznámení o zásadní změně.

Rozdíly v chování

Pokud používáte funkce jako string.IndexOf(string) bez volání přetížení, které přebírá StringComparison argument, můžete mít v úmyslu provést řadové vyhledávání, ale místo toho neúmyslně převezmete závislost na chování specifické pro jazykovou verzi. Vzhledem k tomu, že NLS a ICU implementují různé logiky v lingvistických porovnáních, výsledky metod, jako string.IndexOf(string) jsou, mohou vracet neočekávané hodnoty.

To se může projevit i na místech, kde neočekáváte, že globální zařízení budou aktivní. Následující kód může například vytvořit jinou odpověď v závislosti na aktuálním modulu runtime.

const string greeting = "Hel\0lo";
Console.WriteLine($"{greeting.IndexOf("\0")}");

// The snippet prints:
//
// '3' when running on .NET Core 2.x - 3.x (Windows)
// '0' when running on .NET 5 or later (Windows)
// '0' when running on .NET Core 2.x - 3.x or .NET 5 (non-Windows)
// '3' when running on .NET Core 2.x or .NET 5+ (in invariant mode)

string s = "Hello\r\nworld!";
int idx = s.IndexOf("\n");
Console.WriteLine(idx);

// The snippet prints:
//
// '6' when running on .NET Core 3.1
// '-1' when running on .NET 5 or .NET Core 3.1 (non-Windows OS)
// '-1' when running on .NET 5 (Windows 10 May 2019 Update or later)
// '6' when running on .NET 6+ (all Windows and non-Windows OSs)

Další informace naleznete v tématu Globalization APIs use ICU libraries on Windows.

Ochrana před neočekávaným chováním

Tato část obsahuje dvě možnosti pro řešení neočekávaných změn chování v .NET 5.

Povolení analyzátorů kódu

Analyzátory kódu můžou detekovat pravděpodobně weby volání chyb. Pro ochranu před překvapivým chováním doporučujeme ve vašem projektu povolit analyzátory kompilátoru .NET (Roslyn). Analyzátory pomáhají označit kód, který by mohl neúmyslně používat lingvistický porovnávač, když byl pravděpodobně zamýšlen pořadový porovnávač. Následující pravidla by měla pomoct označit tyto problémy:

Tato konkrétní pravidla nejsou ve výchozím nastavení povolená. Pokud je chcete povolit a zobrazit všechna porušení jako chyby sestavení, nastavte v souboru projektu následující vlastnosti:

<PropertyGroup>
  <AnalysisMode>All</AnalysisMode>
  <WarningsAsErrors>$(WarningsAsErrors);CA1307;CA1309;CA1310</WarningsAsErrors>
</PropertyGroup>

Následující fragment kódu ukazuje příklady kódu, které vytvářejí upozornění nebo chyby příslušného analyzátoru kódu.

//
// Potentially incorrect code - answer might vary based on locale.
//
string s = GetString();
// Produces analyzer warning CA1310 for string; CA1307 matches on char ','
int idx = s.IndexOf(",");
Console.WriteLine(idx);

//
// Corrected code - matches the literal substring ",".
//
string s = GetString();
int idx = s.IndexOf(",", StringComparison.Ordinal);
Console.WriteLine(idx);

//
// Corrected code (alternative) - searches for the literal ',' character.
//
string s = GetString();
int idx = s.IndexOf(',');
Console.WriteLine(idx);

Podobně při vytváření instance seřazené kolekce řetězců nebo řazení existující kolekce založené na řetězcích zadejte explicitní porovnávací nástroj.

//
// Potentially incorrect code - behavior might vary based on locale.
//
SortedSet<string> mySet = new SortedSet<string>();
List<string> list = GetListOfStrings();
list.Sort();

//
// Corrected code - uses ordinal sorting; doesn't vary by locale.
//
SortedSet<string> mySet = new SortedSet<string>(StringComparer.Ordinal);
List<string> list = GetListOfStrings();
list.Sort(StringComparer.Ordinal);

Návrat k chování služby NLS

Pokud chcete vrátit aplikace .NET 5+ zpět ke starším chování služby NLS při spouštění ve Windows, postupujte podle kroků v globalizaci .NET a ICU. Tento přepínač kompatibility pro celou aplikaci musí být nastaven na úrovni aplikace. Jednotlivé knihovny se k tomuto chování nemůžou vyjádřit výslovný souhlas ani se odhlásit.

Tip

Důrazně doporučujeme povolit pravidla analýzy kódu CA1307, CA1309 a CA1310 , která vám pomůžou zlepšit hygienu kódu a zjistit všechny stávající latentní chyby. Další informace naleznete v tématu Povolení analyzátorů kódu.

Ovlivněná rozhraní API

Většina aplikací .NET nenarazí na žádné neočekávané chování kvůli změnám v .NET 5. Vzhledem k počtu ovlivněnýchrozhraníchm technologiím .NET byste ale měli vědět o počtu ovlivněných rozhraní API a o tom, jak jsou tato rozhraní API základem širšího ekosystému .NET, měli byste vědět o potenciálu .NET 5 zavést nežádoucí chování nebo vystavit latentní chyby, které už ve vaší aplikaci existují.

Ovlivněná rozhraní API zahrnují:

Poznámka:

Toto není vyčerpávající seznam ovlivněných rozhraní API.

Všechna výše uvedená rozhraní API používají jazykové vyhledávání a porovnávání s použitím aktuální jazykové verze vlákna. Rozdíly mezi lingvistickým a řadovým vyhledáváním a porovnáním jsou označovány v řadovém a jazykovém vyhledávání a porovnání.

Vzhledem k tomu, že ICU implementuje porovnání lingvistických řetězců odlišně od nlS, aplikace založené na Windows, které upgradují na .NET 5 ze starší verze rozhraní .NET Core nebo .NET Framework a které volají jedno z ovlivněných rozhraní API, si mohou všimnout, že rozhraní API začínají vykazovat různá chování.

Výjimky

  • Pokud rozhraní API přijme explicitní StringComparison nebo CultureInfo parametr, tento parametr přepíše výchozí chování rozhraní API.
  • System.String členové, kde první parametr je typu char (například String.IndexOf(Char)) použít řadové vyhledávání, pokud volající předá explicitní StringComparison argument, který určuje CurrentCulture[IgnoreCase] nebo InvariantCulture[IgnoreCase].

Podrobnější analýzu výchozího chování jednotlivých String rozhraní API najdete v části Výchozí typy vyhledávání a porovnání.

Ordinální vs. lingvistické vyhledávání a porovnání

Řadové vyhledávání (označované také jako nejazyčné) vyhledávání a porovnávání rozloží řetězec do jednotlivých char prvků a provede vyhledávání nebo porovnání typu char-by-char. Například řetězce a porovnávají "dog" se jako stejné pod Ordinal porovnávačem, protože tyto dva řetězce se skládají z úplně stejné sekvence znaků."dog" "dog" Ale a "Dog" porovnejte je jako nerovnající Ordinal se pod porovnávačem, protože se skládají ze stejné posloupnosti znaků. To znamená, že k bodu kódu velkým písmenem 'D'dochází před malými písmeny'd', U+0064což vede k "Dog" řazení před "dog".U+0044

Porovnávací OrdinalIgnoreCase nástroj stále funguje na základě znaků typu char-by-char, ale eliminuje rozdíly v malých a malých písmenech při provádění operace. OrdinalIgnoreCase Pod porovnávačem dvojice znaků a 'D' porovnávají 'd' se stejně, stejně jako dvojice 'á' znaků a 'Á'. Neschváliný znak 'a' se ale porovnává jako nerovnající se znaku se zvýrazněným znakem 'á'.

V následující tabulce jsou uvedené některé příklady:

Řetězec 1 Řetězec 2 Ordinal Porovnání OrdinalIgnoreCase Porovnání
"dog" "dog" je rovno je rovno
"dog" "Dog" není rovno je rovno
"resume" "résumé" není rovno není rovno

Unicode také umožňuje řetězcům mít několik různých reprezentací v paměti. Například e-akutní (é) lze reprezentovat dvěma možnými způsoby:

  • Jeden literálový 'é' znak (také napsaný jako '\u00E9').
  • Literální neakcentovaný 'e' znak následovaný kombinačním modifikátorem '\u0301'zvýraznění .

To znamená, že následující čtyři řetězce se zobrazí jako "résumé", i když se jejich základní části liší. Řetězce používají kombinaci literálových 'é' znaků nebo literálových nepřipravených 'e' znaků a kombinace modifikátoru '\u0301'zvýraznění .

  • "r\u00E9sum\u00E9"
  • "r\u00E9sume\u0301"
  • "re\u0301sum\u00E9"
  • "re\u0301sume\u0301"

V rámci řadového porovnávače se žádný z těchto řetězců nerovná jako stejný. Je to proto, že všechny obsahují různé podkladové sekvence znaků, i když se vykreslují na obrazovku, všechny vypadají stejně.

Při provádění string.IndexOf(..., StringComparison.Ordinal) operace modul runtime hledá přesnou shodu podřetěžce. Výsledky jsou následující.

Console.WriteLine("resume".IndexOf("e", StringComparison.Ordinal)); // prints '1'
Console.WriteLine("r\u00E9sum\u00E9".IndexOf("e", StringComparison.Ordinal)); // prints '-1'
Console.WriteLine("r\u00E9sume\u0301".IndexOf("e", StringComparison.Ordinal)); // prints '5'
Console.WriteLine("re\u0301sum\u00E9".IndexOf("e", StringComparison.Ordinal)); // prints '1'
Console.WriteLine("re\u0301sume\u0301".IndexOf("e", StringComparison.Ordinal)); // prints '1'
Console.WriteLine("resume".IndexOf("E", StringComparison.OrdinalIgnoreCase)); // prints '1'
Console.WriteLine("r\u00E9sum\u00E9".IndexOf("E", StringComparison.OrdinalIgnoreCase)); // prints '-1'
Console.WriteLine("r\u00E9sume\u0301".IndexOf("E", StringComparison.OrdinalIgnoreCase)); // prints '5'
Console.WriteLine("re\u0301sum\u00E9".IndexOf("E", StringComparison.OrdinalIgnoreCase)); // prints '1'
Console.WriteLine("re\u0301sume\u0301".IndexOf("E", StringComparison.OrdinalIgnoreCase)); // prints '1'

Pořadové rutiny vyhledávání a porovnání nejsou nikdy ovlivněny nastavením jazykové verze aktuálního vlákna.

Lingvistické vyhledávání a porovnávací rutiny rozkládají řetězec do kompletačních prvků a provádějí vyhledávání nebo porovnání těchto prvků. Mapování 1:1 mezi znaky řetězce a jeho prvky kolace nemusí být nutně 1:1. Například řetězec délky 2 se může skládat pouze z jednoho kompletačního prvku. Když jsou dva řetězce porovnávány lingvisticky, porovnávač zkontroluje, jestli mají oba prvky kolace řetězců stejný sémantický význam, i když jsou literální znaky řetězce odlišné.

Znovu zvažte řetězec "résumé" a jeho čtyři různé reprezentace. Následující tabulka uvádí jednotlivé reprezentace rozdělené do svých prvků kolace.

String Jako prvky kolace
"r\u00E9sum\u00E9" "r" + "\u00E9" + "s" + "u" + "m" + "\u00E9"
"r\u00E9sume\u0301" "r" + "\u00E9" + "s" + "u" + "m" + "e\u0301"
"re\u0301sum\u00E9" "r" + "e\u0301" + "s" + "u" + "m" + "\u00E9"
"re\u0301sume\u0301" "r" + "e\u0301" + "s" + "u" + "m" + "e\u0301"

Kolační prvek volně odpovídá tomu, co čtenáři považují za jeden znak nebo shluk znaků. Je koncepčně podobný clusteru grafeme, ale zahrnuje poněkud větší deštník.

V lingvistickém porovnávače nejsou přesné shody nezbytné. Kompletační prvky se místo toho porovnávají na základě jejich sémantického významu. Lingvistický porovnávač například považuje podřetězece "\u00E9" a "e\u0301" je stejný, protože oba sémanticky znamenají "malá písmena e s akutním modifikátorem zvýraznění". To umožňuje metodu IndexOf spárovat podřetězce "e\u0301" ve větším řetězci, který obsahuje sémanticky ekvivalentní podřetězce "\u00E9", jak je znázorněno v následující ukázce kódu.

Console.WriteLine("r\u00E9sum\u00E9".IndexOf("e")); // prints '-1' (not found)
Console.WriteLine("r\u00E9sum\u00E9".IndexOf("\u00E9")); // prints '1'
Console.WriteLine("\u00E9".IndexOf("e\u0301")); // prints '0'

V důsledku toho se můžou dva řetězce různých délek porovnávat stejně, pokud se použije lingvistické porovnání. Volající by se měli postarat o logiku zvláštních případů, která se v takových scénářích zabývá délkou řetězce.

Rutiny vyhledávání a porovnání pracující s kulturou jsou zvláštní formou lingvistického vyhledávání a porovnávacích rutin. V rámci porovnávače pracujícího s jazykovou verzí je koncept kompletačního prvku rozšířen tak, aby zahrnoval informace specifické pro zadanou jazykovou verzi.

Například v maďarštině, když se dva znaky <dz> zobrazí zpět, považují se za vlastní jedinečné písmeno odlišné od <d> nebo <z>. To znamená, že když <je dz> zobrazen v řetězci, maďarština porovnávání s ní pracuje jako s jedním kompletačním prvkem.

String Jako prvky kolace Poznámky
"endz" "e" + "n" + "d" + "z" (pomocí standardního lingvistického porovnávače)
"endz" "e" + "n" + "dz" (pomocí porovnávače pracujícího s maďarskou kulturou)

Při použití porovnávání s maďarštinou to znamená, že řetězec "endz"nekončí podřetězcem "z", protože <dz> a <z> jsou považovány za kompletační prvky s odlišným sémantickým významem.

// Set thread culture to Hungarian
CultureInfo.CurrentCulture = CultureInfo.GetCultureInfo("hu-HU");
Console.WriteLine("endz".EndsWith("z")); // Prints 'False'

// Set thread culture to invariant culture
CultureInfo.CurrentCulture = CultureInfo.InvariantCulture;
Console.WriteLine("endz".EndsWith("z")); // Prints 'True'

Poznámka:

  • Chování: Porovnávání s jazykovou a jazykovou verzí může občas projít úpravami chování. IcU i starší zařízení NlS systému Windows se aktualizují tak, aby zohlednily, jak se světové jazyky mění. Další informace najdete v blogovém příspěvku o změnách dat národního prostředí (jazykové verze). Chování řadového porovnávače se nikdy nezmění, protože provádí přesné bitové vyhledávání a porovnání. Chování porovnávače OrdinalIgnoreCase se však může změnit, protože Unicode roste, aby zahrnovalo více znakových sad a opravuje vynechání v existujících datech o velikosti písmen.
  • Použití: Porovnávače StringComparison.InvariantCulture a StringComparison.InvariantCultureIgnoreCase lingvistické porovnávače, které nejsou podporující jazykovou verzi. To znamená, že tito porovnávači chápou koncepty, jako je zvýrazněný znak é s více možnými podkladovými reprezentacemi, a že všechny takové reprezentace by měly být považovány za stejné. Jazykové porovnávače, které nerozlišují jazykovou verzi, ale nebudou obsahovat speciální zpracování pro dz>, jak><<> je znázorněno výše.< Nebudou také speciální znaky jako německý Eszett (ß).

.NET také nabízí invariantní režim globalizace. Tento režim výslovného souhlasu zakáže cesty kódu, které se zabývají lingvistickými vyhledávacími a porovnávanými rutinami. V tomto režimu používají všechny operace chování Pořadové nebo Pořadové nebo OrdinalIgnoreCase bez ohledu na to, co StringComparisonCultureInfo nebo argument volající poskytuje. Další informace najdete v tématu Možnosti konfigurace modulu runtime pro globalizaci a invariantní režim globalizace .NET Core.

Další informace najdete v tématu Osvědčené postupy pro porovnávání řetězců v .NET.

Vliv na zabezpečení

Pokud vaše aplikace používá k filtrování ovlivněné rozhraní API, doporučujeme povolit pravidla analýzy kódu CA1307 a CA1309, která vám pomůžou najít místa, kde se místo řadového vyhledávání mohlo neúmyslně použít lingvistické vyhledávání. Vzory kódu, jako je následující, můžou být náchylné k zneužití zabezpečení.

//
// THIS SAMPLE CODE IS INCORRECT.
// DO NOT USE IT IN PRODUCTION.
//
public bool ContainsHtmlSensitiveCharacters(string input)
{
    if (input.IndexOf("<") >= 0) { return true; }
    if (input.IndexOf("&") >= 0) { return true; }
    return false;
}

string.IndexOf(string) Vzhledem k tomu, že metoda používá lingvistické vyhledávání ve výchozím nastavení, je možné, že řetězec obsahuje literál '<' nebo '&' znak a rutina string.IndexOf(string) vrátit -1, což znamená, že podřetězce hledání nebyl nalezen. Pravidla analýzy kódu CA1307 a CA1309 označují takové weby volání a upozorňují vývojáře na potenciální problém.

Výchozí typy hledání a porovnání

Následující tabulka uvádí výchozí typy vyhledávání a porovnání pro různá rozhraní API podobná řetězcům a řetězcům. Pokud volající poskytne explicitní CultureInfo nebo StringComparison parametr, bude tento parametr dodržen ve všech výchozích hodnotách.

rozhraní API Výchozí chování Poznámky
string.Compare Currentculture
string.CompareTo Currentculture
string.Contains Řadová číslovka
string.EndsWith Řadová číslovka (pokud je prvním parametrem a char)
string.EndsWith Currentculture (pokud je prvním parametrem a string)
string.Equals Řadová číslovka
string.GetHashCode Řadová číslovka
string.IndexOf Řadová číslovka (pokud je prvním parametrem a char)
string.IndexOf Currentculture (pokud je prvním parametrem a string)
string.IndexOfAny Řadová číslovka
string.LastIndexOf Řadová číslovka (pokud je prvním parametrem a char)
string.LastIndexOf Currentculture (pokud je prvním parametrem a string)
string.LastIndexOfAny Řadová číslovka
string.Replace Řadová číslovka
string.Split Řadová číslovka
string.StartsWith Řadová číslovka (pokud je prvním parametrem a char)
string.StartsWith Currentculture (pokud je prvním parametrem a string)
string.ToLower Currentculture
string.ToLowerInvariant Invariantculture
string.ToUpper Currentculture
string.ToUpperInvariant Invariantculture
string.Trim Řadová číslovka
string.TrimEnd Řadová číslovka
string.TrimStart Řadová číslovka
string == string Řadová číslovka
string != string Řadová číslovka

Na rozdíl od string rozhraní API provádějí všechna MemoryExtensions rozhraní API ve výchozím nastavení ordinální vyhledávání a porovnání s následujícími výjimkami.

rozhraní API Výchozí chování Poznámky
MemoryExtensions.ToLower Currentculture (při předání argumentu null CultureInfo )
MemoryExtensions.ToLowerInvariant Invariantculture
MemoryExtensions.ToUpper Currentculture (při předání argumentu null CultureInfo )
MemoryExtensions.ToUpperInvariant Invariantculture

Následkem toho je, že při převodu kódu z využívání string na využívání ReadOnlySpan<char>může být neúmyslně zavedeny změny chování. Následuje příklad.

string str = GetString();
if (str.StartsWith("Hello")) { /* do something */ } // this is a CULTURE-AWARE (linguistic) comparison

ReadOnlySpan<char> span = s.AsSpan();
if (span.StartsWith("Hello")) { /* do something */ } // this is an ORDINAL (non-linguistic) comparison

Doporučeným způsobem, jak to vyřešit, je předat těmto rozhraním API explicitní StringComparison parametr. S tím můžou pomoct pravidla analýzy kódu CA1307 a CA1309.

string str = GetString();
if (str.StartsWith("Hello", StringComparison.Ordinal)) { /* do something */ } // ordinal comparison

ReadOnlySpan<char> span = s.AsSpan();
if (span.StartsWith("Hello", StringComparison.Ordinal)) { /* do something */ } // ordinal comparison

Viz také