.NET-Globalisierung und ICU

Vor .NET 5 verwendeten die .NET-Globalisierungs-APIs verschiedene zugrunde liegende Bibliotheken auf verschiedenen Plattformen. Unter UNIX haben die APIs International Components for Unicode (ICU) und unter Windows National Language Support (NLS) verwendet. Dies führte zu einigen Verhaltensunterschieden in einigen Globalisierungs-APIs, wenn Anwendungen auf verschiedenen Plattformen ausgeführt wurden. Verhaltensunterschiede waren in den folgenden Bereichen ersichtlich:

  • Kulturen und Kulturdaten
  • Groß-/Kleinschreibung von Zeichenfolgen
  • Sortieren und Suchen von Zeichenfolgen
  • Sortieren von Schlüsseln
  • Zeichenfolgennormalisierung
  • Unterstützung für internationalisierte Domänennamen (IDN)
  • Anzeigename der Zeitzone unter Linux

Ab .NET 5 haben Entwickler mehr Kontrolle darüber, welche zugrunde liegende Bibliothek verwendet wird. So können in Anwendungen plattformübergreifende Unterschiede vermieden werden.

ICU unter Windows

Windows enthält jetzt eine vorinstallierte icu.dll-Version als Teil seiner Features, die automatisch für Globalisierungsaufgaben verwendet werden. Mit dieser Änderung kann .NET diese ICU-Bibliothek für die Globalisierungsunterstützung nutzen. In Fällen, in denen die ICU-Bibliothek nicht verfügbar ist oder nicht geladen werden kann, wie z. B bei älteren Windows-Versionen, greifen .NET 5 und nachfolgende Versionen auf die NLS-basierte Implementierung zurück.

Die folgende Tabelle zeigt, welche Versionen von .NET die ICU-Bibliothek in verschiedenen Windows-Client- und Server-Versionen laden können:

.NET-Version Windows-Version
.NET 5 oder .NET 6 Windows-Client 10 (Version 1903) oder höher
.NET 5 oder .NET 6 Windows Server 2022 oder höher
.NET 7 oder höher Windows-Client 10 (Version 1703) oder höher
.NET 7 oder höher Windows Server 2019 oder höher

Hinweis

.NET 7 und höhere Versionen haben die Möglichkeit, ICU unter älteren Windows-Versionen zu laden, im Gegensatz zu .NET 6 und .NET 5.

Hinweis

Selbst bei Verwendung von ICU verwenden die CurrentCulture-, CurrentUICulture- und CurrentRegion-Member weiterhin Windows-Betriebssystem-APIs, um Benutzereinstellungen zu berücksichtigen.

Verhaltensunterschiede

Wenn Sie für Ihre Anwendung ein Upgrade auf .NET 5 oder höher durchführen, bemerken Sie möglicherweise Änderungen in Ihrer Anwendung, auch wenn Sie sich nicht bewusst sind, dass Sie Globalisierungsfunktionen nutzen. In diesem Abschnitt finden Sie eine der Behavior Changes, die Sie möglicherweise beobachten, aber es gibt noch weitere.

String.IndexOf

Betrachten Sie den folgenden Code, der String.IndexOf(String) aufruft, um den Index des NULL-Zeichens \0 in einer Zeichenfolge zu finden.

const string greeting = "Hel\0lo";
Console.WriteLine($"{greeting.IndexOf("\0")}");
Console.WriteLine($"{greeting.IndexOf("\0", StringComparison.CurrentCulture)}");
Console.WriteLine($"{greeting.IndexOf("\0", StringComparison.Ordinal)}");
  • In .NET Core 3.1 und früheren Versionen unter Windows gibt der Codeschnipsel 3 in jeder der drei Zeilen aus.
  • Für .NET 5 und nachfolgende Versionen, die unter den Windows-Versionen ausgeführt werden, die in der Abschnittstabelle ICU unter Windows aufgeführt sind, werden die Codeausschnitte 0, 0 und 3 (für die Ordinalsuche) ausgegeben.

