Beteendeändringar vid jämförelse av strängar på .NET 5+
.NET 5 introducerar en beteendeförändring vid körning där globaliserings-API:er använder ICU som standard på alla plattformar som stöds. Detta är en avvikelse från tidigare versioner av .NET Core och från .NET Framework, som använder operativsystemets nls-funktioner (national language support) när de körs på Windows. Mer information om dessa ändringar, inklusive kompatibilitetsväxlar som kan återställa beteendeändringen, finns i .NET-globalisering och ICU.
Orsak till förändring
Den här ändringen infördes för att förena . NET:s globaliseringsbeteende i alla operativsystem som stöds. Det ger också möjlighet för program att paketerar sina egna globaliseringsbibliotek i stället för att vara beroende av operativsystemets inbyggda bibliotek. Mer information finns i meddelandet om icke-bakåtkompatibla ändringar.
Beteendeskillnader
Om du använder funktioner som string.IndexOf(string) utan att anropa överbelastningen som tar ett StringComparison argument kan du tänka dig att utföra en ordningstalssökning , men i stället oavsiktligt beroende av kulturspecifikt beteende. Eftersom NLS och ICU implementerar olika logik i sina språkliga jämförelser kan resultaten av metoder som kan string.IndexOf(string) returnera oväntade värden.
Detta kan visa sig även på platser där du inte alltid förväntar dig att globaliseringsanläggningar ska vara aktiva. Följande kod kan till exempel ge ett annat svar beroende på den aktuella körningen.
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)
Mer information finns i Globaliserings-API:er använder ICU-bibliotek på Windows.
Skydda mot oväntat beteende
Det här avsnittet innehåller två alternativ för att hantera oväntade beteendeändringar i .NET 5.
Aktivera kodanalysverktyg
Kodanalysverktyg kan identifiera buggiga anropsplatser. För att skydda dig mot överraskande beteenden rekommenderar vi att du aktiverar .NET-kompilatorplattformsanalyser (Roslyn) i projektet. Analysverktygen hjälper till att flagga kod som oavsiktligt använder en språklig jämförelse när en ordningstalsjäxare troligen var avsedd. Följande regler bör hjälpa till att flagga dessa problem:
- CA1307: Ange StringComparison för tydlighetens skull
- CA1309: Använd ordningstalssträngComparison
- CA1310: Ange StringComparison för korrekthet
Dessa specifika regler är inte aktiverade som standard. Om du vill aktivera dem och visa eventuella överträdelser som byggfel anger du följande egenskaper i projektfilen:
<PropertyGroup>
<AnalysisMode>All</AnalysisMode>
<WarningsAsErrors>$(WarningsAsErrors);CA1307;CA1309;CA1310</WarningsAsErrors>
</PropertyGroup>
Följande kodfragment visar exempel på kod som producerar relevanta kodanalysvarningar eller fel.
//
// 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);
När du instansierar en sorterad samling strängar eller sorterar en befintlig strängbaserad samling anger du på samma sätt en explicit jämförelse.
//
// 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);
Återgå till NLS-beteenden
Om du vill återställa .NET 5+-program till äldre NLS-beteenden när de körs på Windows följer du stegen i .NET Globalization och ICU. Den här programomfattande kompatibilitetsväxeln måste anges på programnivå. Enskilda bibliotek kan inte anmäla sig eller välja bort det här beteendet.
Tips
Vi rekommenderar starkt att du aktiverar kodanalysreglerna CA1307, CA1309 och CA1310 för att förbättra kodhygienen och identifiera eventuella befintliga latenta buggar. Mer information finns i Aktivera kodanalysverktyg.
Berörda API:er
De flesta .NET-program stöter inte på oväntade beteenden på grund av ändringarna i .NET 5. Men på grund av antalet berörda API:er och hur grundläggande dessa API:er är för det bredare .NET-ekosystemet bör du vara medveten om risken för att .NET 5 introducerar oönskade beteenden eller exponerar latenta buggar som redan finns i ditt program.
De berörda API:erna omfattar:
- System.String.Compare
- System.String.EndsWith
- System.String.IndexOf
- System.String.StartsWith
- System.String.ToLower
- System.String.ToLowerInvariant
- System.String.ToUpper
- System.String.ToUpperInvariant
- System.Globalization.TextInfo (de flesta medlemmar)
- System.Globalization.CompareInfo (de flesta medlemmar)
- System.Array.Sort (vid sortering av matriser med strängar)
- System.Collections.Generic.List<T>.Sort() (när listelementen är strängar)
- System.Collections.Generic.SortedDictionary<TKey,TValue> (när nycklarna är strängar)
- System.Collections.Generic.SortedList<TKey,TValue> (när nycklarna är strängar)
- System.Collections.Generic.SortedSet<T> (när uppsättningen innehåller strängar)
Anteckning
Det här är inte en fullständig lista över berörda API:er.
Alla ovanstående API:er använder språklig strängsökning och jämförelse med trådens aktuella kultur, som standard. Skillnaderna mellan språklig och ordningstalssökning och jämförelse framhävs i Ordinal kontra språklig sökning och jämförelse.
Eftersom ICU implementerar språkliga strängjämförelser som skiljer sig från NLS kan Windows-baserade program som uppgraderar till .NET 5 från en tidigare version av .NET Core eller .NET Framework och som anropar ett av de berörda API:erna märka att API:erna börjar uppvisa olika beteenden.
Undantag
- Om ett API accepterar en explicit
StringComparisonparameter ellerCultureInfoparameter åsidosätter parametern API:ets standardbeteende. System.Stringmedlemmar där den första parametern är av typenchar(till exempel String.IndexOf(Char)) använder ordningstalssökning, såvida inte anroparen skickar ett explicitStringComparisonargument som angerCurrentCulture[IgnoreCase]ellerInvariantCulture[IgnoreCase].
En mer detaljerad analys av standardbeteendet för varje String API finns i avsnittet Standardsök- och jämförelsetyper .
Ordningstal kontra språklig sökning och jämförelse
Ordningstalssökning (även kallat icke-språklig) sökning och jämförelse delar upp en sträng i dess enskilda char element och utför en char-by-char-sökning eller jämförelse. Till exempel strängarna "dog" och "dog" jämför som lika under en Ordinal jämförelse, eftersom de två strängarna består av exakt samma sekvens av tecken. "dog" Men jämför "Dog" som inte lika med en Ordinal jämförelse, eftersom de inte består av exakt samma sekvens av tecken. Det innebär att versalers 'D'kodpunkt U+0044 inträffar före gemenernas 'd'kodpunkt U+0064, vilket resulterar i "dog" sortering före "Dog".
En OrdinalIgnoreCase jämförelse fungerar fortfarande efter tecken, men den eliminerar skiftlägesskillnader när åtgärden utförs. Under en OrdinalIgnoreCase jämförelse parar 'd' tecken och 'D' jämför som lika, liksom char-paren 'á' och 'Á'. Men det obevakade tecken 'a' jämförs som inte lika med det accenterade tecken 'á'.
Några exempel på detta finns i följande tabell:
| Sträng 1 | Sträng 2 | Ordinal Jämförelse |
OrdinalIgnoreCase Jämförelse |
|---|---|---|---|
"dog" |
"dog" |
equal | equal |
"dog" |
"Dog" |
inte lika med | equal |
"resume" |
"résumé" |
inte lika med | inte lika med |
Unicode tillåter också att strängar har flera olika minnesinterna representationer. Till exempel kan en e-akut (é) representeras på två möjliga sätt:
- Ett enda literaltecken
'é'(även skrivet som'\u00E9'). - Ett literaltecken
'e'som inte stöds följt av ett kombinerat dekorfärgsmodifierartecken'\u0301'.
Det innebär att följande fyra strängar alla visas som "résumé", även om deras beståndsdelar är olika. Strängarna använder en kombination av literaltecken 'é' eller literala obevakade 'e' tecken plus den kombinerade accentmodifieraren '\u0301'.
"r\u00E9sum\u00E9""r\u00E9sume\u0301""re\u0301sum\u00E9""re\u0301sume\u0301"
Under en ordningsjäxare jämförs ingen av dessa strängar som lika med varandra. Det beror på att de alla innehåller olika underliggande teckensekvenser, även om de ser likadana ut när de återges på skärmen.
När du utför en string.IndexOf(..., StringComparison.Ordinal) åtgärd letar körningen efter en exakt delsträngsmatchning. Resultatet är följande.
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'
Ordningstalssökning och jämförelserutiner påverkas aldrig av den aktuella trådens kulturinställning.
Språkliga sök- och jämförelserutiner delar upp en sträng i sorteringselement och utför sökningar eller jämförelser på dessa element. Det finns inte nödvändigtvis en 1:1-mappning mellan en strängs tecken och dess konstituerande sorteringselement. En sträng med längd 2 kan till exempel bara bestå av ett enda sorteringselement. När två strängar jämförs på ett språkligt medvetet sätt kontrollerar jämförelsen om de två strängarnas sorteringselement har samma semantiska betydelse, även om strängens literaltecken skiljer sig åt.
Överväg återigen strängen "résumé" och dess fyra olika representationer. I följande tabell visas varje representation uppdelad i dess sorteringselement.
| Sträng | Som sorteringselement |
|---|---|
"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" |
Ett sorteringselement motsvarar löst vad läsarna skulle betrakta som ett enskilt tecken eller kluster med tecken. Det är konceptuellt likt ett grapheme-kluster men omfattar ett något större paraply.
Under en språklig jämförelse är exakta matchningar inte nödvändiga. Sorteringselement jämförs i stället baserat på deras semantiska betydelse. Till exempel behandlar en språklig jämförelse substrings "\u00E9" och "e\u0301" som lika eftersom de båda semantiskt betyder "en gemen e med en akut accentmodifierare.". På så IndexOf sätt kan metoden matcha delsträngen "e\u0301" i en större sträng som innehåller den semantiskt likvärdiga delsträngen "\u00E9", enligt följande kodexempel.
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'
Därför kan två strängar med olika längd jämföras som lika om en språklig jämförelse används. Anropare bör vara noga med att inte använda specialfallslogik som hanterar stränglängd i sådana scenarier.
Kulturmedvetna sök- och jämförelserutiner är en särskild form av språklig sökning och jämförelserutiner. I en kulturmedveten jämförelse utökas begreppet sorteringselement till att omfatta information som är specifik för den angivna kulturen.
I det ungerska alfabetet anses de till exempel vara egna unika bokstäver som skiljer sig från d <> eller <z> när de två tecknen <dz> visas back-to-back. Det innebär att när <dz> ses i en sträng behandlar en ungersk kulturmedveten jämförelse det som ett enda sorteringselement.
| Sträng | Som sorteringselement | Kommentarer |
|---|---|---|
"endz" |
"e" + "n" + "d" + "z" |
(med en standardspråklig jämförelse) |
"endz" |
"e" + "n" + "dz" |
(med hjälp av en ungersk kulturmedveten jämförelse) |
När du använder en ungersk kulturmedveten jämförelse innebär det att strängen "endz"inte slutar med delsträngen "z", eftersom <\dz> och <\z> betraktas som sorteringselement med annan semantisk betydelse.
// 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'
Anteckning
- Beteende: Språkliga och kulturmedvetna jämförelseverktyg kan då och då genomgå beteendejusteringar. Både ICU och den äldre Windows NLS-anläggningen uppdateras för att ta hänsyn till hur världsspråk förändras. Mer information finns i dataomsättningen för nationella inställningar (kultur). Ordningstalsjämförarens beteende ändras aldrig eftersom den utför exakt bitvis sökning och jämförelse. Men OrdinalIgnoreCase-jämförelsens beteende kan ändras när Unicode växer till att omfatta fler teckenuppsättningar och korrigerar utelämnanden i befintliga casing-data.
- Användning: Jämförelserna
StringComparison.InvariantCultureochStringComparison.InvariantCultureIgnoreCaseär språkliga jämförelseverktyg som inte är kulturmedvetna. Det innebär att dessa jämförelseverktyg förstår begrepp som att accenttecken é har flera möjliga underliggande representationer och att alla sådana representationer ska behandlas lika. Men icke-kulturmedvetna språkjäxare innehåller inte särskild hantering för <dz> som skiljer sig från <d> eller <z>, som du ser ovan. De kommer inte heller specialtecken som tyska Eszett (ß).
.NET erbjuder också det invarianta globaliseringsläget. Det här opt-in-läget inaktiverar kodsökvägar som hanterar språkliga sök- och jämförelserutiner. I det här läget använder alla åtgärder ordningstals - eller ordningstalstal , oavsett vad CultureInfo eller StringComparison argument anroparen tillhandahåller. Mer information finns i Körningskonfigurationsalternativ för globalisering och .NET Core Globalization Invariant Mode.
Mer information finns i Metodtips för att jämföra strängar i .NET.
Säkerhetsaspekter
Om din app använder ett berört API för filtrering rekommenderar vi att du aktiverar kodanalysreglerna CA1307 och CA1309 för att hitta platser där en språklig sökning oavsiktligt kan ha använts i stället för en ordningstalssökning. Kodmönster som följande kan vara sårbara för säkerhetsexploateringar.
//
// 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) Eftersom metoden använder en språklig sökning som standard är det möjligt att en sträng innehåller en literal '<' eller '&' ett tecken och att rutinen string.IndexOf(string) returnerar -1, vilket indikerar att sökundersträngen inte hittades. Kodanalysreglerna CA1307 och CA1309 flaggar sådana anropswebbplatser och varnar utvecklaren om att det finns ett potentiellt problem.
Standardtyper för sökning och jämförelse
I följande tabell visas standardsök- och jämförelsetyperna för olika sträng- och strängliknande API:er. Om anroparen tillhandahåller en explicit CultureInfo parameter eller StringComparison parameter, kommer den parametern att respekteras över alla standardvärden.
| API | Standardbeteende | Kommentarer |
|---|---|---|
string.Compare |
CurrentCulture | |
string.CompareTo |
CurrentCulture | |
string.Contains |
Ordningstal | |
string.EndsWith |
Ordningstal | (när den första parametern är en char) |
string.EndsWith |
CurrentCulture | (när den första parametern är en string) |
string.Equals |
Ordningstal | |
string.GetHashCode |
Ordningstal | |
string.IndexOf |
Ordningstal | (när den första parametern är en char) |
string.IndexOf |
CurrentCulture | (när den första parametern är en string) |
string.IndexOfAny |
Ordningstal | |
string.LastIndexOf |
Ordningstal | (när den första parametern är en char) |
string.LastIndexOf |
CurrentCulture | (när den första parametern är en string) |
string.LastIndexOfAny |
Ordningstal | |
string.Replace |
Ordningstal | |
string.Split |
Ordningstal | |
string.StartsWith |
Ordningstal | (när den första parametern är en char) |
string.StartsWith |
CurrentCulture | (när den första parametern är en string) |
string.ToLower |
CurrentCulture | |
string.ToLowerInvariant |
InvariantCulture | |
string.ToUpper |
CurrentCulture | |
string.ToUpperInvariant |
InvariantCulture | |
string.Trim |
Ordningstal | |
string.TrimEnd |
Ordningstal | |
string.TrimStart |
Ordningstal | |
string == string |
Ordningstal | |
string != string |
Ordningstal |
Till skillnad från string API:er utför alla MemoryExtensions API:er ordningstalssökningar och jämförelser som standard, med följande undantag.
| API | Standardbeteende | Kommentarer |
|---|---|---|
MemoryExtensions.ToLower |
CurrentCulture | (när ett null-argument CultureInfo har skickats) |
MemoryExtensions.ToLowerInvariant |
InvariantCulture | |
MemoryExtensions.ToUpper |
CurrentCulture | (när ett null-argument CultureInfo har skickats) |
MemoryExtensions.ToUpperInvariant |
InvariantCulture |
En konsekvens är att beteendeändringar kan introduceras oavsiktligt när du konverterar kod från användning string till användning ReadOnlySpan<char>. Ett exempel på detta följer.
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
Det rekommenderade sättet att åtgärda detta är att skicka en explicit StringComparison parameter till dessa API:er. Kodanalysreglerna CA1307 och CA1309 kan hjälpa till med detta.
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