Globalizace .NET a ICU

Před .NET 5 používala rozhraní API globalizace .NET různé podkladové knihovny na různých platformách. Na Unixu rozhraní API používala mezinárodní komponenty pro Unicode (ICU) a na Windows používala podporu národních jazyků (NLS). Výsledkem jsou některé rozdíly v chování několika rozhraní API globalizace při spouštění aplikací na různých platformách. Rozdíly v chování byly zřejmé v těchto oblastech:

  • Kultura a data o kultuře
  • Velikost písmen v řetězci
  • Řazení a vyhledávání řetězců
  • Řazení klíčů
  • Normalizace řetězců
  • Podpora internationalizovaných názvů domén (IDN)
  • Zobrazovaný název časového pásma v Linuxu

Počínaje platformou .NET 5 mají vývojáři větší kontrolu nad tím, jakou podkladovou knihovnu používáte, což umožňuje aplikacím vyhnout se rozdílům na různých platformách.

Poznámka:

Data o kultuře, která řídí chování knihovny ICU, se obvykle spravují úložištěm CLDR (Common Locale Data Repository), a nikoli běhovým časem.

ICU ve Windows

Systém Windows teď obsahuje předinstalovanou verzi icu.dll jako součást svých funkcí, které se automaticky používají pro úlohy globalizace. Tato úprava umožňuje rozhraní .NET používat tuto knihovnu ICU pro podporu globalizace. V případech, kdy je knihovna ICU nedostupná nebo nejde načíst, stejně jako u starších verzí Windows, .NET 5 a dalších verzí se vrátí k použití implementace založené na službě NLS.

Následující tabulka uvádí, které verze rozhraní .NET dokážou načíst knihovnu ICU napříč různými verzemi klienta a serveru Windows:

Verze .NET Verze Windows
.NET 5 nebo .NET 6 Klient Windows 10 verze 1903 nebo novější
.NET 5 nebo .NET 6 Windows Server 2022 nebo novější
.NET 7 nebo novější Klient Windows 10 verze 1703 nebo novější
.NET 7 nebo novější Windows Server 2019 nebo novější

Poznámka:

.NET 7 a novější verze mají možnost načíst ICU ve starších verzích Windows na rozdíl od .NET 6 a .NET 5.

Poznámka:

I když používáte ICU, CurrentCulture, CurrentUICulture a CurrentRegion členové stále využívají rozhraní API operačního systému Windows k zachování uživatelských nastavení.

Rozdíly v chování

Pokud upgradujete aplikaci, aby cílovala na .NET 5 nebo novější, mohou se v ní objevit změny, i když si neuvědomujete, že používáte funkce globalizace. V následující části najdete některé změny chování, ke které může dojít.

Řazení řetězců a System.Globalization.CompareOptions

CompareOptions je výčet možností, který lze předat String.Compare k ovlivnění porovnání dvou řetězců.

Porovnání řetězců pro shodnost a určení jejich pořadí řazení se liší mezi NLS a ICU. Zejména jde o toto:

  • Výchozí pořadí řazení řetězců se liší, takže to bude zřejmé i v případě, že ho nepoužíváte CompareOptions přímo. Při použití ICU se výchozí možnost None chová stejně jako StringSort. StringSort seřadí nealfanumerické znaky před alfanumerickými znaky (například "bill's" seřadí před "bills"). Pokud chcete obnovit předchozí None funkce, musíte použít implementaci založenou na NLS.
  • Výchozí zpracování znaků ligatury se liší. V rámci NLS se ligatury a jejich protějšky bez ligatur (například "oeuf" a "œuf") považují za stejné, ale nejedná se o případ ICU v .NET. Důvodem je jiná síla kolace mezi dvěma implementacemi. Pokud chcete obnovit chování NLS při použití ICU, použijte hodnotu CompareOptions.IgnoreNonSpace.

String.IndexOf

Představte si následující kód, který volá funkci String.IndexOf(String), aby našel index nulového znaku \0 v řetězci.

