Gedrag verandert bij het vergelijken van tekenreeksen op .NET 5+

.NET 5 introduceert een runtime-gedragswijziging waarbij globalisatie-API's standaard gebruikmaken van ICU op alle ondersteunde platforms. Dit is een afwijking van eerdere versies van .NET Core en van .NET Framework, die gebruikmaken van de nlS-functionaliteit (National Language Support) van het besturingssysteem wanneer deze wordt uitgevoerd in Windows. Zie .NET-globalisering en ICU voor meer informatie over deze wijzigingen, inclusief compatibiliteitsswitches die de gedragswijziging kunnen terugdraaien.

Reden voor wijziging

Deze wijziging is geïntroduceerd om elkaar te samenvoegen. Het globalisatiegedrag van NET voor alle ondersteunde besturingssystemen. Het biedt ook de mogelijkheid voor toepassingen om hun eigen globalisatiebibliotheken te bundelen in plaats van afhankelijk te zijn van de ingebouwde bibliotheken van het besturingssysteem. Zie de melding voor wijziging die fouten veroorzaken voor meer informatie.

Gedragsverschillen

Als u functies gebruikt, zoals string.IndexOf(string) zonder de overbelasting aan te roepen die een StringComparison argument gebruikt, bent u misschien van plan een ordinale zoekopdracht uit te voeren, maar in plaats daarvan neemt u per ongeluk een afhankelijkheid van cultuurspecifiek gedrag. Omdat NLS en ICU verschillende logica in hun taalkundige vergelijkingen implementeren, kunnen de resultaten van methoden zoals string.IndexOf(string) onverwachte waarden retourneren.

Dit kan zich zelfs op plaatsen waar u niet altijd verwacht dat globaliseringsfaciliteiten actief zijn. De volgende code kan bijvoorbeeld een ander antwoord produceren, afhankelijk van de huidige 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)

Zie Globalization-API's voor meer informatie over ICU-bibliotheken in Windows.

Bescherming tegen onverwacht gedrag

Deze sectie bevat twee opties voor het omgaan met onverwachte gedragswijzigingen in .NET 5.

Codeanalyses inschakelen

Code analyzers kunnen mogelijk buggy call sites detecteren. We raden u aan om .NET-compilerplatformanalyses (Roslyn) in uw project in te schakelen om u te beschermen tegen eventuele verrassende gedragingen. De analyzers helpen code te markeren die per ongeluk een taalkundige vergelijking kan gebruiken wanneer een ordinale vergelijking waarschijnlijk was bedoeld. De volgende regels moeten helpen bij het markeren van deze problemen:

Deze specifieke regels zijn niet standaard ingeschakeld. Als u deze wilt inschakelen en schendingen wilt weergeven als buildfouten, stelt u de volgende eigenschappen in uw projectbestand in:

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

In het volgende codefragment ziet u voorbeelden van code die de relevante waarschuwingen of fouten voor codeanalyse produceert.

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

Geef bij het instantiëren van een gesorteerde verzameling tekenreeksen of het sorteren van een bestaande verzameling op basis van tekenreeksen een expliciete vergelijking op.

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

Terugkeren naar NLS-gedrag

Volg de stappen in .NET Globalization en ICU om terug te keren naar oudere NLS-gedragingen bij het uitvoeren van Windows. Deze compatibiliteitsswitch voor de hele toepassing moet worden ingesteld op toepassingsniveau. Afzonderlijke bibliotheken kunnen zich niet afmelden of afmelden voor dit gedrag.

Tip

We raden u ten zeerste aan de regels voor codeanalyse van CA1307, CA1309 en CA1310 in te schakelen om de hygiëne van code te verbeteren en eventuele bestaande latente bugs te detecteren. Zie Code analyzers inschakelen voor meer informatie.

Betrokken API's

De meeste .NET-toepassingen ondervinden geen onverwacht gedrag vanwege de wijzigingen in .NET 5. Vanwege het aantal betrokken API's en hoe basis deze API's zijn voor het bredere .NET-ecosysteem, moet u zich echter bewust zijn van het potentieel voor .NET 5 om ongewenst gedrag te introduceren of latente bugs bloot te stellen die al in uw toepassing aanwezig zijn.

De betrokken API's zijn onder andere:

Notitie

Dit is geen volledige lijst met betrokken API's.