Standardmäßig führt String.IndexOf(String) eine kulturbewusste linguistische Suche aus. ICU betrachtet das NULL-Zeichen \0 als nullgewichtiges Zeichen, und daher wird das Zeichen bei einer linguistischen Suche in .NET 5 und höher nicht in der Zeichenfolge gefunden. NLS betrachtet das NULL-Zeichen \0 jedoch nicht als nullgewichtiges Zeichen, und eine linguistische Suche in .NET Core 3.1 und früher findet das Zeichen an Position 3. Eine Ordinalsuche findet das Zeichen in allen .NET-Versionen an Position 3.

Sie können Codeanalyseregeln entsprechend CA1307: Angeben von StringComparison für mehr Klarheit und CA1309: Verwenden eines ordinalen StringComparison ausführen, um Aufrufstellen in Ihrem Code zu finden, wo der Zeichenfolgenvergleich nicht angegeben oder nicht ordinal ist.

Weitere Informationen finden Sie unter Verhaltensänderungen beim Vergleichen von Zeichenfolgen ab .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'));

Wichtig

In .NET 5+ unter Windows-Versionen, die in der ICU unter Windows-Tabelle aufgeführt sind, gibt der vorherige Codeausschnitt aus:

True
True
True
False
False

Um dieses Verhalten zu vermeiden, verwenden Sie die char-Parameterüberladung oder 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'));

Wichtig

In .NET 5+ unter Windows-Versionen, die in der ICU unter Windows-Tabelle aufgeführt sind, gibt der vorherige Codeausschnitt aus:

True
True
True
False
False

Um dieses Verhalten zu vermeiden, verwenden Sie die char-Parameterüberladung oder StringComparison.Oridinal.

TimeZoneInfo.FindSystemTimeZoneById

ICU bietet die Flexibilität, TimeZoneInfo Instanzen mit IANA-Zeitzonen-IDs zu erstellen, auch wenn die Anwendung unter Windows ausgeführt wird. Ebenso können Sie TimeZoneInfo-Instanzen mit Windows-Zeitzonen-IDs erstellen, auch wenn sie auf Nicht-Windows-Plattformen ausgeführt werden. Beachten Sie jedoch, dass diese Funktionalität nicht verfügbar ist, wenn Sie den NLS-Modus oder den invarianten Globalisierungsmodus verwenden.

ICU-abhängige APIs

.NET hat APIs eingeführt, die von ICU abhängig sind. Diese APIs können nur erfolgreich sein, wenn ICU verwendet wird. Im Folgenden finden Sie einige Beispiele:

In den Windows-Versionen, die in der Abschnittstabelle ICU unter Windows aufgeführt sind, sind die genannten APIs konsistent erfolgreich. In älteren Versionen von Windows schlagen diese APIs jedoch konsistent fehl. In solchen Fällen können Sie das App-lokale ICU-Feature aktivieren, um den Erfolg dieser APIs sicherzustellen. Auf Nicht-Windows-Plattformen sind diese APIs unabhängig von der Version immer erfolgreich.

Darüber hinaus ist es für Apps von entscheidender Bedeutung, sicherzustellen, dass sie nicht im invarianten Globalisierungsmodus oder NLS-Modus ausgeführt werden, um den Erfolg dieser APIs zu garantieren.

Verwenden von NLS anstelle von ICU

Die Verwendung von ICU anstelle von NLS kann zu Verhaltensunterschieden bei einigen globalisierungsbezogenen Vorgängen führen. Um wieder NLS zu verwenden, kann ein Entwickler die ICU-Implementierung deaktivieren. Anwendungen können den NLS-Modus auf eine der folgenden Arten aktivieren:

  • In der Projektdatei:

    <ItemGroup>
      <RuntimeHostConfigurationOption Include="System.Globalization.UseNls" Value="true" />
    </ItemGroup>
    
  • In der Datei runtimeconfig.json:

    {
      "runtimeOptions": {
         "configProperties": {
           "System.Globalization.UseNls": true
          }
      }
    }
    
  • Durch Festlegen der Umgebungsvariablen DOTNET_SYSTEM_GLOBALIZATION_USENLS auf den Wert true oder 1.