const string greeting = "Hel\0lo";
Console.WriteLine($"{greeting.IndexOf("\0")}");
Console.WriteLine($"{greeting.IndexOf("\0", StringComparison.CurrentCulture)}");
Console.WriteLine($"{greeting.IndexOf("\0", StringComparison.Ordinal)}");
  • V .NET Core 3.1 a starších verzích ve Windows se fragment kódu vytiskne 3 na každý ze tří řádků.
  • V případě .NET 5 a novějších verzí spuštěných ve verzích Windows uvedených v tabulce sekce ICU v systému Windows se fragment kódu vytiskne 0, 0 a 3 (pro řadové vyhledávání).

Ve výchozím nastavení String.IndexOf(String) provede lingvistické vyhledávání citlivé na kulturní rozdíly. ICU považuje znak \0 za znak nulové hmotnosti, a proto se v řetězci při použití lingvistického vyhledávání v .NET 5 a novějším nenajde. NLS však nepovažuje znak null \0 za znak s nulovou váhou a ve verzích .NET Core 3.1 a starších je při lingvistickém vyhledávání tento znak nalezen na pozici 3. Řadové vyhledávání najde znak na pozici 3 ve všech verzích .NET.

Pravidla analýzy kódu CA1307: Zadejte StringComparison pro lepší přehlednost a CA1309: Použijte ordinal StringComparison k nalezení míst volání ve vašem kódu, kde není zadané porovnání řetězců nebo není pořadové.

Další informace najdete v tématu Osvědčené postupy pro porovnávání řetězců v .NET.

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'));

Důležité

V .NET 5 a novějších verzích spuštěných na Windows uvedených v tabulce ICU ve Windows se zobrazí předchozí ukázka:

True
True
True
False
False

Chcete-li se tomuto chování vyhnout, použijte přetížení parametru char nebo 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'));

Důležité

V .NET 5 a novějších verzích spuštěných na Windows uvedených v tabulce ICU ve Windows se zobrazí předchozí ukázka:

True
True
True
False
False

Chcete-li se tomuto chování vyhnout, použijte přetížení parametru char nebo StringComparison.Ordinal.

TimeZoneInfo.FindSystemTimeZoneById

ICU poskytuje flexibilitu při vytváření TimeZoneInfo instancí pomocí ID časových pásem IANA , a to i v případě, že aplikace běží ve Windows. Podobně můžete vytvářet TimeZoneInfo instance s ID časových pásem Windows, i když běží na jiných platformách než Windows. Je však důležité si uvědomit, že tato funkce není dostupná při použití režimu NLS nebo režimu globální invariantnosti.

Zkratky dnů v týdnu

Metoda DateTimeFormatInfo.GetShortestDayName(DayOfWeek) získá nejkratší zkrácený název dne pro zadaný den v týdnu.

  • V .NET Core 3.1 a starších verzích na Windows se tyto zkratky dnů v týdnu skládaly ze dvou znaků, například "Su".
  • V .NET 5 a novějších verzích se tyto zkratky dnů v týdnu skládají jenom z jednoho znaku, například "S".

Rozhraní API závislá na ICU

Rozhraní .NET představilo API, která jsou závislá na ICU. Tato rozhraní API můžou být úspěšná pouze při použití ICU. Několik příkladů:

Ve verzích Windows uvedených v sekci 'ICU ve Windows' uvedená rozhraní API fungují úspěšně. Ve starších verzích Windows však tato rozhraní API selžou. V takových případech můžete povolit funkci ICU místní aplikace, abyste zajistili úspěch těchto rozhraní API. Na jiných platformách než Windows jsou tato rozhraní API vždy úspěšná bez ohledu na verzi.

Kromě toho je pro aplikace důležité zajistit, aby neběžely v režimu invariantní globalizace nebo v režimu NLS, aby byl zajištěn úspěch těchto rozhraní API.

Místo ICU použijte službu NLS.

Použití ICU namísto NLS může způsobit rozdíly v chování u některých operací souvisejících s globalizací. Pokud se chcete vrátit zpět k používání nlS, můžete vyjádřit výslovný nesouhlas s implementací ICU. Aplikace můžou povolit režim služby NLS některým z následujících způsobů:

  • V souboru projektu:

    <ItemGroup>
      <RuntimeHostConfigurationOption Include="System.Globalization.UseNls" Value="true" />
    </ItemGroup>
    
  • V souboru runtimeconfig.json:

    {
      "runtimeOptions": {
         "configProperties": {
           "System.Globalization.UseNls": true
          }
      }
    }
    
  • Nastavením proměnné DOTNET_SYSTEM_GLOBALIZATION_USENLS prostředí na hodnotu true nebo 1.

