Dela via


.NET-globalisering och ICU

Före .NET 5 använde .NET-globaliserings-API:erna olika underliggande bibliotek på olika plattformar. På Unix använde API:erna internationella komponenter för Unicode (ICU) och i Windows använde de National Language Support (NLS). Detta resulterade i vissa beteendeskillnader i en handfull globaliserings-API:er när program kördes på olika plattformar. Beteendeskillnader var tydliga inom följande områden:

  • Kulturer och kulturdata
  • Stränghölje
  • Sortering och sökning av strängar
  • Sortera nycklar
  • Strängnormalisering
  • Stöd för internationaliserade domännamn (IDN)
  • Visningsnamn för tidszon i Linux

Från och med .NET 5 har utvecklare mer kontroll över vilket underliggande bibliotek som används, vilket gör det möjligt för program att undvika skillnader mellan plattformar.

Kommentar

Kulturdata som styr beteendet för ICU-biblioteket underhålls vanligtvis av CLDR (Common Locale Data Repository), inte körningen.

ICU på Windows

Windows innehåller nu en förinstallerad icu.dll version som en del av dess funktioner som används automatiskt för globaliseringsuppgifter. Med den här ändringen kan .NET använda det här ICU-biblioteket för dess globaliseringsstöd. I de fall där ICU-biblioteket inte är tillgängligt eller inte kan läsas in, vilket är fallet med äldre Windows-versioner, återgår .NET 5 och efterföljande versioner till att använda den NLS-baserade implementeringen.

I följande tabell visas vilka versioner av .NET som kan läsa in ICU-biblioteket i olika Windows-klient- och serverversioner:

.NET-version Windows-version
.NET 5 eller .NET 6 Windows-klient 10 version 1903 eller senare
.NET 5 eller .NET 6 Windows Server 2022 eller senare
.NET 7 eller senare Windows-klient 10 version 1703 eller senare
.NET 7 eller senare Windows Server 2019 eller senare

Kommentar

.NET 7- och senare versioner har möjlighet att läsa in ICU på äldre Windows-versioner, till skillnad från .NET 6 och .NET 5.

Kommentar

Även när du använder ICU använder CurrentCulturemedlemmarna , CurrentUICultureoch CurrentRegion fortfarande Windows-operativsystem-API:er för att följa användarinställningarna.

Beteendeskillnader

Om du uppgraderar appen till .NET 5 eller senare kan du se ändringar i din app även om du inte inser att du använder globaliseringsanläggningar. I följande avsnitt visas några beteendeändringar som du kan uppleva.

Strängsortering och System.Globalization.CompareOptions

CompareOptions är den alternativuppräkning som kan skickas till för att String.Compare påverka hur två strängar jämförs.

Att jämföra strängar för likhet och bestämma deras sorteringsordning skiljer sig mellan NLS och ICU. Framför allt:

  • Standardordningen för strängsortering är annorlunda, så detta visas även om du inte använder CompareOptions direkt. När du använder ICU None fungerar standardalternativet på samma sätt som StringSort. StringSort sorterar icke-alfanumeriska tecken före alfanumeriska tecken (så "fakturans" sorterar före "fakturor", till exempel). Om du vill återställa den tidigare None funktionen måste du använda den NLS-baserade implementeringen.
  • Standardhanteringen av ligaturtecken skiljer sig åt. Under NLS anses ligaturer och deras icke-ligaturmotsvarigheter (till exempel "oeuf" och "œuf") vara lika, men så är inte fallet med ICU i .NET. Detta beror på en annan sorteringsstyrka mellan de två implementeringarna. Använd värdet för att återställa NLS-beteendet när du använder ICU CompareOptions.IgnoreNonSpace .

String.IndexOf

Tänk på följande kod som anropar String.IndexOf(String) för att hitta indexet för null-tecknet \0 i en sträng.

const string greeting = "Hel\0lo";
Console.WriteLine($"{greeting.IndexOf("\0")}");
Console.WriteLine($"{greeting.IndexOf("\0", StringComparison.CurrentCulture)}");
Console.WriteLine($"{greeting.IndexOf("\0", StringComparison.Ordinal)}");
  • I .NET Core 3.1 och tidigare versioner i Windows skrivs 3 kodfragmentet ut på var och en av de tre raderna.
  • För .NET 5- och senare versioner som körs i Windows-versionerna som anges i ICU i Windows-avsnittstabellen0skriver kodfragmentet ut , 0och 3 (för ordningssökningen).

