.NET 全球化和 ICU

在 .NET 5 前,.NET 全球化 API 在不同的平台上使用不同的基础库。 在 Unix 上,API 使用 Unicode 国际组件 (ICU),在 Windows 上,API 使用 区域语言支持 (NLS)。 这导致在不同平台上运行应用程序时,在少数全球化 API 中存在一些行为差异。 以下方面就存在明显的行为差异:

  • 区域性和区域性数据
  • 字符串大小写
  • 字符串排序和搜索
  • 排序关键字
  • 字符串规范化
  • 国际化域名 (IDN) 支持
  • Linux 上的时区显示名称

从 .NET 5 开始,开发人员可以更好地控制使用哪个基础库,从而使应用程序可以避免不同平台之间的差异。

Windows 上的 ICU

Windows 2019 年 5 月 10 日更新及更高版本将 icu.dll 作为 OS 的一部分,并且 .NET 5 和更高版本默认使用 ICU。 在 Windows 上运行时,.NET 5 和更高版本尝试加载 icu.dll,如果此库可用,将使用它进行全球化实现。 如果无法找到或无法加载 ICU 库,如在较早版本的 Windows 上运行时,.NET 5 和更高版本将回退到基于 NLS 的实现。

注意

即使使用 ICU,CurrentCultureCurrentUICultureCurrentRegion 成员仍使用 Windows 操作系统 API 来遵从用户设置。

行为差异

如果你将应用升级到目标 .NET 5,即使你不知道正在使用全球化设施,你也可能会在应用中看到更改。 本部分列出了你可能会看到的行为更改之一,但还有其他一些行为更改。

String.IndexOf

请考虑使用以下代码,它调用 String.IndexOf(String) 来查找字符串中的 null 字符 \0 的索引。

const string greeting = "Hel\0lo";
Console.WriteLine($"{greeting.IndexOf("\0")}");
Console.WriteLine($"{greeting.IndexOf("\0", StringComparison.CurrentCulture)}");
Console.WriteLine($"{greeting.IndexOf("\0", StringComparison.Ordinal)}");
  • 在 .NET Core 3.1 和 Windows 上的更早版本中,代码片段在三行的每一行中打印 3
  • 在 Windows 19H1 和更高版本上的 .NET 5 及更高版本中,代码片段会打印 003(对于顺序搜索)。

默认情况下,String.IndexOf(String) 执行区域性感知的语言搜索。 ICU 将 null 字符 \0 视为零权重字符,因此在 .NET 5 和更高版本中使用语言搜索时,在字符串中找不到该字符。 但是,NLS 不会将 null 字符 \0 视为零权重字符,.NET Core 3.1 和更早版本上执行语言搜索会在位置 3 找到该字符。 顺序搜索在所有 .NET 版本的位置 3 处找到该字符。

可以运行代码分析规则 CA1307:指定 StringComparison 以阐明CA1309:使用顺序 StringComparison,在未指定字符串比较或不是顺序搜索的代码中找到调用点。

有关详细信息,请参阅在 .NET 5 及更高版本中比较字符串时的行为更改

使用 NLS 而不是 ICU

使用 ICU 代替 NLS 可能会导致与一些与全球化相关的操作存在行为差异。 若要恢复为使用 NLS,开发人员可以选择退出 ICU 实现。 应用程序可以通过以下任意方式启用 NLS 模式:

  • 在项目文件中:

    <ItemGroup>
      <RuntimeHostConfigurationOption Include="System.Globalization.UseNls" Value="true" />
    </ItemGroup>
    
  • runtimeconfig.json 文件中:

    {
      "runtimeOptions": {
         "configProperties": {
           "System.Globalization.UseNls": true
          }
      }
    }
    
  • 通过将环境变量 DOTNET_SYSTEM_GLOBALIZATION_USENLS 设置为值 true1

注意

在项目或 runtimeconfig.json 文件中设置的值优先于环境变量。

有关详细信息,请参阅运行时配置设置

确定应用是否正在使用 ICU

以下代码片段可帮助你确定应用是否正在运行 ICU 库(而不是 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;
}

如需确定 .NET 的版本,请使用 RuntimeInformation.FrameworkDescription

应用本地 ICU

每个版本的 ICU 都可能附带了 bug 修复以及描述世界语言的更新公共区域设置数据存储库 (CLDR) 数据。 当涉及与全球化相关的操作时,在 ICU 版本间移动可能会对应用行为产生细微影响。 为了帮助应用程序开发人员确保所有部署之间的一致性,.NET 5 和更高版本使 Windows 和 Unix 上的应用能够携带和使用其自己的 ICU 副本。