Hinweis

Ein im Projekt oder in der Datei runtimeconfig.json festgelegter Wert hat Vorrang vor der Umgebungsvariablen.

Weitere Informationen finden Sie unter Laufzeitkonfigurationseinstellungen.

Ermitteln, ob Ihre App ICU verwendet

Mit dem folgenden Codeschnipsel können Sie ermitteln, ob Ihre App mit ICU-Bibliotheken (und nicht mit NLS) ausgeführt wird.

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

Um die Version von .NET zu ermitteln, verwenden Sie RuntimeInformation.FrameworkDescription.

App-lokale ICU

Jedes Release von ICU kann Fehlerbehebungen sowie aktualisierte CLDR-Daten (Common Locale Data Repository) enthalten, die die Sprachen der Welt beschreiben. Der Wechsel zwischen ICU-Versionen kann das Verhalten von Apps subtil beeinflussen, wenn es um globalisierungsbezogene Vorgänge geht. Damit Anwendungsentwickler Konsistenz über alle Bereitstellungen hinweg sicherstellen können, ermöglichen .NET 5 und höhere Versionen Apps unter Windows und UNIX das Einbinden und Verwenden ihrer eigenen Kopie von ICU.

Anwendungen können einen App-lokalen ICU-Implementierungsmodus auf eine der folgenden Arten aktivieren:

  • Legen Sie in der Projektdatei den entsprechenden RuntimeHostConfigurationOption-Wert fest:

    <ItemGroup>
      <RuntimeHostConfigurationOption Include="System.Globalization.AppLocalIcu" Value="<suffix>:<version> or <version>" />
    </ItemGroup>
    
  • Legen Sie alternativ in der runtimeconfig.json-Datei den entsprechenden runtimeOptions.configProperties-Wert fest:

    {
      "runtimeOptions": {
         "configProperties": {
           "System.Globalization.AppLocalIcu": "<suffix>:<version> or <version>"
         }
      }
    }
    
  • Eine weitere Alternative ist das Festlegen der Umgebungsvariablen DOTNET_SYSTEM_GLOBALIZATION_APPLOCALICU auf den Wert <suffix>:<version> oder <version>.

    <suffix>: Optionales Suffix mit weniger als 36 Zeichen gemäß den öffentlichen ICU-Paketkonventionen. Wenn Sie eine benutzerdefinierte ICU-Bibliothek erstellt haben, können Sie diese so anpassen, dass die Bibliotheksnamen und exportierten Symbolnamen mit einem Suffix erstellt werden, z. B. libicuucmyapp, wobei myapp das Suffix ist.

    <version>: Eine gültige ICU-Version, z. B. 67.1. Diese Version wird zum Laden der Binärdateien und zum Abrufen der exportierten Symbole verwendet.

Wenn eine dieser Optionen festgelegt ist, können Sie Ihrem Projekt eine Microsoft.ICU.ICU4C.RuntimePackageReference hinzufügen, die der konfigurierten version entspricht, und das ist alles.

Alternativ verwendet .NET die NativeLibrary.TryLoad-Methode, die mehrere Pfade überprüft, um ICU zu laden, wenn der App-lokale Switch festgelegt ist. Die Methode versucht zuerst, die Bibliothek in der NATIVE_DLL_SEARCH_DIRECTORIES-Eigenschaft zu finden, die vom dotnet-Host basierend auf der Datei deps.json für die App erstellt wird. Weitere Informationen finden Sie unter Standardüberprüfung.

Für eigenständige Apps sind keine besonderen Aktionen durch den Benutzer erforderlich. Es muss nur sichergestellt werden, dass sich die ICU-Bibliothek im App-Verzeichnis befindet (für eigenständige Apps ist das Arbeitsverzeichnis standardmäßig NATIVE_DLL_SEARCH_DIRECTORIES).

