.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,CurrentCulture
、CurrentUICulture
和 CurrentRegion
的成員仍會使用 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 的區段表格中),程式碼片段會列印
0
、0
和3
(用於循序搜尋)。
根據預設,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'));
若要避免此行為,請使用 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'));
若要避免此行為,請使用 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
的值設為true
或1
。
注意
專案或 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 可以嘗試用 libicudata
、libicuuc
和 libicui18n
(依此順序) 來滿足 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:
- CultureInfo.EnglishName
- CultureInfo.NativeName
- DateTimeFormatInfo.NativeCalendarName
- RegionInfo.NativeName
支援下列 API,但有限制:
- String.Normalize(NormalizationForm) 和 String.IsNormalized(NormalizationForm) 不支援不常用的 FormKC 和 FormKD 格式。
- RegionInfo.CurrencyNativeName 會傳回與 RegionInfo.CurrencyEnglishName 相同的值。
此外,支援的地區設定也比較少。 您可以在 dotnet/icu 存放庫中找到支援的清單。
意見反應
https://aka.ms/ContentUserFeedback。
即將登場:在 2024 年,我們將逐步淘汰 GitHub 問題作為內容的意見反應機制,並將它取代為新的意見反應系統。 如需詳細資訊,請參閱:提交並檢視相關的意見反應