Udostępnij za pośrednictwem


Zachowanie zmienia się podczas porównywania ciągów na platformie .NET 5+

Platforma .NET 5 wprowadza zmianę zachowania środowiska uruchomieniowego, w której interfejsy API globalizacji domyślnie używają ICU na wszystkich obsługiwanych platformach. Jest to odejście od wcześniejszych wersji platformy .NET Core i programu .NET Framework, które korzystają z funkcji obsługi języka krajowego systemu operacyjnego (NLS) podczas uruchamiania w systemie Windows. Aby uzyskać więcej informacji na temat tych zmian, w tym przełączników zgodności, które mogą przywrócić zmianę zachowania, zobacz Globalizacja platformy .NET i ICU.

Przyczyna wprowadzenia zmiany

Ta zmiana została wprowadzona w celu ujednolicenia . Zachowanie globalizacji platformy NET we wszystkich obsługiwanych systemach operacyjnych. Zapewnia również aplikacjom możliwość tworzenia pakietów własnych bibliotek globalizacji, a nie zależności od wbudowanych bibliotek systemu operacyjnego. Aby uzyskać więcej informacji, zobacz powiadomienie o zmianie powodującej niezgodność.

Różnice behawioralne

Jeśli używasz funkcji, takich jak string.IndexOf(string) bez wywoływania przeciążenia, które przyjmuje StringComparison argument, możesz zamiar wykonać wyszukiwanie porządkowe , ale zamiast tego przypadkowo przyjmujesz zależność od zachowania specyficznego dla kultury. Ponieważ nlS i ICU implementują różne logiki w swoich porównaniach językowych, wyniki metod, takich jak string.IndexOf(string) mogą zwracać nieoczekiwane wartości.

Może to przejawiać się nawet w miejscach, w których nie zawsze oczekujesz, że obiekty globalizacji będą aktywne. Na przykład poniższy kod może wygenerować inną odpowiedź w zależności od bieżącego środowiska uruchomieniowego.

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)

Aby uzyskać więcej informacji, zobacz Interfejsy API globalizacji używają bibliotek ICU w systemie Windows.

Ochrona przed nieoczekiwanym zachowaniem

Ta sekcja zawiera dwie opcje obsługi nieoczekiwanych zmian zachowania na platformie .NET 5.

Włączanie analizatorów kodu

Analizatory kodu mogą wykrywać prawdopodobnie witryny wywołań błędów. Aby chronić przed wszelkimi zaskakującymi zachowaniami, zalecamy włączenie analizatorów platformy kompilatora .NET (Roslyn) w projekcie. Analizatory pomagają oznaczyć kod flagi, który może przypadkowo używać porównywarki językowej, gdy prawdopodobnie był zamierzony moduł porównywania porządkowego. Następujące reguły powinny ułatwić flagę tych problemów:

Te konkretne reguły nie są domyślnie włączone. Aby je włączyć i pokazać wszelkie naruszenia jako błędy kompilacji, ustaw następujące właściwości w pliku projektu:

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

Poniższy fragment kodu przedstawia przykłady kodu, który generuje odpowiednie ostrzeżenia lub błędy analizatora kodu.

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

Podobnie podczas tworzenia wystąpienia posortowanej kolekcji ciągów lub sortowania istniejącej kolekcji opartej na ciągach określ jawny moduł porównujący.

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

Przywracanie do zachowań nls

Aby przywrócić starsze zachowania nlS dla platformy .NET 5+ podczas uruchamiania w systemie Windows, wykonaj kroki opisane w temacie Globalizacja platformy .NET i ICU. Ten przełącznik zgodności dla całej aplikacji musi być ustawiony na poziomie aplikacji. Poszczególne biblioteki nie mogą wyrazić zgody ani zrezygnować z tego zachowania.

Napiwek

Zdecydowanie zalecamy włączenie reguł analizy kodu CA1307, CA1309 i CA1310 w celu poprawy higieny kodu i odnalezienia istniejących ukrytych usterek. Aby uzyskać więcej informacji, zobacz Włączanie analizatorów kodu.

Dotyczy interfejsów API

Większość aplikacji .NET nie napotka żadnych nieoczekiwanych zachowań ze względu na zmiany na platformie .NET 5. Jednak ze względu na liczbę dotkniętych interfejsów API i sposób, w jaki te interfejsy API są podstawowe dla szerszego ekosystemu platformy .NET, należy pamiętać o możliwości wprowadzenia niepożądanych zachowań lub uwidocznienia ukrytych usterek, które już istnieją w aplikacji.

