Globalizacja platformy .NET i ICU
Przed platformą .NET 5 interfejsy API globalizacji platformy .NET używały różnych bibliotek bazowych na różnych platformach. W systemie Unix interfejsy API używały międzynarodowych składników dla standardu Unicode (ICU) i w systemie Windows używały obsługi języka narodowego (NLS). Spowodowało to pewne różnice behawioralne w kilku interfejsach API globalizacji podczas uruchamiania aplikacji na różnych platformach. Różnice zachowań były widoczne w następujących obszarach:
- Kultury i dane kulturowe
- Wielkość liter ciągów
- Sortowanie i wyszukiwanie ciągów
- Sortowanie kluczy
- Normalizacja ciągu
- Obsługa nazw domen międzynarodowych (IDN)
- Nazwa wyświetlana strefy czasowej w systemie Linux
Począwszy od platformy .NET 5, deweloperzy mają większą kontrolę nad tym, która podstawowa biblioteka jest używana, umożliwiając aplikacjom unikanie różnic między platformami.
Uwaga
Dane kulturowe, które napędzają zachowanie biblioteki ICU, są zwykle obsługiwane przez repozytorium Common Locale Data Repository (CLDR), a nie środowisko uruchomieniowe.
ICU w systemie Windows
System Windows zawiera teraz wstępnie zainstalowaną wersję icu.dll w ramach funkcji, które są automatycznie stosowane do zadań globalizacji. Ta modyfikacja umożliwia platformie .NET używanie tej biblioteki ICU do obsługi globalizacji. W przypadkach, gdy biblioteka ICU jest niedostępna lub nie można jej załadować, podobnie jak w przypadku starszych wersji systemu Windows, platformy .NET 5 i kolejnych wersji powrócić do korzystania z implementacji opartej na nlS.
W poniższej tabeli przedstawiono, które wersje platformy .NET mogą ładować bibliotekę ICU w różnych wersjach klienta i serwera systemu Windows:
Wersja platformy .NET | Wersja dla systemu Windows |
---|---|
.NET 5 lub .NET 6 | Klient systemu Windows 10 w wersji 1903 lub nowszej |
.NET 5 lub .NET 6 | Windows Server 2022 lub nowszy |
.NET 7 lub nowszy | Klient systemu Windows 10 w wersji 1703 lub nowszej |
.NET 7 lub nowszy | Windows Server 2019 lub nowszy |
Uwaga
Platforma .NET 7 i nowsze wersje mają możliwość ładowania ICU w starszych wersjach systemu Windows, w przeciwieństwie do platformy .NET 6 i .NET 5.
Uwaga
Nawet w przypadku korzystania z ICU, CurrentCulture
CurrentUICulture
, i CurrentRegion
członków nadal używają interfejsów API systemu operacyjnego Windows do przestrzegania ustawień użytkownika.
Różnice behawioralne
Jeśli uaktualnisz aplikację do platformy .NET 5 lub nowszej, możesz zobaczyć zmiany w aplikacji, nawet jeśli nie zdajesz sobie sprawy, że używasz obiektów globalizacji. W poniższej sekcji wymieniono niektóre zmiany behawioralne, które mogą wystąpić.
Sortowanie ciągów i System.Globalization.CompareOptions
CompareOptions
to wyliczenie opcji, które można przekazać, String.Compare
aby wpłynąć na sposób porównywania dwóch ciągów.
Porównywanie ciągów pod kątem równości i określanie ich kolejności sortowania różni się między NLS i ICU. W szczególności:
- Domyślna kolejność sortowania ciągów jest inna, więc będzie to widoczne, nawet jeśli nie używasz
CompareOptions
bezpośrednio. W przypadku korzystania z funkcji ICU domyślnaNone
opcja wykonuje to samo coStringSort
.StringSort
sortuje znaki inne niż alfanumeryczne przed alfanumeryczne (więc "bill's" sortuje przed "rachunkami", na przykład). Aby przywrócić poprzednieNone
funkcje, należy użyć implementacji opartej na nlS. - Domyślna obsługa znaków ligatury różni się. W obszarze NLS, ligatury i ich odpowiedniki nienależące do ligatury (na przykład "oeuf" i "śuf") są traktowane jako równe, ale nie dotyczy to ICU na platformie .NET. Wynika to z innej siły sortowania między dwiema implementacjami. Aby przywrócić zachowanie nlS podczas korzystania z ICU, użyj
CompareOptions.IgnoreNonSpace
wartości .
String.IndexOf
Rozważ następujący kod, który wywołuje String.IndexOf(String) metodę w celu znalezienia indeksu znaku \0
o wartości null w ciągu.
const string greeting = "Hel\0lo";
Console.WriteLine($"{greeting.IndexOf("\0")}");
Console.WriteLine($"{greeting.IndexOf("\0", StringComparison.CurrentCulture)}");
Console.WriteLine($"{greeting.IndexOf("\0", StringComparison.Ordinal)}");
- W programie .NET Core 3.1 i starszych wersjach systemu Windows fragment kodu jest drukowany w każdym z trzech wierszy
3
. - W przypadku platformy .NET 5 i nowszych wersji działających w wersjach systemu Windows wymienionych w tabeli sekcji ICU w systemie Windows fragment kodu drukuje
0
wartości ,0
i3
(dla wyszukiwania porządkowego).
Domyślnie String.IndexOf(String) wykonuje wyszukiwanie językowe z uwzględnieniem kultury. Funkcja ICU uważa, że znak \0
null jest znakiem zerowej wagi, a zatem znak nie znajduje się w ciągu podczas wyszukiwania językowego na platformie .NET 5 i nowszych. Jednak nlS nie uwzględnia znaku null jako znaku \0
zerowego i wyszukiwania językowego na platformie .NET Core 3.1 i wcześniejsze lokalizuje znak na pozycji 3. Wyszukiwanie porządkowe znajduje znak na pozycji 3 we wszystkich wersjach platformy .NET.
Reguły analizy kodu CA1307 można uruchomić: Określ wartość StringComparison dla jasności i CA1309: Użyj porządkowego ciąguComparison , aby znaleźć witryny wywołań w kodzie, w którym nie określono porównania ciągów lub nie jest porządkowa.
Aby uzyskać więcej informacji, zobacz Zmiany zachowania podczas porównywania ciągów na platformie .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'));
Ważne
W programie .NET 5+ uruchomionym w wersjach systemu Windows wymienionych w tabeli ICU w systemie Windows powyższe wydruki fragmentu kodu:
True
True
True
False
False
Aby uniknąć tego zachowania, użyj przeciążenia parametru char
lub 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'));
Ważne
W programie .NET 5+ uruchomionym w wersjach systemu Windows wymienionych w tabeli ICU w systemie Windows powyższe wydruki fragmentu kodu:
True
True
True
False
False
Aby uniknąć tego zachowania, użyj przeciążenia parametru char
lub StringComparison.Ordinal
.
TimeZoneInfo.FindSystemTimeZoneById
Funkcja ICU zapewnia elastyczność tworzenia TimeZoneInfo wystąpień przy użyciu identyfikatorów stref czasowych IANA , nawet jeśli aplikacja jest uruchomiona w systemie Windows. Podobnie można tworzyć TimeZoneInfo wystąpienia z identyfikatorami stref czasowych systemu Windows, nawet w przypadku uruchamiania na platformach innych niż Windows. Należy jednak pamiętać, że ta funkcja nie jest dostępna w przypadku korzystania z trybu NLS lub niezmiennego trybu globalizacji.
Skróty dni tygodnia
Metoda DateTimeFormatInfo.GetShortestDayName(DayOfWeek) uzyskuje najkrótszą skróconą nazwę dnia dla określonego dnia tygodnia.
- W wersji .NET Core 3.1 i starszych w systemie Windows skróty dnia tygodnia składały się z dwóch znaków, na przykład "Su".
- W wersjach .NET 5 i nowszych skróty dnia tygodnia składają się tylko z jednego znaku, na przykład "S".
Interfejsy API zależne od ICU
Platforma .NET wprowadziła interfejsy API zależne od ICU. Te interfejsy API mogą zakończyć się powodzeniem tylko w przypadku korzystania z ICU. Oto kilka przykładów:
W wersjach systemu Windows wymienionych w tabeli sekcji ICU w systemie Windows wymienione interfejsy API kończą się powodzeniem. Jednak w starszych wersjach systemu Windows te interfejsy API kończą się niepowodzeniem. W takich przypadkach można włączyć funkcję ICU app-local, aby zapewnić powodzenie tych interfejsów API. Na platformach innych niż Windows te interfejsy API zawsze kończą się powodzeniem niezależnie od wersji.
Ponadto niezwykle ważne jest, aby aplikacje miały pewność, że nie działają w trybie globalizacji lub w trybie NLS, aby zagwarantować sukces tych interfejsów API.
Używanie nls zamiast ICU
Użycie ICU zamiast NLS może spowodować różnice behawioralne w niektórych operacjach związanych z globalizacją. Aby powrócić do korzystania z równoważenia obciążenia sieciowego, możesz zrezygnować z implementacji ICU. Aplikacje mogą włączyć tryb NLS na dowolny z następujących sposobów:
W pliku projektu:
<ItemGroup> <RuntimeHostConfigurationOption Include="System.Globalization.UseNls" Value="true" /> </ItemGroup>
W pliku
runtimeconfig.json
:{ "runtimeOptions": { "configProperties": { "System.Globalization.UseNls": true } } }
Ustawiając zmienną środowiskową
DOTNET_SYSTEM_GLOBALIZATION_USENLS
na wartośćtrue
lub1
.
Uwaga
Wartość ustawiona w projekcie lub w runtimeconfig.json
pliku ma pierwszeństwo przed zmienną środowiskową.
Aby uzyskać więcej informacji, zobacz Ustawienia konfiguracji środowiska uruchomieniowego.
Określanie, czy aplikacja korzysta z interfejsu ICU
Poniższy fragment kodu może pomóc w ustaleniu, czy aplikacja jest uruchomiona z bibliotekami ICU (a nie 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;
}
Aby określić wersję platformy .NET, użyj polecenia RuntimeInformation.FrameworkDescription.
Lokalne ICU aplikacji
Każda wersja ICU może przynieść poprawki błędów i zaktualizować dane common locale Data Repository (CLDR), które opisują języki świata. Przenoszenie między wersjami ICU może subtelnie wpływać na zachowanie aplikacji, jeśli chodzi o operacje związane z globalizacją. Aby ułatwić deweloperom aplikacji zapewnienie spójności we wszystkich wdrożeniach, platforma .NET 5 i nowsze wersje umożliwiają aplikacjom zarówno w systemach Windows, jak i Unix przenoszenie i używanie własnej kopii ICU.
Aplikacje mogą zdecydować się na tryb implementacji lokalnego ICU aplikacji na jeden z następujących sposobów:
W pliku projektu ustaw odpowiednią
RuntimeHostConfigurationOption
wartość:<ItemGroup> <RuntimeHostConfigurationOption Include="System.Globalization.AppLocalIcu" Value="<suffix>:<version> or <version>" /> </ItemGroup>
Lub w pliku runtimeconfig.json ustaw odpowiednią
runtimeOptions.configProperties
wartość:{ "runtimeOptions": { "configProperties": { "System.Globalization.AppLocalIcu": "<suffix>:<version> or <version>" } } }
Możesz też ustawić zmienną środowiskową
DOTNET_SYSTEM_GLOBALIZATION_APPLOCALICU
na wartość<suffix>:<version>
lub<version>
.<suffix>
: Opcjonalny sufiks o długości mniejszej niż 36 znaków, zgodnie z publiczną konwencją pakowania ICU. Podczas tworzenia niestandardowego ICU można dostosować go w celu utworzenia nazw lib i wyeksportowanych nazw symboli, aby zawierał sufiks, na przykładlibicuucmyapp
, gdziemyapp
jest sufiksem.<version>
: Prawidłowa wersja ICU, na przykład 67.1. Ta wersja służy do ładowania plików binarnych i pobierania wyeksportowanych symboli.
Po ustawieniu jednej z tych opcji możesz dodać do projektu plik Microsoft.ICU.ICU4C.Runtime PackageReference
odpowiadający skonfigurowanemu version
elementowi i wszystkiemu, co jest potrzebne.
Alternatywnie, aby załadować ICU po ustawieniu przełącznika lokalnego aplikacji, platforma .NET używa NativeLibrary.TryLoad metody , która sonduje wiele ścieżek. Metoda najpierw próbuje znaleźć bibliotekę we NATIVE_DLL_SEARCH_DIRECTORIES
właściwości , która jest tworzona przez hosta dotnet na deps.json
podstawie pliku dla aplikacji. Aby uzyskać więcej informacji, zobacz Domyślne sondowanie.
W przypadku aplikacji samodzielnie użytkownik nie wymaga żadnej specjalnej akcji, innej niż upewnienie się, że ICU znajduje się w katalogu aplikacji (w przypadku aplikacji samodzielnie katalog roboczy domyślnie ma wartość NATIVE_DLL_SEARCH_DIRECTORIES
).
Jeśli używasz ICU za pośrednictwem pakietu NuGet, działa to w aplikacjach zależnych od platformy. Narzędzie NuGet rozpoznaje zasoby natywne i uwzględnia je w deps.json
pliku i w katalogu wyjściowym aplikacji w runtimes
katalogu . Platforma .NET ładuje go stamtąd.
W przypadku aplikacji zależnych od platformy (nie samodzielnych), w przypadku których iCU jest używane z kompilacji lokalnej, należy wykonać dodatkowe kroki. Zestaw .NET SDK nie ma jeszcze funkcji dla "luźnych" natywnych plików binarnych do włączenia deps.json
do (zobacz ten problem z zestawem SDK). Zamiast tego można to włączyć, dodając dodatkowe informacje do pliku projektu aplikacji. Na przykład:
<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>
Należy to zrobić dla wszystkich plików binarnych ICU dla obsługiwanych środowisk uruchomieniowych. NuGetPackageId
Ponadto metadane w RuntimeTargetsCopyLocalItems
grupie elementów muszą być zgodne z pakietem NuGet, do którego rzeczywiście odwołuje się projekt.
Zachowanie systemu macOS
System macOS ma inne zachowanie w przypadku rozpoznawania zależnych bibliotek dynamicznych z poleceń ładowania określonych w Mach-O
pliku niż moduł ładujący systemu Linux. W module ładujący systemu Linux platforma .NET może wypróbować libicudata
element , libicuuc
i libicui18n
(w tej kolejności), aby spełnić graf zależności ICU. Jednak w systemie macOS nie działa to. Podczas kompilowania funkcji ICU w systemie macOS domyślnie uzyskujesz bibliotekę dynamiczną z tymi poleceniami ładowania w systemie libicuuc
. Poniższy fragment kodu przedstawia przykład.
~/ % 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)
Te polecenia odwołują się tylko do nazwy bibliotek zależnych dla innych składników ICU. Moduł ładujący wykonuje wyszukiwanie zgodnie dlopen
z konwencjami, które obejmują posiadanie tych bibliotek w katalogach systemowych lub ustawienie LD_LIBRARY_PATH
wariantów env lub posiadanie ICU w katalogu na poziomie aplikacji. Jeśli nie możesz ustawić LD_LIBRARY_PATH
ani upewnić się, że pliki binarne ICU znajdują się w katalogu na poziomie aplikacji, musisz wykonać dodatkową pracę.
Istnieją pewne dyrektywy modułu ładującego, takie jak @loader_path
, które informują moduł ładujący o wyszukiwaniu tej zależności w tym samym katalogu co plik binarny za pomocą tego polecenia ładowania. Istnieją dwa sposoby osiągnięcia tego celu:
install_name_tool -change
Uruchom następujące polecenia:
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
Stosowanie poprawek ICU w celu utworzenia nazw instalacji za pomocą polecenia
@loader_path
Przed uruchomieniem autokonf (
./runConfigureICU
) zmień następujące wiersze na: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 w usłudze WebAssembly
Dostępna jest wersja ICU przeznaczona specjalnie dla obciążeń zestawu WebAssembly. Ta wersja zapewnia zgodność globalizacji z profilami komputerów stacjonarnych. Aby zmniejszyć rozmiar pliku danych ICU z 24 MB do 1,4 MB (lub ~0,3 MB, jeśli jest skompresowany z Brotli), to obciążenie ma kilka ograniczeń.
Następujące interfejsy API nie są obsługiwane:
- CultureInfo.EnglishName
- CultureInfo.NativeName
- DateTimeFormatInfo.NativeCalendarName
- RegionInfo.NativeName
Następujące interfejsy API są obsługiwane z ograniczeniami:
- String.Normalize(NormalizationForm) i String.IsNormalized(NormalizationForm) nie obsługują rzadko używanych FormKC i FormKD formularzy.
- RegionInfo.CurrencyNativeName Zwraca tę samą wartość co RegionInfo.CurrencyEnglishName.
Ponadto obsługiwana jest mniejsza liczba ustawień regionalnych. Listę obsługiwanych można znaleźć w repozytorium dotnet/icu.