Poznámka:

V .NET 9 a novějších verzích má přednost nastavení proměnné prostředí. V předchozích verzích má hodnota nastavená v projektu nebo v runtimeconfig.json souboru přednost před proměnnou prostředí.

Další informace naleznete v tématu Nastavení konfigurace modulu runtime.

Určení, jestli vaše aplikace používá ICU

Následující fragment kódu vám může pomoct určit, jestli je vaše aplikace spuštěná s knihovnami ICU (a ne 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;
}

K určení verze rozhraní .NET použijte RuntimeInformation.FrameworkDescription.

Místní ICU pro aplikace

Každá verze ICU může obsahovat opravy chyb a aktualizovaná data cLDR (Common Locale Data Repository), která popisují jazyky světa. Přechod mezi verzemi ICU může subtálně ovlivnit chování aplikace, pokud jde o operace související s globalizací. Aby vývojáři aplikací zajistili konzistenci napříč všemi nasazeními, umožňují .NET 5 a novějším verzím aplikacím ve Windows i Unixu přenášet a používat vlastní kopii ICU.

Aplikace se můžou přihlásit k režimu implementace ICU v místní aplikaci jedním z následujících způsobů:

  • V souboru projektu nastavte odpovídající RuntimeHostConfigurationOption hodnotu:

    <ItemGroup>
      <RuntimeHostConfigurationOption Include="System.Globalization.AppLocalIcu" Value="<suffix>:<version> or <version>" />
    </ItemGroup>
    
  • Nebo v souboru runtimeconfig.json nastavte odpovídající runtimeOptions.configProperties hodnotu:

    {
      "runtimeOptions": {
         "configProperties": {
           "System.Globalization.AppLocalIcu": "<suffix>:<version> or <version>"
         }
      }
    }
    
  • Nebo nastavením proměnné DOTNET_SYSTEM_GLOBALIZATION_APPLOCALICU prostředí na hodnotu <suffix>:<version> nebo <version>.

    <suffix>: Volitelná přípona kratší než 36 znaků, která následuje veřejné konvence balení ICU. Při vytváření vlastní jednotky ICU ji můžete přizpůsobit tak, aby vznikly názvy libů a exportované názvy symbolů, které budou obsahovat příponu, libicuucmyappnapříklad , kde myapp je přípona.

    <version>: Platná verze ICU, například 67.1. Tato verze slouží k načtení binárních souborů a získání exportovaných symbolů.

Pokud je některý z těchto možností nastavený, můžete do projektu přidat Microsoft.ICU.ICU4C.RuntimePackageReference, který odpovídá nakonfigurované version a to vše, co je potřeba.

Pokud chcete načíst ICU, když je přepínač místního režimu aplikace nastaven, .NET použije metodu NativeLibrary.TryLoad, která testuje více cest. Metoda se nejprve pokusí najít knihovnu ve vlastnosti NATIVE_DLL_SEARCH_DIRECTORIES, která je vytvořena hostitelem dotnet na základě souboru deps.json pro aplikaci. Další informace najdete v tématu Výchozí kontrola.

V případě samostatných aplikací nevyžaduje uživatel žádnou zvláštní akci, kromě zajištění, že je ICU v adresáři aplikace (u samostatných aplikací je výchozí NATIVE_DLL_SEARCH_DIRECTORIESpracovní adresář).

Pokud icU využíváte prostřednictvím balíčku NuGet, funguje to v aplikacích závislých na architektuře. NuGet rozpozná nativní prostředky a zahrne je do tohoto deps.json souboru a do výstupního adresáře pro aplikaci pod adresářem runtimes. .NET ho odtud načte.

U aplikací závislých na architektuře (ne v samostatném prostředí), které spotřebovávají JEDNOTKY ICU z místního sestavení, musíte provést další kroky. Sada .NET SDK zatím nemá funkci pro "volné" nativní binární soubory, do deps.json kterých se mají začlenit (viz tento problém se sadou SDK). Místo toho to můžete povolit přidáním dalších informací do souboru projektu aplikace. Příklad:

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