Interfejsy API, których dotyczy problem, obejmują:

Uwaga

Nie jest to wyczerpująca lista dotkniętych interfejsów API.

Wszystkie powyższe interfejsy API domyślnie używają wyszukiwania ciągów językowych i porównywania przy użyciu bieżącej kultury wątku. Różnice między wyszukiwaniem językowym i porządkowym i porównaniem są wywoływane w wyszukiwaniu porządkowym a wyszukiwaniu językowym i porównywaniu.

Ponieważ funkcja ICU implementuje porównania ciągów językowych różni się od nlS, aplikacje oparte na systemie Windows, które są uaktualniające do platformy .NET 5 z wcześniejszej wersji platformy .NET Core lub .NET Framework, i które wywołają jeden z dotkniętych interfejsów API, mogą zauważyć, że interfejsy API zaczynają wykazywać różne zachowania.

Wyjątki

  • Jeśli interfejs API akceptuje jawne StringComparison lub CultureInfo parametr, ten parametr zastępuje domyślne zachowanie interfejsu API.
  • System.String elementy członkowskie, w których pierwszy parametr ma typ char (na przykład String.IndexOf(Char)), używają wyszukiwania porządkowego, chyba że obiekt wywołujący przekazuje jawny StringComparison argument określający CurrentCulture[IgnoreCase] lub InvariantCulture[IgnoreCase].

Aby uzyskać bardziej szczegółową analizę domyślnego zachowania każdego String interfejsu API, zobacz sekcję Domyślne typy wyszukiwania i porównania.

Porządkowe a wyszukiwanie językowe i porównanie

Wyszukiwanie porządkowe (znane również jako wyszukiwanie nielingwistyczne) i porównywanie rozkłada ciąg na poszczególne char elementy i wykonuje wyszukiwanie znak po znakach lub porównanie. Na przykład ciągi "dog" i "dog" porównaj je jako równe w porównaniu Ordinal , ponieważ dwa ciągi składają się z dokładnie tej samej sekwencji znaków. "dog" Jednak i "Dog" porównaj je jako nie równe w ramach Ordinal porównania, ponieważ nie składają się one z dokładnie tej samej sekwencji znaków. Oznacza to, że punkt U+0044 kodu wielkiej litery występuje przed małymi literami w punkcie 'D''d'U+0064kodu , co powoduje "Dog" sortowanie przed ."dog"

Porównanie OrdinalIgnoreCase nadal działa na podstawie char-by-char, ale eliminuje różnice wielkości liter podczas wykonywania operacji. OrdinalIgnoreCase W ramach porównania pary 'd' char i 'D' porównaj je jako równe, podobnie jak pary 'á' char i 'Á'. Ale nieprzychycony znak 'a' porównuje się jako nie równy akcentowanemu znakowi 'á'.

Oto kilka przykładów przedstawionych w poniższej tabeli:

Ciąg 1 Ciąg 2 Ordinal porównanie OrdinalIgnoreCase porównanie
"dog" "dog" równa się równa się
"dog" "Dog" nie równa się równa się
"resume" "résumé" nie równa się nie równa się

Unicode umożliwia również ciągom kilka różnych reprezentacji w pamięci. Na przykład e-ostre (é) można przedstawić na dwa możliwe sposoby:

  • Pojedynczy znak literału 'é' (napisany również jako '\u00E9').
  • Literał nieprzychytowany 'e' znak, po którym następuje łączący znak '\u0301'modyfikatora akcentu .

Oznacza to, że wszystkie następujące cztery ciągi są wyświetlane jako "résumé", mimo że ich elementy składowe są różne. Ciągi używają kombinacji znaków literałów 'é' lub literałów 'e' bez znaku plus łączący modyfikator '\u0301'akcentu .

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

W ramach modułu porównania porządkowego żaden z tych ciągów nie jest porównywany jako równy sobie. Dzieje się tak, ponieważ wszystkie zawierają różne podstawowe sekwencje znaków, mimo że gdy są one renderowane na ekranie, wszystkie wyglądają tak samo.

Podczas wykonywania string.IndexOf(..., StringComparison.Ordinal) operacji środowisko uruchomieniowe szuka dokładnego dopasowania podciągów. Wyniki są następujące.

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'

Porządkowe procedury wyszukiwania i porównywania nigdy nie mają wpływu na ustawienie kultury bieżącego wątku.