应用程序可以通过以下任一方式选择使用应用本地 ICU 实现模式:

  • 在项目文件中:

    <ItemGroup>
      <RuntimeHostConfigurationOption Include="System.Globalization.AppLocalIcu" Value="<suffix>:<version> or <version>" />
    </ItemGroup>
    
  • runtimeconfig.json 文件中:

    {
      "runtimeOptions": {
         "configProperties": {
           "System.Globalization.AppLocalIcu": "<suffix>:<version> or <version>"
         }
      }
    }
    
  • 通过将环境变量 DOTNET_SYSTEM_GLOBALIZATION_APPLOCALICU 设置为值 <suffix>:<version><version>

    <suffix>:长度小于 36 个字符的可选后缀,遵循公共 ICU 打包约定。 在生成自定义 ICU 时,可以对其自定义以生成 lib 名称并导出符号名称以包含后缀,例如 libicuucmyapp,其中 myapp 是后缀。

    <version>:有效的 ICU 版本,例如 67.1。 此版本用于加载二进制文件和获取导出的符号。

为了在设置应用本地开关时加载 ICU,.NET 使用 NativeLibrary.TryLoad 方法探测多个路径。 该方法首先尝试在 NATIVE_DLL_SEARCH_DIRECTORIES 属性中查找库,该属性由 dotnet 主机基于应用的 deps.json 文件创建。 有关更多信息,请参阅默认探测

对于独立应用,用户不需要执行任何特殊操作,只需确保 ICU 位于应用目录中(对于独立应用,工作目录默认为 NATIVE_DLL_SEARCH_DIRECTORIES)。

如果通过 NuGet 包使用 ICU,则可在依赖框架的应用程序中使用。 NuGet 解析本机资产,并将它们包含在 deps.json 文件和 runtimes 目录下应用程序的输出目录中。 .NET 从这里加载它。

对于在本地版本使用 ICU 的依赖框架的应用(非独立应用),必须执行额外的步骤。 .NET SDK 尚不具有用于将“松散”的本机二进制文件合并到 deps.json 中的功能(请参阅此 SDK 问题)。 相反,你可以通过将其他信息添加到应用程序的项目文件来启用此功能。 例如:

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

必须为支持的运行时的所有 ICU 二进制文件执行此操作。 此外,RuntimeTargetsCopyLocalItems 项组中的 NuGetPackageId 元数据需要与项目实际引用的 NuGet 包匹配。

macOS 行为

macOS 关于使用 match-o 文件中指定的加载命令解析依赖的动态库的行为与 Linux 加载程序不同。 在 Linux 加载程序中,.NET 可以尝试使用 libicudatalibicuuclibicui18n(按此顺序)来满足 ICU 依赖项关系图。 但在 macOS 上,这不起作用。 在 macOS 上生成 ICU 时,默认情况下,可以使用 libicuuc 中的这些加载命令获取动态库。 以下代码片段演示了一个示例。

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

这些命令只引用 ICU 其他组件的依赖库的名称。 加载程序将按照 dlopen 约定执行搜索,这涉及到在系统目录中包含这些库,或设置 LD_LIBRARY_PATH env var,或在应用级目录中包含 ICU。 如果无法设置 LD_LIBRARY_PATH 或无法确保 ICU 二进制文件位于应用级目录中,则需要执行一些额外的操作。

对于加载程序,有一些指令(如 @loader_path)会告知加载程序使用加载命令在与二进制文件相同的目录中搜索该依赖项。 可通过两种方式实现此目的:

  • install_name_tool -change

    运行以下命令:

    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
    
  • 修补 ICU 以生成含有 @loader_path 的安装名称

    在运行 autoconf (./runConfigureICU) 之前,将这些行更改为以下内容:

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

WebAssembly 上的 ICU

存在专门针对 WebAssembly 工作负荷的 ICU 版本。 此版本提供与桌面配置文件的全球化兼容性。 若要将 ICU 数据文件大小从 24 MB 减小到 1.4 MB(如果使用 Brotli 压缩,可压缩到约 0.3 MB),则此工作负荷有少量限制。

不支持以下 API:

支持以下 API,但有一些限制:

此外,还可以在 dotnet/icu 存储库中找到受支持区域设置的列表。