To je nutné provést pro veškeré binární soubory ICU pro podporovaná prostředí runtime. NuGetPackageId Metadata ve RuntimeTargetsCopyLocalItems skupině položek musí také odpovídat balíčku NuGet, na který projekt ve skutečnosti odkazuje.

Načtení konkrétní verze ICU v Linuxu

Při použití ICU v Linuxu se .NET pokusí načíst nejnovější nainstalovanou verzi ICU ze systému. Můžete ale zadat konkrétní verzi ICU, která se má načíst, nastavením proměnné prostředí DOTNET_ICU_VERSION_OVERRIDE.

Pokud je například proměnná prostředí nastavená na konkrétní číslo verze, například 67.1, .NET se pokusí načíst tuto verzi ICU. Například .NET hledá knihovny libicuuc.so.67.1 a libicui18n.so.67.1.

Poznámka:

Tato proměnná prostředí je podporována pouze v sestaveních .NET poskytovaných společností Microsoft a není podporována ve verzích distribucí Linuxu. Pro verze .NET starší než .NET 10 se proměnná prostředí nazývá CLR_ICU_VERSION_OVERRIDE.

Pokud se zadaná verze nenajde, .NET přejde na načtení nejvyšší nainstalované verze ICU ze systému.

Tato konfigurace poskytuje flexibilitu při řízení využití verzí ICU a zajišťuje kompatibilitu s verzemi ICU poskytovanými konkrétními aplikacemi nebo systémem.

Chování macOS

macOS má jiné chování při vyhodnocování závislých dynamických knihoven z načítacích příkazů zadaných v souboru Mach-O, než tomu je u zavaděče Linuxu. V zavaděči Linuxu může .NET vyzkoušet libicudata, libicuuc a libicui18n (v tomto pořadí) k uspokojení grafu závislostí ICU. V systému macOS to ale nefunguje. Při vytváření ICU v systému macOS ve výchozím nastavení získáte dynamickou knihovnu s těmito příkazy pro načtení v libicuuc. Následující fragment kódu ukazuje příklad.

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

Tyto příkazy pouze odkazují na název závislých knihoven pro ostatní komponenty ICU. Zavaděč provádí vyhledávání podle dlopen konvencí, které zahrnují použití těchto knihoven v systémových adresářích nebo nastavení LD_LIBRARY_PATH proměnných prostředí, nebo mít ICU v adresáři na úrovni aplikace. Pokud nemůžete nastavit LD_LIBRARY_PATH nebo zajistit, aby binární soubory ICU byly v adresáři na úrovni aplikace, budete muset udělat další práci.

Pro zavaděč existují určité direktivy, například @loader_path, které zavaděče říkají, že má vyhledat danou závislost ve stejném adresáři jako binární soubor s tímto příkazem pro načtení. Existují dva způsoby, jak toho dosáhnout:

  • install_name_tool -change

    Spusťte následující příkazy:

    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
    
  • Upravit ICU pro generování názvů instalací pomocí @loader_path

    Před spuštěním příkazu autoconf (./runConfigureICU) změňte tyto řádky 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 na WebAssembly

K dispozici je verze ICU určená speciálně pro úlohy WebAssembly. Tato verze poskytuje podporu globalizace s desktopovými profily. Pokud chcete zmenšit velikost datového souboru ICU z 24 MB na 1,4 MB (nebo ~0,3 MB v případě komprimace pomocí Brotli), má tato úloha několik omezení.

Následující rozhraní API nejsou podporována:

Následující rozhraní API jsou podporována s omezeními:

Kromě toho je podporováno méně lokalit. Podporovaný seznam najdete v úložišti dotnet/icu.

Nastavení globalizace v aplikacích .NET

Inicializace globalizace .NET je složitý proces, který zahrnuje načtení příslušné knihovny globalizace, nastavení údajů o kultuře a konfiguraci nastavení globalizace. Následující části popisují, jak inicializace globalizace funguje na různých platformách.

Windows