Wenn Sie ICU mithilfe eines NuGet-Pakets nutzen, funktioniert dies in frameworkabhängigen Anwendungen. NuGet löst die nativen Ressourcen auf und bindet sie in die Datei deps.json sowie in das Ausgabeverzeichnis für die Anwendung im Verzeichnis runtimes. .NET lädt Sie von dort.

Für frameworkabhängige Apps (nicht eigenständig), in denen ICU aus einem lokalen Build genutzt wird, müssen Sie zusätzliche Schritte ausführen. Das .NET SDK verfügt noch nicht über eine Funktion, die „lose“ native Binärdateien in deps.json integriert (weitere Informationen dazu finden Sie in diesem SDK-Problem). Stattdessen können Sie dies aktivieren, indem Sie der Projektdatei der Anwendung zusätzliche Informationen hinzufügen. Zum Beispiel:

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

Dies muss für alle ICU-Binärdateien für die unterstützten Laufzeiten durchgeführt werden. Außerdem müssen die NuGetPackageId-Metadaten in der RuntimeTargetsCopyLocalItems-Elementgruppe mit einem NuGet-Paket übereinstimmen, auf das das Projekt tatsächlich verweist.

macOS-Verhalten

macOS weist ein anderes Verhalten zum Auflösen abhängiger dynamischer Bibliotheken aus den in der Mach-O-Datei angegebenen Ladebefehlen als das Linux-Lademodul auf. Im Linux-Lademodul kann .NET versuchen, libicudata, libicuuc und libicui18n (in dieser Reihenfolge) zu verwenden, um das ICU-Abhängigkeitsdiagramm zu erfüllen. Unter macOS funktioniert dies jedoch nicht. Wenn Sie ICU unter macOS erstellen, erhalten Sie standardmäßig eine dynamische Bibliothek mit diesen Ladebefehlen in libicuuc. Der folgende Ausschnitt zeigt ein Beispiel.

~/ % 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)

Diese Befehle verweisen nur auf den Namen der abhängigen Bibliotheken für die anderen Komponenten von ICU. Das Lademodul führt die Suche nach den dlopen-Konventionen durch, wobei diese Bibliotheken in den Systemverzeichnissen enthalten sind oder die LD_LIBRARY_PATH-Umgebungsvariablen festgelegt werden oder sich ICU im Verzeichnis auf App-Ebene befindet. Wenn Sie LD_LIBRARY_PATH nicht festlegen oder sicherstellen können, dass sich die ICU-Binärdateien im Verzeichnis auf App-Ebene befinden, müssen Sie zusätzliche Schritte ausführen.

Es gibt einige Direktiven für das Ladeprogramm (z. B. @loader_path), die das Ladeprogramm anweisen, diese Abhängigkeit im gleichen Verzeichnis wie die Binärdatei mit diesem Ladebefehl zu suchen. Es gibt zwei Möglichkeiten, dies zu erreichen:

  • install_name_tool -change

    Führen Sie die folgenden Befehle aus:

    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
    
  • Patchen von ICU zum Generieren der Installationsnamen mit @loader_path

    Ändern Sie vor dem Ausführen der automatischen Konfiguration (./runConfigureICU) diese Zeilen wie folgt:

    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 für WebAssembly

Eine Version von ICU ist verfügbar, die speziell für WebAssembly-Workloads gilt. Diese Version bietet Globalisierungskompatibilität mit Desktopprofilen. Um die ICU-Datendateigröße von 24 MB auf 1,4 MB zu reduzieren (oder etwa 0,3 MB bei Komprimierung mit Brotli), weist diese Workload einige Einschränkungen auf.

Folgende APIs werden nicht unterstützt:

Die folgenden APIs werden mit Einschränkungen unterstützt:

Darüber hinaus werden weniger Gebietsschemata unterstützt. Die unterstützte Liste finden Sie im dotnet/icu-Repository.