Utför som standard String.IndexOf(String) en kulturmedveten språksökning. ICU anser att null-tecknet \0 är ett nollviktstecken och därför hittas inte tecknet i strängen när du använder en språklig sökning på .NET 5 och senare. NLS anser dock inte att null-tecknet \0 är ett nollviktstecken, och en språklig sökning på .NET Core 3.1 och tidigare letar upp tecknet på position 3. En ordningssökning hittar tecknet vid position 3 på alla .NET-versioner.

Du kan köra kodanalysregler CA1307: Ange StringComparison för tydlighetens skull och CA1309: Använd ordningstalssträngKomparison för att hitta anropsplatser i koden där strängjämförelsen inte har angetts eller inte är ordningstal.

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

String.EndsWith

const string foo = "abc";

Console.WriteLine(foo.EndsWith("\0"));
Console.WriteLine(foo.EndsWith("c"));
Console.WriteLine(foo.EndsWith("\0", StringComparison.CurrentCulture));
Console.WriteLine(foo.EndsWith("\0", StringComparison.Ordinal));
Console.WriteLine(foo.EndsWith('\0'));

Viktigt!

I .NET 5+ som körs på Windows-versioner som anges i tabellen ICU i Windows skrivs föregående kodfragment ut:

True
True
True
False
False

Undvik det här beteendet genom att använda parameteröverlagringen char eller StringComparison.Ordinal.

String.StartsWith

const string foo = "abc";

Console.WriteLine(foo.StartsWith("\0"));
Console.WriteLine(foo.StartsWith("a"));
Console.WriteLine(foo.StartsWith("\0", StringComparison.CurrentCulture));
Console.WriteLine(foo.StartsWith("\0", StringComparison.Ordinal));
Console.WriteLine(foo.StartsWith('\0'));

Viktigt!

I .NET 5+ som körs på Windows-versioner som anges i tabellen ICU i Windows skrivs föregående kodfragment ut:

True
True
True
False
False

Undvik det här beteendet genom att använda parameteröverlagringen char eller StringComparison.Ordinal.

TimeZoneInfo.FindSystemTimeZoneById

ICU ger flexibiliteten att skapa TimeZoneInfo instanser med IANA-tidszons-ID,även när programmet körs i Windows. På samma sätt kan du skapa TimeZoneInfo instanser med Tidszons-ID:t för Windows, även när de körs på plattformar som inte är Windows. Det är dock viktigt att observera att den här funktionen inte är tillgänglig när du använder NLS-läge eller globalisering i variant läge.

Förkortningar för veckodag

Metoden DateTimeFormatInfo.GetShortestDayName(DayOfWeek) hämtar det kortaste förkortade dagnamnet för en angiven veckodag.

  • I .NET Core 3.1 och tidigare versioner i Windows bestod dessa veckodagars förkortningar av två tecken, till exempel "Su".
  • I .NET 5 och senare versioner består dessa veckodagars förkortningar av endast ett tecken, till exempel "S".

ICU-beroende API:er

.NET introducerade API:er som är beroende av ICU. Dessa API:er kan bara lyckas när du använder ICU. Nedan följer några exempel:

I windowsversionerna som visas i avsnittstabellen ICU i Windows lyckas de nämnda API:erna. I äldre versioner av Windows misslyckas dock dessa API:er. I sådana fall kan du aktivera funktionen applokal ICU för att säkerställa att dessa API:er lyckas. På icke-Windows-plattformar lyckas dessa API:er alltid oavsett version.

Dessutom är det viktigt att appar ser till att de inte körs i globaliseringsläge i variant läge eller NLS-läge för att garantera att dessa API:er lyckas.

Använda NLS i stället för ICU

Användning av ICU i stället för NLS kan leda till beteendeskillnader med vissa globaliseringsrelaterade åtgärder. Om du vill återgå till att använda NLS kan du avregistrera dig från ICU-implementeringen. Program kan aktivera NLS-läge på något av följande sätt:

  • I projektfilen:

    <ItemGroup>
      <RuntimeHostConfigurationOption Include="System.Globalization.UseNls" Value="true" />
    </ItemGroup>
    
  • I filen runtimeconfig.json:

    {
      "runtimeOptions": {
         "configProperties": {
           "System.Globalization.UseNls": true
          }
      }
    }
    
  • Genom att ange miljövariabeln DOTNET_SYSTEM_GLOBALIZATION_USENLS till värdet true eller 1.

Kommentar

Ett värde som anges i projektet eller i runtimeconfig.json filen har företräde framför miljövariabeln.

Mer information finns i Konfigurationsinställningar för Körning.

