Share 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 det här avsnittet visas en av de beteendeändringar som du kan se, men det finns även andra.

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

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

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.

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 anges i ICU i Windows-avsnittstabellen lyckas de nämnda API:erna konsekvent. I äldre versioner av Windows misslyckas dock dessa API:er konsekvent. 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 en utvecklare välja bort 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.

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.