.NET Hybrid Globalization 混合模式解析:Apple 移动平台上的平台原生全球化实现
【免费下载链接】runtime.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.项目地址: https://gitcode.com/GitHub_Trending/runtime6/runtime
导读
本文基于 .NET 运行时仓库(dotnet/runtime)中的 Hybrid Globalization 设计文档,系统讲解.NET Hybrid Globalization(混合全球化)模式:在 iOS/tvOS/MacCatalyst 等 Apple 移动平台上,运行时如何优先调用平台原生国际化 API、仅对剩余 ICU 操作链接系统icucore库,而不打包 App-Local ICU 数据文件。你将掌握 Hybrid 模式的启用机制、与 ICU 模式的逐项行为差异(字符串比较、前缀/后缀匹配、索引查找、排序键、大小写转换、日历数据),以及这些差异背后的源码级实现与兼容性取舍。
Hybrid Globalization 是什么
HybridGlobalization模式的核心思想是:能使用平台原生国际化 API 的地方就使用平台原生 API,只有平台 API 无法覆盖的功能才回退到 ICU。在 Apple 移动平台(iOS/tvOS/MacCatalyst)上,Hybrid 模式默认始终处于激活状态,除非显式启用了 Invariant(不变量)模式。
从源码可以确认这一开关的硬编码行为。在 GlobalizationMode.cs 中,设置类对三个目标平台直接赋值Hybrid = true:
internal static bool Invariant { get; } = AppContextConfigHelper.GetBooleanConfig("System.Globalization.Invariant", "DOTNET_SYSTEM_GLOBALIZATION_INVARIANT"); #if TARGET_MACCATALYST || TARGET_IOS || TARGET_TVOS internal static bool Hybrid { get; } = true; #endif并且注意 GlobalizationMode.cs 中的说明:Invariant模式优先于 Hybrid——一旦InvariantGlobalization=true,全局化直接走不变量模式,Hybrid 相关逻辑不会生效。
HybridGlobalization 构建属性的真实作用
一个容易混淆的点是:HybridGlobalization构建属性在 Apple 移动平台上并不负责“开启”Hybrid 模式。该属性仅为了兼容 linker 和原生构建流程而被保留——它作为 MSBuild 参数被透传给AppleAppBuilderTask,见 AppleBuild.targets:
<AppleAppBuilderTask Runtime="$(AppleAppBuilderRuntime)" ... HybridGlobalization="$(HybridGlobalization)" InvariantGlobalization="$(InvariantGlobalization)" ...>也就是说:在这些平台上 Hybrid 永远生效,HybridGlobalization属性既不会开启 Hybrid,也不会带来 App-Local ICU 数据(icudt*.dat)。真正决定行为的是InvariantGlobalization属性以及运行时配置System.Globalization.Invariant/ 环境变量DOTNET_SYSTEM_GLOBALIZATION_INVARIANT。
ICU 数据与 icucore 的职责划分
在 Apple 移动平台上:
- 运行时优先使用 Apple 原生 API(Foundation / CoreFoundation);
- 剩余的 ICU 支持操作(例如IDN 映射)通过链接系统自带的
icucore库完成; - 不会随应用打包或加载 App-Local ICU 数据文件(
icudt*.dat)。
这一点在 ICU 加载路径中也可以佐证:GlobalizationMode.LoadICU.iOS.cs 中LoadICU()只是把ICU_DAT_FILE_PATH(可能为 null)交给原生层处理,苹果平台的默认路径并不存在 App-Local 数据文件。
与 ICU 模式的行为差异总览
因为原生 API 并不能完全覆盖目前 ICU 支持的全部全球化功能,Hybrid 模式下的行为会与 ICU 平台存在差异,部分功能甚至不受支持。差异主要集中在以下六类 API 上:
| 功能域 | 受影响的主要公开 API | Apple 原生映射 |
|---|---|---|
| 字符串比较 | CompareInfo.Compare、String.Compare、String.Equals | compare:options:range:locale: |
| 前缀/后缀 | CompareInfo.IsPrefix、IsSuffix、String.StartsWith、String.EndsWith | compare:options:range:locale: |
| 字符串索引 | CompareInfo.IndexOf、LastIndexOf、String.IndexOf、LastIndexOf | rangeOfString:options:range:locale: |
| 排序键 | CompareInfo.GetSortKey、GetSortKeyLength、GetHashCode | stringByFoldingWithOptions:locale: |
| 大小写转换 | TextInfo.ToLower、TextInfo.ToUpper | uppercaseString/lowercaseString系列 |
| 日历数据 | DateTimeFormatInfo大量模式/名称属性 | NSCalendar/NSDateFormatter数据源 |
字符串比较(String comparison)
受影响的公开 API:
CompareInfo.CompareString.CompareString.Equals
Hybrid 实现映射到 Apple 原生 APIcompare:options:range:locale:,其内部使用了诸如precomposedStringWithCanonicalMapping之类的规范化技术,这会导致与其他平台的行为差异——特别是预组合字符串(precomposed strings)与基于 locale 的额外字符串折叠(string folding)会直接影响比较结果。因此,Apple 平台上字符串比较的精确结果可能与其他平台不同。
CompareOptions与NSStringCompareOptions的可用组合数量有限。CompareOptions的原始定义见 .NET 文档System.Globalization.CompareOptions,NSStringCompareOptions则来自 Apple Foundation 文档。
IgnoreSymbols(忽略符号)
IgnoreSymbols通过在托管侧先过滤掉可忽略符号,再调用原生 API 来实现。源码 CompareInfo.iOS.cs 中的CompareStringNative展示了完整流程:先通过SymbolFilteringBuffer.TryFilterString过滤字符串,再移除IgnoreSymbols标志后调用Interop.Globalization.CompareStringNative。其中IsIgnorableSymbol(CompareInfo.iOS.cs)定义了哪些 Unicode 类别会被过滤:所有分隔符类别(SpaceSeparator/LineSeparator/ParagraphSeparator)、所有标点类别(ConnectorPunctuation 到 OtherPunctuation)、所有符号类别(MathSymbol 到 ModifierSymbol),以及空白类控制字符(制表符、换行、回车等)。过滤时若栈缓冲区不够,会回退到ArrayPool<char>.Shared堆分配(阈值StackAllocThreshold = 150)。
IgnoreKanaType(忽略假名类型)
IgnoreKanaType使用kCFStringTransformHiraganaKatakana转换(平假名 ↔ 片假名)后再进行比较。
None(默认比较)
CompareOptions.None映射为NSStringCompareOptions.NSLiteralSearch(字面搜索)。存在行为变化,例如平假名与片假名字符的排序顺序与 ICU 不同。文档给出了如下实测对照表(hybrid 为 Apple 平台结果,icu 为 ICU 平台结果,1 表示字符1 > 字符2,-1 表示字符1 < 字符2):
| 字符 1 | 字符 2 | CompareOptions | hybrid globalization | icu | 说明 |
|---|---|---|---|---|---|
\u3042あ | \u30A1ァ | None | 1 | -1 | 平假名与片假名字符的排序与 ICU 不同 |
\u304D\u3083きゃ | \u30AD\u30E3キャ | None | 1 | -1 | 平假名与片假名字符的排序与 ICU 不同 |
\u304D\u3083きゃ | \u30AD\u3083キゃ | None | 1 | -1 | 平假名与片假名字符的排序与 ICU 不同 |
\u3070\u3073\uFF8C\uFF9E\uFF8D\uFF9E\u307Cばびブベぼ | \u30D0\u30D3\u3076\u30D9\uFF8E\uFF9Eバビぶベボ | None | 1 | -1 | 平假名与片假名字符的排序与 ICU 不同 |
\u3060だ | \u30C0ダ | None | 1 | -1 | 平假名与片假名字符的排序与 ICU 不同 |
StringSort(字符串排序)
CompareOptions.StringSort映射为NSStringCompareOptions.NSLiteralSearch。ICU 的默认行为就是使用 "StringSort"——即非字母数字符号排在字母数字之前,NSLiteralSearch的行为与此一致。
IgnoreCase(忽略大小写)
CompareOptions.IgnoreCase映射为NSStringCompareOptions.NSCaseInsensitiveSearch | NSStringCompareOptions.NSLiteralSearch。也存在行为差异:
| 字符 1 | 字符 2 | CompareOptions | hybrid globalization | icu | 说明 |
|---|---|---|---|---|---|
\u3060だ | \u30C0ダ | IgnoreCase | 1 | -1 | 平假名与片假名字符的排序与 ICU 不同 |
IgnoreNonSpace(忽略非空格组合符号)
CompareOptions.IgnoreNonSpace映射为NSStringCompareOptions.NSDiacriticInsensitiveSearch | NSStringCompareOptions.NSLiteralSearch。
IgnoreWidth(忽略全半角宽度)
CompareOptions.IgnoreWidth映射为NSStringCompareOptions.NSWidthInsensitiveSearch | NSStringCompareOptions.NSLiteralSearch。
不受支持的 CompareOptions
托管层对可用的比较选项做了白名单校验。在 CompareInfo.iOS.cs 中:
private const CompareOptions SupportedCompareOptions = CompareOptions.None | CompareOptions.IgnoreCase | CompareOptions.IgnoreNonSpace | CompareOptions.IgnoreWidth | CompareOptions.StringSort | CompareOptions.IgnoreKanaType | CompareOptions.IgnoreSymbols; private static void AssertComparisonSupported(CompareOptions options) { if ((options | SupportedCompareOptions) != SupportedCompareOptions) throw new PlatformNotSupportedException(GetPNSE(options)); }即CompareOptions中未被列入白名单的组合(如IgnoreCase | Ordinal等混合用法)会抛出PlatformNotSupportedException,异常信息为PlatformNotSupported_HybridGlobalizationWithCompareOptions。
字符串前缀/后缀匹配(Starts with / Ends with)
受影响的公开 API:
CompareInfo.IsPrefixCompareInfo.IsSuffixString.StartsWithString.EndsWith
实现同样映射到compare:options:range:locale:。由于 Apple 原生 API 没有暴露 locale 敏感的 endsWith/startsWith 函数,托管层采用如下变通方案:
- 对两个字符串都做规范化(normalize);
- 移除无权重(weightless)字符;
- 将结果字符串裁剪到相同长度;
- 执行比较。
由于为了裁剪而对字符串做了规范化,无法在原始字符串上计算匹配长度(match length)。因此,凡是需要计算并返回匹配长度的方法都会抛出PlatformNotSupportedException:
CompareInfo.IsPrefixCompareInfo.IsSuffix
IgnoreSymbols的处理方式与字符串比较一致:在托管侧先用SymbolFilteringBuffer过滤掉可忽略符号,再交给原生 API 比较(见 CompareInfo.iOS.cs 中NativeStartsWith/NativeEndsWith的实现)。
字符串索引查找(String indexing)
受影响的公开 API:
CompareInfo.IndexOfCompareInfo.LastIndexOfString.IndexOfString.LastIndexOf
同样地,计算matchLength的重载会抛出PlatformNotSupportedException,包括:
CompareInfo.IndexOf(ReadOnlySpan<char>, ReadOnlySpan<char>, CompareOptions, out int)CompareInfo.LastIndexOf(ReadOnlySpan<char>, ReadOnlySpan<char>, CompareOptions, out int)
实现映射到 Apple 原生 APIrangeOfString:options:range:locale:。该 API 通过检查码点序列的Unicode 规范等价性(canonical equivalence)来比较对象。当搜索字符串包含组合字符(diacritics)且与源字符串的规范化形式不同时,结果可能不正确。
规范化形式的背景
- 字符通常由 Unicode 码点表示,某些字符既可以表示为单个码点,也可以由多个字符组合而成(如组合附加符 diacritics / 分音符 diaeresis)。
- Normalization Form C(NFC):把原本以多个码点序列表示的字符压缩为单个码点形式。
- Normalization Form D(NFD):反过来,把字符尽量展开为多个码点形式。
NSString的rangeOfString:options:range:locale:使用规范等价性在源字符串中定位搜索字符串,但它不会自动处理预组合(precomposed,单码点表示)与分解(decomposed,多码点表示)的差异。由于searchString与sourceString可能采用不同形式,为了正确找到索引,需要尝试每一种规范化形式调用rangeOfString:options:range:locale:,确保 searchString 与 sourceString 处于相同形式。
已覆盖的带组合符场景
- 搜索字符串包含组合符,且与源字符串的规范化形式相同。
- 搜索字符串包含组合符,与源字符串是相同字母但字符长度不同,且子串在源字符串中已规范化:
- a. 搜索字符串规范化为 Form C后是源字符串的子串。例:搜索串
U\u0308,源串Source is \u00DC⇒ matchLength 为 1。 - b. 搜索字符串规范化为 Form D后是源字符串的子串。例:搜索串
\u00FC,源串Source is \u0075\u0308⇒ matchLength 为 2。
- a. 搜索字符串规范化为 Form C后是源字符串的子串。例:搜索串
未覆盖的混合组合形式场景
源字符串中目标匹配子串包含混合组合形式的字符时,无法通过上述第 2 种方式匹配,因为实现不做部分预组合/分解。例:搜索串U\u0308 and \u00FC(Ü 和 ü),源串Source is \u00DC and \u0075\u0308(Source is Ü 和 ü)。从例子可见,把搜索串规范化为 Form C 或 D 都无法在源串中找到该子串。
这一限制在托管层以明确的错误码体现:IndexOfCoreNative(CompareInfo.iOS.cs)在原生层返回ERROR_MIXED_COMPOSITION_NOT_FOUND (-3)时,会抛出PlatformNotSupportedException(资源消息PlatformNotSupported_HybridGlobalizationWithMixedCompositions)。而ERROR_INDEX_NOT_FOUND (-1)代表正常的“未找到”结果。
多字素(grapheme)字母问题
Apple 原生 API 不保证按“字母(letter)”而是按“字素(grapheme)”切分字符串。例如在cs-CZ与sk-SK文化中,"ch"是一个字母、但由 2 个字素组成。以下代码在 ICU 平台上返回 -1(未找到),在 Apple 移动平台上返回 1:
new CultureInfo("sk-SK").CompareInfo.IndexOf("ch", "h"); // -1 或 1多字素等价字符问题
某些字素存在多字素等价形式。例如de-DE文化中,ß(\u00DF)是一个字母、一个字素,而"ss"是一个字母、被识别为两个字素。Apple 原生 API 中IgnoreNonSpace的等价操作会把二者视为同一字母;类似的例子还有 dz(\u01F3)与dz。
使用IgnoreNonSpace比较这两组字符时,ICU 平台也返回 0(相等);但 Apple 移动实现按字素逐个比较,返回 -1:
new CultureInfo("de-DE").CompareInfo.IndexOf("strasse", "stra\u00DFe", 0, CompareOptions.IgnoreNonSpace); // 0 或 -1排序键(SortKey)
受影响的公开 API:
CompareInfo.GetSortKeyCompareInfo.GetSortKeyLengthCompareInfo.GetHashCode
排序键使用 Apple 原生 APIstringByFoldingWithOptions:locale:实现。
⚠️重要注意:此实现并不会像 ICU 的ucol_getSortKey那样构造真正的 SortKey,因此可能不满足 SortKey 的规范要求,例如:
- 不同 collator(排序器)生成的 SortKey 之间不可比较;
- SortKey 的合并(merging)语义可能不被支持。
大小写转换(Case change)
受影响的公开 API:
TextInfo.ToLowerTextInfo.ToUpper
使用以下 Apple 原生函数:
uppercaseStringlowercaseStringuppercaseStringWithLocalelowercaseStringWithLocale
源码层面,TextInfo.iOS.cs 中的ChangeCaseNative会先断言GlobalizationMode.Hybrid为真,然后根据是否有文化名选择调用ChangeCaseInvariantNative(空文化名)或ChangeCaseNative(带文化名),并通过ResultCode区分失败原因(InvalidCodePoint、InsufficientBuffer等)。注意大小写转换的输入输出是原始 UTF-16 缓冲区(char* src/char* dstBuffer),涉及缓冲区容量管理。
日历数据(Calendars)
受影响的公开 API(均为DateTimeFormatInfo成员):
AbbreviatedDayNames/GetAbbreviatedDayName()AbbreviatedMonthGenitiveNamesAbbreviatedMonthNames/GetAbbreviatedMonthName()AMDesignatorCalendarWeekRuleDayNames/GetDayName()GetEraName()FirstDayOfWeekFullDateTimePatternLongDatePatternLongTimePatternMonthDayPatternMonthGenitiveNamesMonthNames/GetMonthName()NativeCalendarNamePMDesignatorShortDatePatternShortestDayNames/GetShortestDayName()ShortTimePatternYearMonthPattern
日历数据在 Hybrid 模式下由 Apple 原生日历/格式化 API 提供。源码 CalendarData.iOS.cs 展示了数据加载路径:LoadCalendarDataFromNative通过GetCalendarInfoNative获取日历本地名称(NativeName)与 MonthDay 模式,通过EnumDatePatterns枚举短日期/长日期/年月模式,通过EnumCalendarInfo/EnumMonthNames枚举天名、缩写天名、最短天名与月份名(包括希伯来历闰月 Adar II 的覆盖逻辑)。
已知限制:Apple 原生 API 没有与“缩写纪元名(abbreviated era name)”等价的功能,因此以下方法会返回空字符串:
DateTimeFormatInfo.GetAbbreviatedEraName()
平台行为差异速查
| 功能 | Hybrid(Apple 移动平台) | ICU(其他平台) |
|---|---|---|
| ICU 数据文件 | 不打包/不加载icudt*.dat,链接系统icucore | 使用 App-Local 或系统 ICU 数据 |
CompareOptions.None的平假名/片假名排序 | 平假名排在片假名之后(示例返回 1) | 平假名排在片假名之前(示例返回 -1) |
| 前缀/后缀匹配长度 | 抛出PlatformNotSupportedException | 正常返回 |
索引匹配长度(out int matchLength重载) | 抛出PlatformNotSupportedException | 正常返回 |
| 混合组合形式的子串查找 | 抛出PlatformNotSupportedException | 可匹配 |
| SortKey 语义 | 不保证跨 collator 可比、不支持合并 | 遵循 ICUucol_getSortKey规范 |
GetAbbreviatedEraName() | 返回空字符串 | 返回正常缩写纪元名 |
总结与适用建议
- 何时使用 Hybrid:在 iOS/tvOS/MacCatalyst 上这是默认且唯一的非 Invariant 行为,无需(也无法通过
HybridGlobalization属性)显式开启;它最大的优势是无需随应用携带 ICU 数据文件,从而减小体积并优先利用平台原生能力。 - 何时考虑 Invariant:当应用不依赖文化敏感的全球化行为时,可设置
InvariantGlobalization=true以获得最小化行为;Invariant 优先级高于 Hybrid。 - 兼容性审查:如果你的应用在 Apple 移动平台上依赖精确的字符串排序顺序(尤其涉及平假名/片假名混合文本)、
IgnoreNonSpace对 ß/ss 与 dz/dz 这类多字素等价字符的处理、matchLength重载或 SortKey 跨 collator 比较,需要针对 Hybrid 的行为差异做专门的测试与适配。 - 进一步阅读:行为差异的权威定义见本仓库的 globalization-hybrid-mode.md;底层实现的托管入口集中在 src/libraries/System.Private.CoreLib/src/System/Globalization 目录下的
*.iOS.cs文件(如 CompareInfo.iOS.cs、TextInfo.iOS.cs、CalendarData.iOS.cs、GlobalizationMode.cs),构建集成见 AppleBuild.targets。
【免费下载链接】runtime.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.项目地址: https://gitcode.com/GitHub_Trending/runtime6/runtime
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考