Ta reda på om din app använder ICU

Följande kodfragment kan hjälpa dig att avgöra om appen körs med ICU-bibliotek (och inte NLS).

public static bool ICUMode()
{
    SortVersion sortVersion = CultureInfo.InvariantCulture.CompareInfo.Version;
    byte[] bytes = sortVersion.SortId.ToByteArray();
    int version = bytes[3] << 24 | bytes[2] << 16 | bytes[1] << 8 | bytes[0];
    return version != 0 && version == sortVersion.FullVersion;
}

Om du vill fastställa versionen av .NET använder du RuntimeInformation.FrameworkDescription.

Applokal ICU

Varje version av ICU kan medföra felkorrigeringar och uppdaterade CLDR-data (Common Locale Data Repository) som beskriver världens språk. Om du flyttar mellan versioner av ICU kan det påverka appens beteende när det gäller globaliseringsrelaterade åtgärder. För att hjälpa programutvecklare att säkerställa konsekvens i alla distributioner gör .NET 5 och senare versioner det möjligt för appar i både Windows och Unix att bära och använda sin egen kopia av ICU.

Program kan välja ett applokalt ICU-implementeringsläge på något av följande sätt:

  • I projektfilen anger du lämpligt RuntimeHostConfigurationOption värde:

    <ItemGroup>
      <RuntimeHostConfigurationOption Include="System.Globalization.AppLocalIcu" Value="<suffix>:<version> or <version>" />
    </ItemGroup>
    
  • Eller i filen runtimeconfig.json anger du lämpligt runtimeOptions.configProperties värde:

    {
      "runtimeOptions": {
         "configProperties": {
           "System.Globalization.AppLocalIcu": "<suffix>:<version> or <version>"
         }
      }
    }
    
  • Eller genom att ange miljövariabeln DOTNET_SYSTEM_GLOBALIZATION_APPLOCALICU till värdet <suffix>:<version> eller <version>.

    <suffix>: Valfritt suffix med färre än 36 tecken, enligt de offentliga ICU-paketeringskonventionerna. När du skapar en anpassad ICU kan du anpassa den för att skapa lib-namnen och exporterade symbolnamn så att de innehåller ett suffix, libicuucmyapptill exempel , där myapp är suffixet.

    <version>: En giltig ICU-version, till exempel 67.1. Den här versionen används för att läsa in binärfilerna och för att hämta de exporterade symbolerna.

När något av dessa alternativ har angetts kan du lägga till en Microsoft.ICU.ICU4C.RuntimePackageReference i projektet som motsvarar den konfigurerade version och det är allt som behövs.

Om du vill läsa in ICU när den applokala växeln har angetts använder NativeLibrary.TryLoad .NET metoden som avsöker flera sökvägar. Metoden försöker först hitta biblioteket i NATIVE_DLL_SEARCH_DIRECTORIES egenskapen, som skapas av dotnet-värden baserat på deps.json filen för appen. Mer information finns i Standardsökning.

För fristående appar krävs ingen särskild åtgärd av användaren, förutom att se till att ICU finns i appkatalogen (för fristående appar är arbetskatalogen standardvärdet NATIVE_DLL_SEARCH_DIRECTORIES).

Om du använder ICU via ett NuGet-paket fungerar detta i ramverksberoende program. NuGet löser de inbyggda tillgångarna och innehåller dem i deps.json filen och i utdatakatalogen för programmet under runtimes katalogen. .NET läser in det därifrån.

För ramverksberoende appar (inte fristående) där ICU används från en lokal version måste du vidta ytterligare åtgärder. .NET SDK har ännu ingen funktion för "lösa" interna binärfiler som ska införlivas i deps.json (se det här SDK-problemet). I stället kan du aktivera detta genom att lägga till ytterligare information i programmets projektfil. Till exempel:

<ItemGroup>
  <IcuAssemblies Include="icu\*.so*" />
  <RuntimeTargetsCopyLocalItems Include="@(IcuAssemblies)" AssetType="native" CopyLocal="true"
    DestinationSubDirectory="runtimes/linux-x64/native/" DestinationSubPath="%(FileName)%(Extension)"
    RuntimeIdentifier="linux-x64" NuGetPackageId="System.Private.Runtime.UnicodeData" />
</ItemGroup>

Detta måste göras för alla ICU-binärfiler för de körningar som stöds. Metadata i NuGetPackageIdRuntimeTargetsCopyLocalItems objektgruppen måste också matcha ett NuGet-paket som projektet faktiskt refererar till.