Alle bovenstaande API's maken standaard gebruik van taalkundige tekenreeksen zoeken en vergelijken met behulp van de huidige cultuur van de thread. De verschillen tussen linguïstische en ordinale zoekopdrachten en vergelijkingen worden genoemd in het ordinaal versus taalkundige zoek- en vergelijkingsmateriaal.

Omdat ICU taalkundige tekenreeksvergelijkingen anders implementeert dan NLS, kunnen Windows-toepassingen die een upgrade uitvoeren naar .NET 5 van een eerdere versie van .NET Core of .NET Framework en die een van de betrokken API's aanroepen, merken dat de API's verschillende gedragingen vertonen.

Uitzonderingen

  • Als een API een expliciete StringComparison parameter CultureInfo accepteert, overschrijft die parameter het standaardgedrag van de API.
  • System.String leden waarvan de eerste parameter van het type char is (bijvoorbeeld String.IndexOf(Char)) gebruiken rangtelwoorden zoeken, tenzij de aanroeper een expliciet StringComparison argument doorgeeft of CurrentCulture[IgnoreCase]InvariantCulture[IgnoreCase].

Zie de sectie Standaardzoek- en vergelijkingstypen voor een gedetailleerdere analyse van het standaardgedrag van elke String API.

Ordinaal versus taalkundige zoekopdrachten en vergelijkingen

Ordinaal (ook wel bekend als niet-taalkundige) zoek- en vergelijkingsbewerkingen maken een tekenreeks af in de afzonderlijke char elementen en voert een teken-op-teken-zoekopdracht of vergelijking uit. De tekenreeksen "dog" en "dog" vergelijken als gelijk onder een Ordinal vergelijkingsfunctie, omdat de twee tekenreeksen uit exact dezelfde reeks tekens bestaan. "dog" En "Dog" vergelijken als niet gelijk aan een Ordinal vergelijkingsfunctie, omdat ze niet uit exact dezelfde reeks tekens bestaan. Dat wil gezegd: het codepunt U+0044 van hoofdletters 'D'vindt plaats vóór het codepunt U+0064van kleine letters'd', wat resulteert in "Dog" sorteren voor."dog"

Een OrdinalIgnoreCase comparer werkt nog steeds op basis van char-by-char, maar elimineert caseverschillen tijdens het uitvoeren van de bewerking. Onder een OrdinalIgnoreCase vergelijkingsfunctie vergelijken de tekenparen 'd' en 'D' vergelijken als gelijk, net als de tekenparen 'á' en 'Á'. Maar het niet-geaccenteerde teken 'a' vergelijkt zich als niet gelijk aan het accentteken 'á'.

In de volgende tabel ziet u enkele voorbeelden hiervan:

Tekenreeks 1 Tekenreeks 2 Ordinal Vergelijking OrdinalIgnoreCase Vergelijking
"dog" "dog" equal equal
"dog" "Dog" not equal equal
"resume" "résumé" not equal not equal

Met Unicode kunnen tekenreeksen ook verschillende weergaven in het geheugen hebben. Een e-acute (é) kan bijvoorbeeld op twee mogelijke manieren worden weergegeven:

  • Eén letterlijk 'é' teken (ook geschreven als '\u00E9').
  • Een letterlijk niet-geaccenteerd 'e' teken gevolgd door een combinatie van accentaanpassingsteken '\u0301'.

Dit betekent dat de volgende vier tekenreeksen allemaal worden weergegeven als "résumé", ook al zijn hun samenstellende stukken verschillend. De tekenreeksen gebruiken een combinatie van letterlijke 'é' tekens of letterlijke niet-begeleide 'e' tekens plus de combinatie van accentaanpassing '\u0301'.

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

Onder een ordinale vergelijking worden geen van deze tekenreeksen vergeleken als gelijk aan elkaar. Dit komt doordat ze allemaal verschillende onderliggende tekenreeksen bevatten, ook al zien ze er allemaal hetzelfde uit wanneer ze op het scherm worden weergegeven.

Bij het uitvoeren van een string.IndexOf(..., StringComparison.Ordinal) bewerking zoekt de runtime naar een exacte subtekenreeksovereenkomst. De resultaten zijn als volgt.

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'

Standaardzoek- en vergelijkingsroutines worden nooit beïnvloed door de cultuurinstelling van de huidige thread.

