共用方式為


.NET 全球化和 ICU

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

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

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

注意

驅動 ICU 函式庫行為的文化特性資料通常由 Common Locale Data Repository (CLDR) 維護,而不是執行階段。

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 或更新版本為目標而升級您的應用程式,即使您不知道您使用的是全球化設施,也可能會在應用程式中看到變更。 下一節列出您可能遇到的一些行為變更。

字串排序和 System.Globalization.CompareOptions

CompareOptions 是可傳遞至 String.Compare 的選項列舉,以影響兩個字串的比較方式。

比較字串是否相等,並判斷其排序次序在 NLS 和 ICU 之間有所不同。 特別是:

  • 預設字串排序次序不同,因此即使您未直接使用 CompareOptions,這也會很明顯。 使用 ICU 時,None 預設選項會與 StringSort 執行相同。 StringSort 會先排序非英數字元後,再排序英數字元 (例如,"bill's" 會較 "bills" 優先排序)。 若要還原先前的 None 功能,您必須使用以 NLS 為基礎的實作。
  • 連字字元的預設處理方式不同。 在 NLS 下,連字及其非連字對應項目 (例如,"oeuf" 和 "œuf") 視為相等,但這與 .NET 中的 ICU 並不相同。 這是因為兩個實作之間的定序強度不同。 若要在使用 ICU 時還原 NLS 行為,請使用 CompareOptions.IgnoreNonSpace 值。

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 上的 ICU 這節表格中所列之 Windows 版本上執行的 .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+ 上比較字串時的行為變更

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

重要

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

True
True
True
False
False

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

TimeZoneInfo.FindSystemTimeZoneById

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

星期幾縮寫

DateTimeFormatInfo.GetShortestDayName(DayOfWeek) 方法會取得指定一週中指定日的最短縮寫日名稱。

  • 在 Windows 上的 .NET Core 3.1 和更早版本中,這些星期幾縮寫包含兩個字元,例如 "Su"。
  • 在 .NET 5 和更新版本中,這些星期幾縮寫只包含一個字元,例如 "S"。

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 二進位檔執行。 此外,NuGetPackageId 項目群組中的 RuntimeTargetsCopyLocalItems 中繼資料必須符合專案實際上參考的 NuGet 套件。

在 Linux 上載入特定的 ICU 版本

根據預設,在 Linux 上使用 ICU 時,.NET 會嘗試從系統載入最新安裝的 ICU 版本。 不過,您可以藉由設定 DOTNET_ICU_VERSION_OVERRIDE 環境變數,指定要載入的特定 ICU 版本。

例如,如果環境變數設定為特定版本號碼,例如 67.1,.NET 會嘗試載入該版本的ICU。 例如,.NET 會尋找程式庫 libicuuc.so.67.1libicui18n.so.67.1

注意

此環境變數僅在 Microsoft 提供的 .NET 組建上受到支援,而且 Linux 發行版提供的組建不支援此環境變數。 對於比 .NET 10 更早的 .NET 版本,環境變數稱為 CLR_ICU_VERSION_OVERRIDE

如果找不到指定的版本,.NET 會退回從系統載入已安裝的最高 ICU 版本。

此設定提供控制ICU版本使用方式的彈性,確保與應用程式特定或系統提供的ICU版本相容。

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 存放庫中找到支援的清單。

.NET 應用程式中的全球化設定

.NET 全球化初始化是一個複雜的程式,牽涉到載入適當的全球化連結庫、設定文化特性數據,以及設定全球化設定。 下列各節說明全球化初始化在不同平臺上的運作方式。

窗戶

在 Windows 上,.NET 遵循下列步驟來初始化全球化:

  • 檢查是否已啟用 全球化非變異模式。 當此模式為使用中時,.NET 會略過載入ICU函式庫,並避免使用NLS API。 相反地,它依賴內建的不變文化資料,確保行為完全獨立於操作系統和ICU函式庫。

  • 檢查是否已啟用 NLS 模式。 如果啟用,.NET 會略過載入 ICU 連結庫,而是依賴 Windows NLS API 進行全球化支援。

  • 請確認是否已啟用 應用程式的本地 ICU 功能。 如果是,.NET 會嘗試從應用程式目錄載入 ICU 連結庫,方法是將指定的版本附加至連結庫名稱。 例如,如果版本是 72.1,.NET 會先嘗試載入 icuuc72.dllicuin72.dllicudt72.dll。 如果無法載入這些程式庫,則會嘗試載入 icuuc72.1.dllicuin72.1.dllicudt72.1.dll。 如果找不到任何程式庫,程序將會終止,並出現錯誤訊息,例如:Failed to load app-local ICU: {library name}

  • 如果上述任何條件都不滿足,.NET 就會嘗試從系統目錄載入 ICU 程式庫。 它會先嘗試載入 icu.dll。 如果此函式庫無法使用,則會嘗試從系統目錄載入 icuuc.dllicuin.dll。 如果找不到這些函式庫,執行階段會回退為使用 NLS API 進行全球化支援。

注意

NLS API 一律可在所有 Windows 版本中使用,因此 .NET 一律可以回復它們以取得全球化支援。

Linux

  • 檢查是否已啟用 全球化非變異模式。 當此模式啟用時,.NET 會略過載入 ICU 程式庫。 相反地,它依賴內建的不變文化資料,確保行為完全獨立於操作系統和ICU函式庫。
  • 請確認是否已啟用 應用程式的本地 ICU 功能。 如果是,.NET 會嘗試從應用程式目錄載入 ICU 連結庫,方法是將指定的版本附加至連結庫名稱。 例如,如果版本是 68.2.0.9,.NET 會嘗試載入 libicuuc.so.68.2.0.9libicui18n.so.68.2.0.9。 如果找不到任何程式庫,程序將會終止,並出現錯誤訊息,例如:Failed to load app-local ICU: {library name}
  • 檢查是否已設定 DOTNET_ICU_VERSION_OVERRIDE 環境變數。 如果是,.NET 將嘗試根據「在 Linux 上載入特定 ICU 版本 」中的描述,載入指定的 ICU 版本
  • 如果上述條件皆不滿足,.NET 會嘗試從系統中載入已安裝的最高版本 ICU 函式庫。 它會嘗試載入程式庫 libicuuc.so.[version]libicui18n.so.[version],其中 [version] 是系統上已安裝的最高 ICU 版本。 如果找不到連結庫,行程就會終止並出現錯誤訊息,例如:Failed to load system ICU: {library name}

macOS

  • 檢查是否已啟用 全球化非變異模式。 當此模式啟用時,.NET 會略過載入 ICU 程式庫。 相反地,它依賴內建的不變文化資料,確保行為完全獨立於操作系統和ICU函式庫。
  • 請確認是否已啟用 應用程式的本地 ICU 功能。 如果是,.NET 會嘗試從應用程式目錄載入 ICU 連結庫,方法是將指定的版本附加至連結庫名稱。 例如,如果版本是 68.2.0.9,.NET 會嘗試載入 libicuuc68.2.0.9.dyliblibicui18n68.2.0.9.dylib。 如果找不到任何函式庫,過程就會終止,並出現錯誤訊息,例如:Failed to load app-local ICU: {library name}
  • 如果未滿足上述任何條件,.NET 會嘗試載入已安裝的 ICU 連結庫版本,如 macOS 行為 所述,