.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 dieNone
-Standardoption mitStringSort
identisch ausgeführt.StringSort
werden nicht alphanumerische Zeichen vor alphanumerischen Zeichen sortiert (z. B. „bill‘s“ vor „bills“). Um die vorherigeNone
-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
und3
(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.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'));
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 Werttrue
oder1
.
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
, wobeimyapp
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:
- CultureInfo.EnglishName
- CultureInfo.NativeName
- DateTimeFormatInfo.NativeCalendarName
- RegionInfo.NativeName
Die folgenden APIs werden mit Einschränkungen unterstützt:
- String.Normalize(NormalizationForm) und String.IsNormalized(NormalizationForm) unterstützen die selten verwendeten FormKC- und FormKD-Formulare nicht.
- RegionInfo.CurrencyNativeName gibt denselben Wert zurück wie RegionInfo.CurrencyEnglishName.
Darüber hinaus werden weniger Gebietsschemata unterstützt. Die unterstützte Liste finden Sie im dotnet/icu-Repository.