Läs in specifik ICU-version på Linux

När du använder ICU i Linux försöker .NET som standard läsa in den senaste installerade versionen av ICU från systemet. Du kan dock ange en specifik version av ICU som ska läsas in genom att ange DOTNET_ICU_VERSION_OVERRIDE miljövariabeln.

Om miljövariabeln till exempel är inställd på ett visst versionsnummer, till exempel 67.1, försöker .NET läsa in den versionen av ICU. .NET söker till exempel efter biblioteken libicuuc.so.67.1 och libicui18n.so.67.1.

Kommentar

Den här miljövariabeln stöds endast i .NET-versioner som tillhandahålls av Microsoft och stöds inte på versioner som tillhandahålls av Linux-distributioner. För .NET-versioner som är tidigare än .NET 10 kallas miljövariabeln CLR_ICU_VERSION_OVERRIDE.

Om den angivna versionen inte hittas återgår .NET till att läsa in den högsta installerade ICU-versionen från systemet.

Den här konfigurationen ger flexibilitet när det gäller att kontrollera användning av ICU-versioner, vilket säkerställer kompatibilitet med programspecifika eller systemspecifika ICU-versioner.

macOS-beteende

macOS har ett annat beteende för att matcha beroende dynamiska bibliotek från de inläsningskommandon som anges i Mach-O filen än Linux-inläsaren. I Linux-inläsaren kan .NET prova libicudata, libicuucoch libicui18n (i den ordningen) för att uppfylla ICU-beroendediagram. Men på macOS fungerar detta inte. När du skapar ICU på macOS får du som standard ett dynamiskt bibliotek med dessa inläsningskommandon i libicuuc. Följande kodfragment visar ett exempel.

~/ % otool -L /Users/santifdezm/repos/icu-build/icu/install/lib/libicuuc.67.1.dylib
/Users/santifdezm/repos/icu-build/icu/install/lib/libicuuc.67.1.dylib:
 libicuuc.67.dylib (compatibility version 67.0.0, current version 67.1.0)
 libicudata.67.dylib (compatibility version 67.0.0, current version 67.1.0)
 /usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1281.100.1)
 /usr/lib/libc++.1.dylib (compatibility version 1.0.0, current version 902.1.0)

Dessa kommandon refererar bara till namnet på de beroende biblioteken för de andra komponenterna i ICU. Inläsaren utför sökningen enligt konventionerna dlopen , vilket innebär att du har dessa bibliotek i systemkatalogerna eller anger LD_LIBRARY_PATH env-vars eller har ICU på appnivåkatalogen. Om du inte kan ange LD_LIBRARY_PATH eller se till att ICU-binärfiler finns i katalogen på appnivå måste du utföra lite extra arbete.

Det finns vissa direktiv för inläsaren, till exempel @loader_path, som talar om för inläsaren att söka efter det beroendet i samma katalog som binärfilen med det inläsningskommandot. Det finns två sätt att uppnå detta:

  • install_name_tool -change

    Kör följande kommandon:

    install_name_tool -change "libicudata.67.dylib" "@loader_path/libicudata.67.dylib" /path/to/libicuuc.67.1.dylib
    install_name_tool -change "libicudata.67.dylib" "@loader_path/libicudata.67.dylib" /path/to/libicui18n.67.1.dylib
    install_name_tool -change "libicuuc.67.dylib" "@loader_path/libicuuc.67.dylib" /path/to/libicui18n.67.1.dylib
    
  • Korrigera ICU för att skapa installationsnamnen med @loader_path

    Innan du kör autokonfiguration (./runConfigureICU) ändrar du följande rader till:

    LD_SONAME = -Wl,-compatibility_version -Wl,$(SO_TARGET_VERSION_MAJOR) -Wl,-current_version -Wl,$(SO_TARGET_VERSION) -install_name @loader_path/$(notdir $(MIDDLE_SO_TARGET))
    

ICU på WebAssembly

En version av ICU är tillgänglig som är specifikt för WebAssembly-arbetsbelastningar. Den här versionen ger globaliseringskompatibilitet med skrivbordsprofiler. För att minska filstorleken för ICU-data från 24 MB till 1,4 MB (eller ~0,3 MB om den komprimeras med Brotli) har den här arbetsbelastningen en handfull begränsningar.

Följande API:er stöds inte:

Följande API:er stöds med begränsningar:

Dessutom stöds färre nationella inställningar. Listan som stöds finns på lagringsplatsen dotnet/icu.

Globaliseringskonfiguration i .NET-appar