Procedury wyszukiwania językowego i porównywania rozkładają ciąg na elementy sortowania i wykonują wyszukiwania lub porównania na tych elementach. Nie musi istnieć mapowanie 1:1 między znakami ciągu a elementami sortowania składników. Na przykład ciąg o długości 2 może składać się tylko z jednego elementu sortowania. Gdy dwa ciągi są porównywane w sposób obsługujący język, porównujący sprawdza, czy dwa elementy sortowania ciągów mają takie samo znaczenie semantyczne, nawet jeśli znaki literału ciągu są różne.

Rozważ ponownie ciąg "résumé" i jego cztery różne reprezentacje. W poniższej tabeli przedstawiono każdą reprezentację podzieloną na elementy sortowania.

String Jako elementy sortowania
"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"

Element sortowania odnosi się luźno do tego, co czytelnicy uważają za pojedynczy znak lub klaster znaków. Jest on koncepcyjnie podobny do klastra grafu, ale obejmuje nieco większy parasol.

W ramach porównania językowego dokładne dopasowania nie są konieczne. Zamiast tego elementy sortowania są porównywane na podstawie ich znaczenia semantycznego. Na przykład porównujący lingwistyczny traktuje podciągy "\u00E9" i "e\u0301" jako równe, ponieważ obie te wartości oznaczają "małe litery e z ostrym modyfikatorem akcentu". Dzięki temu metoda jest IndexOf zgodna z podciągem w większym ciągu, który zawiera semantycznie równoważne podciąg "e\u0301" , "\u00E9"jak pokazano w poniższym przykładzie kodu.

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'

W związku z tym dwa ciągi o różnych długościach mogą być porównywane jako równe, jeśli jest używane porównanie językowe. Osoby wywołujące powinny dbać o logikę specjalnej wielkości liter, która zajmuje się długością ciągu w takich scenariuszach.

Procedury wyszukiwania i porównywania z uwzględnieniem kultury to specjalna forma procedur wyszukiwania językowego i porównywania. W ramach porównania obsługującego kulturę pojęcie elementu sortowania jest rozszerzone w celu uwzględnienia informacji specyficznych dla określonej kultury.

Na przykład w alfabetu węgierskim, gdy dwa znaki <dz> pojawiają się z powrotem do tyłu, są uważane za własną unikatową literę odrębną od <d> lub <z>. Oznacza to, że gdy <dz> jest widoczny w ciągu, węgierski porównujący z kulturą traktuje go jako pojedynczy element sortowania.

String Jako elementy sortowania Uwagi
"endz" "e" + "n" + "d" + "z" (przy użyciu standardowego porównania językowego)
"endz" "e" + "n" + "dz" (przy użyciu węgierskich porównań świadomych kultury)

W przypadku korzystania z węgierskiego porównania obsługującego kulturę oznacza to, że ciąg "endz" nie kończy się podciągem "z", ponieważ <dz> i <z> są traktowane jako elementy sortowania o innym znaczeniu semantycznym.

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

Uwaga

  • Zachowanie: Porównania językowe i oparte na kulturze mogą być poddawane korektom zachowań od czasu do czasu. Zarówno ICU, jak i starszy obiekt równoważenia obciążenia sieciowego systemu Windows są aktualizowane w celu uwzględnienia sposobu zmiany języków światowych. Aby uzyskać więcej informacji, zobacz wpis w blogu Dotyczący zmian danych ustawień regionalnych (kultury). Zachowanie porównania porządkowego nigdy się nie zmieni, ponieważ wykonuje dokładne wyszukiwanie bitowe i porównywanie. Jednak zachowanie modułu porównującego OrdinalIgnoreCase może ulec zmianie w miarę wzrostu standardu Unicode w celu objęcia większej liczby zestawów znaków i poprawia pominięcie istniejących danych wielkości liter.
  • Użycie: porównania StringComparison.InvariantCulture i StringComparison.InvariantCultureIgnoreCase są porównaniami językowymi, które nie są świadome kultury. Oznacza to, że te porównania rozumieją pojęcia, takie jak akcentowany znak o wielu możliwych reprezentacjach bazowych, i że wszystkie takie reprezentacje powinny być traktowane równie. Jednak porównania językowe bez znajomości kultury nie będą zawierać specjalnej obsługi dz <> jako odrębnej od <d> lub <z>, jak pokazano powyżej. Nie będą również specjalnymi znakami, takimi jak niemiecki Eszett (ß).