Taalkundige zoek- en vergelijkingsroutines ontleden een tekenreeks in sorteringselementen en voeren zoekopdrachten of vergelijkingen uit op deze elementen. Er is niet noodzakelijkerwijs een 1:1-toewijzing tussen de tekens van een tekenreeks en de bijbehorende samenvoegingselementen. Een tekenreeks met lengte 2 kan bijvoorbeeld bestaan uit slechts één sorteringselement. Wanneer twee tekenreeksen op taalkundige wijze worden vergeleken, controleert de vergelijkingsfunctie of de sorteringselementen van de twee tekenreeksen dezelfde semantische betekenis hebben, zelfs als de letterlijke tekens van de tekenreeks verschillen.

Bekijk opnieuw de tekenreeks "résumé" en de vier verschillende weergaven. In de volgende tabel ziet u elke weergave onderverdeeld in de sorteringselementen.

String Als sorteringselementen
"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"

Een sorteringselement komt losjes overeen met wat lezers zouden denken als één teken of cluster tekens. Het is conceptueel vergelijkbaar met een grapheme-cluster , maar omvat een iets grotere paraplu.

Onder een taalkundige vergelijking zijn exacte overeenkomsten niet nodig. Sorteringselementen worden in plaats daarvan vergeleken op basis van hun semantische betekenis. Een taalkundige vergelijking behandelt bijvoorbeeld de subtekenreeksen "\u00E9" en "e\u0301" is gelijk, omdat ze beide semantisch 'een kleine letter e met een acute accentaanpassing' betekenen. Hierdoor kan de IndexOf methode overeenkomen met de subtekenreeks "e\u0301" binnen een grotere tekenreeks die de semantisch equivalente subtekenreeks "\u00E9"bevat, zoals wordt weergegeven in het volgende codevoorbeeld.

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'

Als gevolg hiervan kunnen twee tekenreeksen van verschillende lengten als gelijk worden vergeleken als een taalkundige vergelijking wordt gebruikt. Bellers moeten ervoor zorgen dat er geen speciale logica wordt gebruikt die betrekking heeft op tekenreekslengte in dergelijke scenario's.

Cultuurbewuste zoek- en vergelijkingsroutines zijn een speciale vorm van taalkundige zoek- en vergelijkingsroutines. Onder een cultuurbewuste vergelijking wordt het concept van een sorteringselement uitgebreid met informatie die specifiek is voor de opgegeven cultuur.

Wanneer de twee tekens dz bijvoorbeeld in het Hongaarse alfabet als back-to-back worden weergegeven, worden ze beschouwd als hun eigen unieke letter die verschilt van <d> of <z>. >< Dit betekent dat wanneer <dz> in een tekenreeks wordt gezien, een Hongaarse cultuurbewuste vergelijkingsfunctie deze als één sorteringselement behandelt.

String Als sorteringselementen Opmerkingen
"endz" "e" + "n" + "d" + "z" (met behulp van een standaard linguïstische vergelijking)
"endz" "e" + "n" + "dz" (met een Hongaarse cultuurbewuste vergelijking)

Wanneer u een Hongaarse cultuurbewuste vergelijking gebruikt, betekent dit dat de tekenreeks "endz"niet eindigt op de subtekenreeks "z", omdat <dz> en <z> worden beschouwd als sorteringselementen met verschillende semantische betekenis.

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

Notitie

  • Gedrag: Taalkundige en cultuurbewuste vergelijkingen kunnen van tijd tot tijd gedragsaanpassingen ondergaan. Zowel ICU als de oudere Windows NLS-faciliteit worden bijgewerkt om te bepalen hoe wereldtalen veranderen. Zie het blogbericht Landinstellingen (cultuur) gegevensverloop voor meer informatie. Het gedrag van de ordinale vergelijkingsfunctie verandert nooit, omdat het exact bitsgewijze zoeken en vergelijken uitvoert. Het gedrag van de OrdinalIgnoreCase-vergelijkingsfunctie kan echter veranderen naarmate Unicode meer tekensets omvat en weglatingen in bestaande hoofdletters corrigeert.
  • Gebruik: De comparers en StringComparison.InvariantCultureIgnoreCase zijn taalkundige vergelijkingen StringComparison.InvariantCulture die niet cultuurbewust zijn. Dat wil gezegd, deze vergelijkingen begrijpen concepten zoals het accentteken met meerdere mogelijke onderliggende representaties en dat dergelijke representaties gelijk moeten worden behandeld. Maar niet-cultuurbewuste taalkundige vergelijkingen bevatten geen speciale verwerking voor <dz>, <<>>zoals hierboven wordt weergegeven. Ze zullen ook geen speciale karakters zoals de Duitse Eszett (ß) gebruiken.