Ve Windows .NET inicializuje globalizaci pomocí následujících kroků:

  • Zkontrolujte, jestli je povolený režim invariantní globalizace . Pokud je tento režim aktivní, rozhraní .NET obchází načítání knihovny ICU a nepoužívá rozhraní API služby NLS. Místo toho spoléhá na vestavěná invariantní kulturní data, čímž zajišťuje, že chování zůstává plně nezávislé na operačním systému a knihovně ICU.

  • Zkontrolujte, jestli je povolený režim služby NLS . Pokud je tato možnost povolená, rozhraní .NET přeskočí načítání knihovny ICU a místo toho spoléhá na rozhraní API služby Windows NLS pro podporu globalizace.

  • Zkontrolujte, jestli je povolená funkce místní ICU aplikace. Pokud ano, rozhraní .NET se pokusí načíst knihovnu ICU z adresáře aplikace připojením zadané verze k názvům knihoven. Pokud je například verze 72.1, pokusí se .NET nejprve načíst icuuc72.dll, icuin72.dlla icudt72.dll. Pokud tyto knihovny nelze načíst, pokusí se načíst icuuc72.1.dll, icuin72.1.dlla icudt72.1.dll. Pokud se žádná z knihoven nenajde, proces se ukončí chybovou zprávou, například: Failed to load app-local ICU: {library name}.

  • Pokud nejsou splněny žádné z předchozích podmínek, rozhraní .NET se pokusí načíst knihovnu ICU ze systémového adresáře. Nejprve se pokusí načíst icu.dll. Pokud tato knihovna není k dispozici, pokusí se načíst icuuc.dll a icuin.dll z systémového adresáře. Pokud některé z těchto knihoven nenajdete, modul runtime se vrátí k použití rozhraní API NLS pro podporu globalizace.

Poznámka:

Rozhraní API služby NLS jsou vždy dostupná ve všech verzích Windows, takže .NET se na ně může kdykoli vrátit kvůli podpoře globalizace.

Operační systém Linux

  • Zkontrolujte, jestli je povolený režim invariantní globalizace . Pokud je tento režim aktivní, .NET obchází načítání knihovny ICU. Místo toho spoléhá na vestavěná invariantní kulturní data, čímž zajišťuje, že chování zůstává plně nezávislé na operačním systému a knihovně ICU.
  • Zkontrolujte, jestli je povolená funkce místní ICU aplikace. Pokud ano, rozhraní .NET se pokusí načíst knihovnu ICU z adresáře aplikace připojením zadané verze k názvům knihoven. Pokud je například verze 68.2.0.9, pokusí se .NET načíst libicuuc.so.68.2.0.9 a libicui18n.so.68.2.0.9. Pokud se žádná z knihoven nenajde, proces se ukončí chybovou zprávou, například: Failed to load app-local ICU: {library name}.
  • Zkontrolujte, jestli je proměnná prostředí DOTNET_ICU_VERSION_OVERRIDE nastavená. Pokud ano, rozhraní .NET se pokusí načíst zadanou verzi ICU, jak je popsáno v části Načtení konkrétní verze ICU v Linuxu.
  • Pokud nejsou splněny žádné z předchozích podmínek, rozhraní .NET se pokusí načíst nejvyšší nainstalovanou verzi knihovny ICU ze systému. Snaží se načíst knihovny libicuuc.so.[version] a libicui18n.so.[version], kde [version] je nejvyšší nainstalovaná verze ICU v systému. Pokud knihovny nenajdete, proces se ukončí chybovou zprávou, například: Failed to load system ICU: {library name}.

macOS

  • Zkontrolujte, jestli je povolený režim invariantní globalizace . Pokud je tento režim aktivní, .NET obchází načítání knihovny ICU. Místo toho spoléhá na vestavěná invariantní kulturní data, čímž zajišťuje, že chování zůstává plně nezávislé na operačním systému a knihovně ICU.
  • Zkontrolujte, jestli je povolená funkce místní ICU aplikace. Pokud ano, rozhraní .NET se pokusí načíst knihovnu ICU z adresáře aplikace připojením zadané verze k názvům knihoven. Pokud je například verze 68.2.0.9, pokusí se .NET načíst libicuuc68.2.0.9.dylib a libicui18n68.2.0.9.dylib. Pokud se nenajde žádná z knihoven, proces se ukončí chybovou zprávou, například: Failed to load app-local ICU: {library name}.
  • Pokud nejsou splněny žádné z předchozích podmínek, pokusí se .NET načíst nainstalovanou verzi knihovny ICU, jak je popsáno v chování systému macOS.