.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,CurrentCulture
、CurrentUICulture
和 CurrentRegion
的成員仍會使用 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 和更新版本,程式碼片段會輸出
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.Ordinal
。
TimeZoneInfo.FindSystemTimeZoneById
即使應用程式在 Windows 上執行,ICU 也能彈性地使用 IANA 時區 ID 建立 TimeZoneInfo 執行個體。 同樣地,即使在 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
的值設為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.Runtime PackageReference
新增至對應到已設定之 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 存放庫中找到支援的清單。