.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 trueinternal 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——一旦InvariantGlobalizationtrue全局化直接走不变量模式Hybrid 相关逻辑不会生效。HybridGlobalization 构建属性的真实作用一个容易混淆的点是HybridGlobalization构建属性在 Apple 移动平台上并不负责“开启”Hybrid 模式。该属性仅为了兼容 linker 和原生构建流程而被保留——它作为 MSBuild 参数被透传给AppleAppBuilderTask见 AppleBuild.targetsAppleAppBuilderTask 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 原生 APIFoundation / 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 上功能域受影响的主要公开 APIApple 原生映射字符串比较CompareInfo.Compare、String.Compare、String.Equalscompare:options:range:locale:前缀/后缀CompareInfo.IsPrefix、IsSuffix、String.StartsWith、String.EndsWithcompare:options:range:locale:字符串索引CompareInfo.IndexOf、LastIndexOf、String.IndexOf、LastIndexOfrangeOfString:options:range:locale:排序键CompareInfo.GetSortKey、GetSortKeyLength、GetHashCodestringByFoldingWithOptions:locale:大小写转换TextInfo.ToLower、TextInfo.ToUpperuppercaseString/lowercaseString系列日历数据DateTimeFormatInfo大量模式/名称属性NSCalendar/NSDateFormatter数据源字符串比较String comparison受影响的公开 APICompareInfo.CompareString.CompareString.EqualsHybrid 实现映射到 Apple 原生 APIcompare:options:range:locale:其内部使用了诸如precomposedStringWithCanonicalMapping之类的规范化技术这会导致与其他平台的行为差异——特别是预组合字符串precomposed strings与基于 locale 的额外字符串折叠string folding会直接影响比较结果。因此Apple 平台上字符串比较的精确结果可能与其他平台不同。CompareOptions与NSStringCompareOptions的可用组合数量有限。CompareOptions的原始定义见 .NET 文档System.Globalization.CompareOptionsNSStringCompareOptions则来自 Apple Foundation 文档。IgnoreSymbols忽略符号IgnoreSymbols通过在托管侧先过滤掉可忽略符号再调用原生 API 来实现。源码 CompareInfo.iOS.cs 中的CompareStringNative展示了完整流程先通过SymbolFilteringBuffer.TryFilterString过滤字符串再移除IgnoreSymbols标志后调用Interop.Globalization.CompareStringNative。其中IsIgnorableSymbolCompareInfo.iOS.cs定义了哪些 Unicode 类别会被过滤所有分隔符类别SpaceSeparator/LineSeparator/ParagraphSeparator、所有标点类别ConnectorPunctuation 到 OtherPunctuation、所有符号类别MathSymbol 到 ModifierSymbol以及空白类控制字符制表符、换行、回车等。过滤时若栈缓冲区不够会回退到ArrayPoolchar.Shared堆分配阈值StackAllocThreshold 150。IgnoreKanaType忽略假名类型IgnoreKanaType使用kCFStringTransformHiraganaKatakana转换平假名 ↔ 片假名后再进行比较。None默认比较CompareOptions.None映射为NSStringCompareOptions.NSLiteralSearch字面搜索。存在行为变化例如平假名与片假名字符的排序顺序与 ICU 不同。文档给出了如下实测对照表hybrid 为 Apple 平台结果icu 为 ICU 平台结果1 表示字符1 字符2-1 表示字符1 字符2字符 1字符 2CompareOptionshybrid globalizationicu说明\u3042あ\u30A1ァNone1-1平假名与片假名字符的排序与 ICU 不同\u304D\u3083きゃ\u30AD\u30E3キャNone1-1平假名与片假名字符的排序与 ICU 不同\u304D\u3083きゃ\u30AD\u3083キゃNone1-1平假名与片假名字符的排序与 ICU 不同\u3070\u3073\uFF8C\uFF9E\uFF8D\uFF9E\u307Cばびぼ\u30D0\u30D3\u3076\u30D9\uFF8E\uFF9EバビぶベNone1-1平假名与片假名字符的排序与 ICU 不同\u3060だ\u30C0ダNone1-1平假名与片假名字符的排序与 ICU 不同StringSort字符串排序CompareOptions.StringSort映射为NSStringCompareOptions.NSLiteralSearch。ICU 的默认行为就是使用 StringSort——即非字母数字符号排在字母数字之前NSLiteralSearch的行为与此一致。IgnoreCase忽略大小写CompareOptions.IgnoreCase映射为NSStringCompareOptions.NSCaseInsensitiveSearch | NSStringCompareOptions.NSLiteralSearch。也存在行为差异字符 1字符 2CompareOptionshybrid globalizationicu说明\u3060だ\u30C0ダIgnoreCase1-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受影响的公开 APICompareInfo.IsPrefixCompareInfo.IsSuffixString.StartsWithString.EndsWith实现同样映射到compare:options:range:locale:。由于 Apple 原生 API 没有暴露 locale 敏感的 endsWith/startsWith 函数托管层采用如下变通方案对两个字符串都做规范化normalize移除无权重weightless字符将结果字符串裁剪到相同长度执行比较。由于为了裁剪而对字符串做了规范化无法在原始字符串上计算匹配长度match length。因此凡是需要计算并返回匹配长度的方法都会抛出PlatformNotSupportedExceptionCompareInfo.IsPrefixCompareInfo.IsSuffixIgnoreSymbols的处理方式与字符串比较一致在托管侧先用SymbolFilteringBuffer过滤掉可忽略符号再交给原生 API 比较见 CompareInfo.iOS.cs 中NativeStartsWith/NativeEndsWith的实现。字符串索引查找String indexing受影响的公开 APICompareInfo.IndexOfCompareInfo.LastIndexOfString.IndexOfString.LastIndexOf同样地计算matchLength的重载会抛出PlatformNotSupportedException包括CompareInfo.IndexOf(ReadOnlySpanchar, ReadOnlySpanchar, CompareOptions, out int)CompareInfo.LastIndexOf(ReadOnlySpanchar, ReadOnlySpanchar, CompareOptions, out int)实现映射到 Apple 原生 APIrangeOfString:options:range:locale:。该 API 通过检查码点序列的Unicode 规范等价性canonical equivalence来比较对象。当搜索字符串包含组合字符diacritics且与源字符串的规范化形式不同时结果可能不正确。规范化形式的背景字符通常由 Unicode 码点表示某些字符既可以表示为单个码点也可以由多个字符组合而成如组合附加符 diacritics / 分音符 diaeresis。Normalization Form CNFC把原本以多个码点序列表示的字符压缩为单个码点形式。Normalization Form DNFD反过来把字符尽量展开为多个码点形式。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。未覆盖的混合组合形式场景源字符串中目标匹配子串包含混合组合形式的字符时无法通过上述第 2 种方式匹配因为实现不做部分预组合/分解。例搜索串U\u0308 and \u00FCÜ 和 ü源串Source is \u00DC and \u0075\u0308Source is Ü 和 ü。从例子可见把搜索串规范化为 Form C 或 D 都无法在源串中找到该子串。这一限制在托管层以明确的错误码体现IndexOfCoreNativeCompareInfo.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 移动平台上返回 1new CultureInfo(sk-SK).CompareInfo.IndexOf(ch, h); // -1 或 1多字素等价字符问题某些字素存在多字素等价形式。例如de-DE文化中ß\u00DF是一个字母、一个字素而ss是一个字母、被识别为两个字素。Apple 原生 API 中IgnoreNonSpace的等价操作会把二者视为同一字母类似的例子还有 dz\u01F3与dz。使用IgnoreNonSpace比较这两组字符时ICU 平台也返回 0相等但 Apple 移动实现按字素逐个比较返回 -1new CultureInfo(de-DE).CompareInfo.IndexOf(strasse, stra\u00DFe, 0, CompareOptions.IgnoreNonSpace); // 0 或 -1排序键SortKey受影响的公开 APICompareInfo.GetSortKeyCompareInfo.GetSortKeyLengthCompareInfo.GetHashCode排序键使用 Apple 原生 APIstringByFoldingWithOptions:locale:实现。⚠️重要注意此实现并不会像 ICU 的ucol_getSortKey那样构造真正的 SortKey因此可能不满足 SortKey 的规范要求例如不同 collator排序器生成的 SortKey 之间不可比较SortKey 的合并merging语义可能不被支持。大小写转换Case change受影响的公开 APITextInfo.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()平台行为差异速查功能HybridApple 移动平台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当应用不依赖文化敏感的全球化行为时可设置InvariantGlobalizationtrue以获得最小化行为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),仅供参考