.NET-globaliseringsinitiering är en komplex process som innebär att läsa in lämpligt globaliseringsbibliotek, konfigurera kulturdata och konfigurera inställningarna för globalisering. I följande avsnitt beskrivs hur globaliseringsinitiering fungerar på olika plattformar.

Windows

I Windows följer .NET följande steg för att initiera globaliseringen:

  • Kontrollera om globalisering oberoende läge är aktiverat. När det här läget är aktivt kringgår .NET inläsningen av ICU-biblioteket och undviker att använda NLS-API:er. I stället förlitar den sig på inbyggda invarianta kulturdata, vilket säkerställer att beteendet förblir helt oberoende av operativsystemet och ICU-biblioteket.

  • Kontrollera om NLS-läge är aktiverat. Om det är aktiverat hoppar .NET över inläsningen av ICU-biblioteket och förlitar sig i stället på Windows NLS API:er för globaliseringsstöd.

  • Kontrollera om funktionen app-lokal ICU är aktiverad. I så fall försöker .NET läsa in ICU-biblioteket från programkatalogen genom att lägga till den angivna versionen i biblioteksnamnen. Om versionen till exempel är 72.1 försöker .NET först läsa in icuuc72.dll, icuin72.dlloch icudt72.dll. Om de här biblioteken inte kan läsas in kommer det sedan att försöka läsa in icuuc72.1.dll, icuin72.1.dlloch icudt72.1.dll. Om inget av biblioteken hittas avslutas processen med ett felmeddelande som: Failed to load app-local ICU: {library name}.

  • Om inget av ovanstående villkor uppfylls försöker .NET läsa in ICU-biblioteket från systemkatalogen. Den försöker först läsa in icu.dll. Om det här biblioteket inte är tillgängligt försöker det läsa in icuuc.dll och icuin.dll från systemkatalogen. Om något av dessa bibliotek inte hittas återgår körningen till att använda NLS-API:er för globaliseringsstöd.

Kommentar

NLS-API:er är alltid tillgängliga i alla Windows-versioner, så .NET kan alltid återgå till dem för globaliseringsstöd.

Linux

  • Kontrollera om globalisering oberoende läge är aktiverat. När det här läget är aktivt kringgår .NET inläsningen av ICU-biblioteket. I stället förlitar den sig på inbyggda invarianta kulturdata, vilket säkerställer att beteendet förblir helt oberoende av operativsystemet och ICU-biblioteket.
  • Kontrollera om funktionen app-lokal ICU är aktiverad. I så fall försöker .NET läsa in ICU-biblioteket från programkatalogen genom att lägga till den angivna versionen i biblioteksnamnen. Om versionen till exempel är 68.2.0.9 försöker .NET läsa in libicuuc.so.68.2.0.9 och libicui18n.so.68.2.0.9. Om inget av biblioteken hittas avslutas processen med ett felmeddelande som: Failed to load app-local ICU: {library name}.
  • Kontrollera om miljövariabeln DOTNET_ICU_VERSION_OVERRIDE har angetts. Om så är fallet försöker .NET läsa in den angivna versionen av ICU enligt beskrivningen i Läs in specifik ICU-version på Linux.
  • Om inget av ovanstående villkor är uppfyllt försöker .NET läsa in den högsta installerade versionen av ICU-biblioteket från systemet. Den försöker läsa in biblioteken libicuuc.so.[version] och libicui18n.so.[version], där [version] är den högsta installerade versionen av ICU i systemet. Om biblioteken inte hittas avslutas processen med ett felmeddelande som: Failed to load system ICU: {library name}.

macOS

  • Kontrollera om globalisering oberoende läge är aktiverat. När det här läget är aktivt kringgår .NET inläsningen av ICU-biblioteket. I stället förlitar den sig på inbyggda invarianta kulturdata, vilket säkerställer att beteendet förblir helt oberoende av operativsystemet och ICU-biblioteket.
  • Kontrollera om funktionen app-lokal ICU är aktiverad. I så fall försöker .NET läsa in ICU-biblioteket från programkatalogen genom att lägga till den angivna versionen i biblioteksnamnen. Om versionen till exempel är 68.2.0.9 försöker .NET läsa in libicuuc68.2.0.9.dylib och libicui18n68.2.0.9.dylib. Om inget av biblioteken hittas avslutas processen med ett felmeddelande som: Failed to load app-local ICU: {library name}.
  • Om inget av ovanstående villkor uppfylls försöker .NET läsa in den installerade versionen av ICU-biblioteket enligt beskrivningen i macOS-beteende.