Teilen über


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

Hinweis

Die Kulturdaten, die das Verhalten der ICU-Bibliothek fördern, werden in der Regel vom Common Locale Data Repository (CLDR) verwaltet, nicht von der Laufzeit.

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 verwenden. 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. Im folgenden Abschnitt werden einige Verhaltensänderungen aufgeführt, die möglicherweise auftreten.

Zeichenfolgensortierung und System.Globalization.CompareOptions

CompareOptions ist die Optionsaufzählung, die an String.Compare übergeben werden kann, um zu beeinflussen, wie zwei Zeichenfolgen verglichen werden.

Der Vergleich von Zeichenfolgen für Gleichheit und das Bestimmen der Sortierreihenfolge unterscheidet sich zwischen NLS und ICU. Dies gilt insbesondere für:

  • Die Standardsortierreihenfolge für Zeichenfolgen unterscheidet sich, sodass dies auch dann sichtbar ist, wenn Sie CompareOptions nicht direkt verwenden. Bei Verwendung der ICU wird die None-Standardoption mit StringSortidentisch ausgeführt. StringSort werden nicht alphanumerische Zeichen vor alphanumerischen Zeichen sortiert (z. B. „bill‘s“ vor „bills“). Um die vorherige None-Funktionalität wiederherzustellen, müssen Sie die NLS-basierte Implementierung verwenden.
  • Die Standardbehandlung von Ligaturzeichen unterscheidet sich. Unter NLS gelten Ligaturen und ihre Nicht-Ligaturen-Entsprechungen (z. B. „oeuf“ und „œuf“) als gleich, dies ist jedoch nicht bei ICU in .NET der Fall. Dies liegt an einer anderen Sortierstärke zwischen den beiden Implementierungen. Um das NLS-Verhalten bei Verwendung von ICU wiederherzustellen, verwenden Sie den CompareOptions.IgnoreNonSpace-Wert.

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 Tabelle im Abschnitt ICU unter Windows aufgeführt sind, werden die Codeschnipsel 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.Ordinal.

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.

Wochentag, Abkürzungen

Die DateTimeFormatInfo.GetShortestDayName(DayOfWeek) Methode ruft den kürzesten abgekürzten Tagnamen für einen angegebenen Wochentag ab.

  • In .NET Core 3.1 und früheren Versionen unter Windows bestand diese Abkürzung für einen Wochentag aus zwei Zeichen, z. B. "So".
  • In .NET 5 und höheren Versionen bestehen diese Abkürzung für einen Wochentag aus nur einem Zeichen, z. B. "S".

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 erfolgreich. In älteren Versionen von Windows schlagen diese APIs jedoch 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 könnte zu Verhaltensunterschieden bei einigen globalisierungsbezogenen Vorgängen führen. Um wieder NLS zu verwenden, können Sie 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 könnte 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.Runtime PackageReference 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 nicht 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.