.NET biedt ook de invariante globalisatiemodus. In deze opt-in-modus worden codepaden uitgeschakeld die betrekking hebben op taalkundige zoek- en vergelijkingsroutines. In deze modus maken alle bewerkingen gebruik van ordinaal of OrdinalIgnoreCase-gedrag, ongeacht het argument StringComparison dat CultureInfo de aanroeper biedt. Zie Runtime-configuratieopties voor globalisatie en .NET Core Globalization Invariant Mode voor meer informatie.

Zie Best practices voor het vergelijken van tekenreeksen in .NET voor meer informatie.

Gevolgen voor beveiliging

Als uw app gebruikmaakt van een betrokken API voor filteren, raden we u aan om de regels voor codeanalyse van CA1307 en CA1309 in te schakelen om te helpen bij het vinden van locaties waar een taalkundige zoekopdracht mogelijk per ongeluk is gebruikt in plaats van een ordinale zoekopdracht. Codepatronen zoals de volgende kunnen vatbaar zijn voor beveiligingsexplots.

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

Omdat de string.IndexOf(string) methode standaard gebruikmaakt van een taalkundige zoekopdracht, is het mogelijk dat een tekenreeks een letterlijke '<' waarde of '&' teken bevat en dat de string.IndexOf(string) routine moet worden geretourneerd -1, waarmee wordt aangegeven dat de zoeksubtekenreeks niet is gevonden. Codeanalyseregels CA1307 en CA1309 markeren dergelijke oproepsites en waarschuwen de ontwikkelaar dat er een potentieel probleem is.

Standaardzoek- en vergelijkingstypen

De volgende tabel bevat de standaardzoek- en vergelijkingstypen voor verschillende tekenreeks- en tekenreeksachtige API's. Als de aanroeper een expliciete CultureInfo parameter of StringComparison parameter opgeeft, wordt die parameter boven elke standaardwaarde uitgevoerd.

API Standaardgedrag Opmerkingen
string.Compare CurrentCulture
string.CompareTo CurrentCulture
string.Contains Rangtelwoord
string.EndsWith Rangtelwoord (wanneer de eerste parameter een char)
string.EndsWith CurrentCulture (wanneer de eerste parameter een string)
string.Equals Rangtelwoord
string.GetHashCode Rangtelwoord
string.IndexOf Rangtelwoord (wanneer de eerste parameter een char)
string.IndexOf CurrentCulture (wanneer de eerste parameter een string)
string.IndexOfAny Rangtelwoord
string.LastIndexOf Rangtelwoord (wanneer de eerste parameter een char)
string.LastIndexOf CurrentCulture (wanneer de eerste parameter een string)
string.LastIndexOfAny Rangtelwoord
string.Replace Rangtelwoord
string.Split Rangtelwoord
string.StartsWith Rangtelwoord (wanneer de eerste parameter een char)
string.StartsWith CurrentCulture (wanneer de eerste parameter een string)
string.ToLower CurrentCulture
string.ToLowerInvariant InvariantCulture
string.ToUpper CurrentCulture
string.ToUpperInvariant InvariantCulture
string.Trim Rangtelwoord
string.TrimEnd Rangtelwoord
string.TrimStart Rangtelwoord
string == string Rangtelwoord
string != string Rangtelwoord

In tegenstelling tot string API's voeren alle MemoryExtensions API's standaard ordinale zoekopdrachten en vergelijkingen uit, met de volgende uitzonderingen.

API Standaardgedrag Opmerkingen
MemoryExtensions.ToLower CurrentCulture (wanneer een null-argument CultureInfo is doorgegeven)
MemoryExtensions.ToLowerInvariant InvariantCulture
MemoryExtensions.ToUpper CurrentCulture (wanneer een null-argument CultureInfo is doorgegeven)
MemoryExtensions.ToUpperInvariant InvariantCulture

Een gevolg hiervan is dat bij het converteren van code van verbruik string naar verbruik ReadOnlySpan<char>, gedragswijzigingen per ongeluk kunnen worden geïntroduceerd. Hier volgt een voorbeeld van.

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

De aanbevolen manier om dit aan te pakken, is door een expliciete StringComparison parameter door te geven aan deze API's. De codeanalyseregels CA1307 en CA1309 kunnen hierbij helpen.

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

Zie ook