.NET 全球化和 ICU

在 .NET 5 以前,.NET 全球化 API 在不同的平台上會使用不同的基礎程式庫。 API 在 Unix 上使用的是 Unicode 國際元件 (ICU),在 Windows 上使用的則是國家語言支援 (NLS)。 這會導致少數全球化 API 在應用程式於不同平台上執行時出現行為差異。 以下區域的行為差異非常明顯:

  • 文化特性和文化特性資料
  • 字串大小寫
  • 字串排序和搜尋
  • 排序鍵
  • 字串正規化
  • 國際化網域名稱 (IDN) 的支援
  • Linux 上的時區顯示名稱

從 .NET 5 開始,開發人員可以更充分掌控其使用的基礎程式庫,讓應用程式可以避免不同平台之間的差異。

Windows 上的 ICU

Windows 現在會將預先安裝的 icu.dll 版本納入其自動用於全球化工作的功能之中。 這項修改可讓 .NET 利用此 ICU 程式庫提供全球化支援。 如果 ICU 程式庫無法使用或無法載入,如同舊版 Windows 的情況,.NET 5 和後續版本會還原為使用 NLS 型實作。

下表顯示哪些 .NET 版本能夠在不同的 Windows 用戶端和伺服器版本中,載入 ICU 程式庫:

.NET 版本 Windows 版本
.NET 5 或 .NET 6 Windows 用戶端 10 版本 1903 或更新版本
.NET 5 或 .NET 6 Windows Server 2022 或更新版本
.NET 7 或更新版本 Windows 用戶端 10 版本 1703 或更新版本
.NET 7 或更新版本 Windows Server 2019 或更新版本

注意

與 .NET 6 和 .NET 5 相反,.NET 7 和更新版本具有在舊版 Windows 上載入 ICU 的功能。

注意

即使使用 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)}");
  • 在 Windows 上的 .NET Core 3.1 和更舊版本當中,此程式碼片段會在這三行的每一行列印 3
  • 針對在 Windows 版本上執行的 .NET 5 和後續版本 (列於 Windows 上 ICU 的區段表格中),程式碼片段會列印 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+ 上比較字串時的行為變更

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

重要

在 Windows 版本上執行的 .NET 5+ 中 (列於 Windows 上 ICU 表),下列程式碼片段會列印:

True
True
True
False
False

若要避免此行為,請使用 char 參數多載或 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'));

重要

在 Windows 版本上執行的 .NET 5+ 中 (列於 Windows 上 ICU 表),下列程式碼片段會列印:

True
True
True
False
False

若要避免此行為,請使用 char 參數多載或 StringComparison.Oridinal

TimeZoneInfo.FindSystemTimeZoneById

即使應用程式在 Windows 上執行,ICU 也能彈性地使用 IANA 時區 ID 建立 TimeZoneInfo 執行個體。 同樣地,即使在 Windows 以外的平台上執行,您也可以使用 Windows 時區 ID 來建立 TimeZoneInfo 執行個體。 不過,請務必注意,使用 NLS 模式全球化非變異模式時,無法使用此功能。

ICU 相依 API

.NET 引進了相依於 ICU 的 API。 這些 API 只有在使用 ICU 時才能成功。 以下列出一些範例:

Windows 上 ICU 區段表中所列的 Windows 版本上,提及的 API 會持續成功。 不過,在舊版 Windows 上,這些 API 會持續失敗。 在這種情況下,您可以啟用應用程式本機 ICU 功能,確保這些 API 能夠成功。 在 Windows 以外的平台上,無論版本為何,這些 API 一律都會成功。

此外,應用程式必須確保並非在全球化非變異模式NLS 模式中執行,才能保證這些 API 會成功。

使用 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 版本都可能有其錯誤修正程式,還有用來描述全球不同語言的更新版通用地區設定資料存放庫 (CLDR) 資料。 進行全球化相關作業時,在不同的 ICU 版本間切換可能會稍微影響應用程式的行為。 為了幫助應用程式開發人員確保所有部署的一致性,.NET 5 和更新版本可讓 Windows 和 Unix 上的應用程式攜帶和使用自己的 ICU。

應用程式可以透過下列方式之一選擇加入一個應用程式本機 ICU 實作模式:

  • 在專案檔中設定適當的 RuntimeHostConfigurationOption 值:

    <ItemGroup>
      <RuntimeHostConfigurationOption Include="System.Globalization.AppLocalIcu" Value="<suffix>:<version> or <version>" />
    </ItemGroup>
    
  • 或者,在 runtimeconfig.json 檔案中設定適當的 runtimeOptions.configProperties 值:

    {
      "runtimeOptions": {
         "configProperties": {
           "System.Globalization.AppLocalIcu": "<suffix>:<version> or <version>"
         }
      }
    }
    
  • 或者,將環境變數 DOTNET_SYSTEM_GLOBALIZATION_APPLOCALICU 的值設為 <suffix>:<version><version>

    <suffix>:加在公用 ICU 封裝慣例後方,長度為 36 個字元以內的選擇性尾碼。 建置自訂 ICU 時,您可以自訂它以產生包含尾碼的程式庫名稱和匯出符號名稱,例如 libicuucmyapp,其中 myapp 是尾碼。

    <version>:有效的 ICU 版本,例如 67.1。 此版本可載入二進位檔案並取得匯出的符號。

設定好上述任一選項之後,您只需要將 Microsoft.ICU.ICU4C.RuntimePackageReference 新增至對應到已設定之 version 的專案即可。

或者,如果要在設定好應用程式本機參數的狀況下載入 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 用 Mach-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 vars,或將 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
    
  • @loader_path 修補 ICU 以產生安裝名稱

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

ICU 有一個 WebAssembly 工作負載專用的版本。 此版本提供與桌面設定檔的全球化相容性。 若要將此 ICU 資料檔案大小從 24 MB 減少到 1.4 MB(或用 Brotli 壓縮到 ~0.3 MB),此工作負載會有一些限制。

不支援下列 API:

支援下列 API,但有限制:

此外,支援的地區設定也比較少。 您可以在 dotnet/icu 存放庫中找到支援的清單。