Udostępnij za pośrednictwem


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, CurrentCultureCurrentUICulture, 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ślna None opcja wykonuje to samo co StringSort. StringSort sortuje znaki inne niż alfanumeryczne przed alfanumeryczne (więc "bill's" sortuje przed "rachunkami", na przykład). Aby przywrócić poprzednie None 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 0wartości , 0i 3 (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 lub 1.

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ład libicuucmyapp, gdzie myapp 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ć libicudataelement , libicuuci 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:

Następujące interfejsy API są obsługiwane z ograniczeniami:

Ponadto obsługiwana jest mniejsza liczba ustawień regionalnych. Listę obsługiwanych można znaleźć w repozytorium dotnet/icu.