Beteendeändringar vid jämförelse av strängar på .NET 5+

.NET 5 introducerar en beteendeförändring för 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 nationella språkstöd (NLS) när de körs i Windows. Mer information om dessa ändringar, inklusive kompatibilitetsväxlar som kan återställa beteendeändringen, finns i .NET-globalisering och ICU.

Orsak till ändringen

Den här ändringen infördes för att ena . 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, kanske du tänker utföra en ordningsvis sökning, men i stället oavsiktligt tar du ett beroende av kulturspecifikt beteende. Eftersom NLS och ICU implementerar olika logik i sina språkliga jämförelseverktyg kan resultatet av metoder som returnera string.IndexOf(string) 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 i 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 kodanalyserare

Kodanalyserare kan identifiera buggiga samtalswebbplatser. 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 kan använda en språklig jämförelse när en ordningsjäxare troligen var avsedd. Följande regler bör hjälpa till att flagga dessa problem:

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 genererar 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 i 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.

Dricks

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 kodanalyserare.

Berörda API:er

De flesta .NET-program stöter inte på några 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:

Kommentar

Det här är inte en fullständig lista över berörda API:er.

Alla API:er ovan använder språklig strängsökning och jämförelse med trådens aktuella kultur som standard. Skillnaderna mellan språklig och ordningssam sökning och jämförelse beskrivs i Ordinal kontra språklig sökning och jämförelse.

Eftersom ICU implementerar språkliga strängjämförelser på ett annat sätt ä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 StringComparison parameter eller CultureInfo parameter åsidosätter den parametern API:ets standardbeteende.
  • System.String medlemmar där den första parametern är av typen char (till exempel String.IndexOf(Char)) använder ordningstalssökning, såvida inte anroparen skickar ett explicit StringComparison argument som anger CurrentCulture[IgnoreCase] eller InvariantCulture[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 kodpunkt U+0044 inträffar före gemeners 'd'kodpunktU+0064, vilket resulterar i "Dog" sortering före "dog".'D'

En OrdinalIgnoreCase jämförelse fungerar fortfarande efter tecken, men eliminerar skiftlägesskillnader när åtgärden utförs. Under en OrdinalIgnoreCase jämförelse parar 'd''D' och jämför char-paren som lika, liksom char-paren 'á' och 'Á'. Men det ouppnämda 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" lika med lika med
"dog" "Dog" inte lika med lika med
"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 enliteralt 'é' tecken (även skrivet som '\u00E9').
  • Ett literaltecken 'e' som inte stöds följt av ett kombinerat dekormodifierartecken '\u0301'.

Det innebär att följande fyra strängar alla visas som "résumé", även om deras komponenter skiljer sig åt. Strängarna använder en kombination av literaltecken 'é' eller literala oaccenterade 'e' tecken plus den kombinerande 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åksöknings - 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 ingående 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 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.

String 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 ett 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'

Som en följd av detta kan två strängar med olika längd jämföras som lika om en språklig jämförelse används. Uppringare 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åkliga sök- 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 till exempel det ungerska alfabetet, när de två tecknen <dz> visas back-to-back, anses de vara sin egen unika bokstav som skiljer sig från antingen <d> eller <z>. Det innebär att när <dz> ses i en sträng behandlar en ungersk kulturmedveten jämförelse den som ett enda sorteringselement.

String 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 olika 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'

Kommentar

  • Beteende: Språkliga och kulturmedvetna jämförelser kan genomgå beteendejusteringar då och då. Både ICU och den äldre Windows NLS-anläggningen uppdateras för att ta hänsyn till hur världsspråken förändras. Mer information finns i blogginlägget Språkdataomsättning (kultur). Ordningstalsjämförarens beteende ändras aldrig eftersom det utför exakt bitvis sökning och jämförelse. OrdinalIgnoreCase-jämförelsens beteende kan dock ändras när Unicode växer till att omfatta fler teckenuppsättningar och korrigerar utelämnanden i befintliga höljedata.
  • Användning: Jämförelsen StringComparison.InvariantCulture och StringComparison.InvariantCultureIgnoreCase är språkliga jämförelseverktyg som inte är kulturmedvetna. Det innebär att dessa jämförelseverktyg förstår begrepp som den accenterade tecken é som har flera möjliga underliggande representationer, och att alla sådana representationer bör behandlas lika. Men icke-kulturmedvetna språkjäxare kommer inte att innehålla särskild hantering för <dz> som skiljer sig från <d> eller <z>, som visas ovan. De kommer inte heller specialfallstecken som tyska Eszett (ß).

.NET erbjuder också det invarianta globaliseringsläget. Det här opt-in-läget inaktiverar kodsökvägar som hanterar språksöknings- och jämförelserutiner. I det här läget använder alla åtgärder ordningstals - eller ordningstalsbeteenden , 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äkerhetskonsekvenser

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åksökning oavsiktligt kan ha använts i stället för en ordningstalssökning. Kodmönster som följande kan vara känsliga 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. Kodanalysregler 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 Ordning
string.EndsWith Ordning (när den första parametern är en char)
string.EndsWith CurrentCulture (när den första parametern är en string)
string.Equals Ordning
string.GetHashCode Ordning
string.IndexOf Ordning (när den första parametern är en char)
string.IndexOf CurrentCulture (när den första parametern är en string)
string.IndexOfAny Ordning
string.LastIndexOf Ordning (när den första parametern är en char)
string.LastIndexOf CurrentCulture (när den första parametern är en string)
string.LastIndexOfAny Ordning
string.Replace Ordning
string.Split Ordning
string.StartsWith Ordning (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 Ordning
string.TrimEnd Ordning
string.TrimStart Ordning
string == string Ordning
string != string Ordning

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 skickades)
MemoryExtensions.ToLowerInvariant InvariantCulture
MemoryExtensions.ToUpper CurrentCulture (när ett null-argument CultureInfo skickades)
MemoryExtensions.ToUpperInvariant InvariantCulture

En konsekvens är att när du konverterar kod från användning string till att använda ReadOnlySpan<char>kan beteendeändringar introduceras oavsiktligt. 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

Se även