Platforma .NET oferuje również niezmienny tryb globalizacji. Ten tryb zgody wyłącza ścieżki kodu, które zajmują się wyszukiwaniem językowym i procedurami porównywania. W tym trybie wszystkie operacje używają zachowań porządkowych lub porządkowychIgnoreCase, niezależnie od tego, co CultureInfo lub StringComparison argument zapewnia obiekt wywołujący. Aby uzyskać więcej informacji, zobacz Opcje konfiguracji środowiska uruchomieniowego dla globalizacji i Tryb niezmienny globalizacji platformy .NET Core.

Aby uzyskać więcej informacji, zobacz Najlepsze rozwiązania dotyczące porównywania ciągów na platformie .NET.

Implikacje dotyczące zabezpieczeń

Jeśli aplikacja używa objętego interfejsu API do filtrowania, zalecamy włączenie reguł analizy kodu CA1307 i CA1309 w celu ułatwienia lokalizowania miejsc, w których wyszukiwanie językowe mogło zostać przypadkowo użyte zamiast wyszukiwania porządkowego. Wzorce kodu, takie jak poniższe, mogą być podatne na luki w zabezpieczeniach.

//
// 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) Ponieważ metoda domyślnie używa wyszukiwania językowego, istnieje możliwość, że ciąg zawiera literał '<' lub '&' znak, a string.IndexOf(string) procedura zwraca -1wartość , wskazując, że podciąg wyszukiwania nie został znaleziony. Reguły analizy kodu CA1307 i CA1309 flagują takie witryny połączeń i ostrzegają dewelopera, że istnieje potencjalny problem.

Domyślne typy wyszukiwania i porównania

W poniższej tabeli wymieniono domyślne typy wyszukiwania i porównania dla różnych interfejsów API ciągów i podobnych do ciągu. Jeśli obiekt wywołujący udostępnia jawny CultureInfo lub StringComparison parametr, ten parametr zostanie uhonorowany za pomocą dowolnej wartości domyślnej.

interfejs API Zachowanie domyślne Uwagi
string.Compare CurrentCulture
string.CompareTo CurrentCulture
string.Contains Liczba porządkowa
string.EndsWith Liczba porządkowa (gdy pierwszy parametr to )char
string.EndsWith CurrentCulture (gdy pierwszy parametr to )string
string.Equals Liczba porządkowa
string.GetHashCode Liczba porządkowa
string.IndexOf Liczba porządkowa (gdy pierwszy parametr to )char
string.IndexOf CurrentCulture (gdy pierwszy parametr to )string
string.IndexOfAny Liczba porządkowa
string.LastIndexOf Liczba porządkowa (gdy pierwszy parametr to )char
string.LastIndexOf CurrentCulture (gdy pierwszy parametr to )string
string.LastIndexOfAny Liczba porządkowa
string.Replace Liczba porządkowa
string.Split Liczba porządkowa
string.StartsWith Liczba porządkowa (gdy pierwszy parametr to )char
string.StartsWith CurrentCulture (gdy pierwszy parametr to )string
string.ToLower CurrentCulture
string.ToLowerInvariant Niezmiennaculture
string.ToUpper CurrentCulture
string.ToUpperInvariant Niezmiennaculture
string.Trim Liczba porządkowa
string.TrimEnd Liczba porządkowa
string.TrimStart Liczba porządkowa
string == string Liczba porządkowa
string != string Liczba porządkowa

W przeciwieństwie do string interfejsów API wszystkie MemoryExtensions interfejsy API domyślnie wykonują wyszukiwania porządkowe i porównania z następującymi wyjątkami.

interfejs API Zachowanie domyślne Uwagi
MemoryExtensions.ToLower CurrentCulture (w przypadku przekazania argumentu o wartości null CultureInfo )
MemoryExtensions.ToLowerInvariant Niezmiennaculture
MemoryExtensions.ToUpper CurrentCulture (w przypadku przekazania argumentu o wartości null CultureInfo )
MemoryExtensions.ToUpperInvariant Niezmiennaculture

Konsekwencją jest to, że podczas konwertowania kodu z używania na korzystanie string ReadOnlySpan<char>z programu zmiany behawioralne mogą zostać przypadkowo wprowadzone. Poniżej przedstawiono przykład.

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

Zalecanym sposobem rozwiązania tego problemu jest przekazanie jawnego StringComparison parametru do tych interfejsów API. Reguły analizy kodu CA1307 i CA1309 mogą w tym pomóc.

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

Zobacz też