用途
本技能针对 Kotlin 中“一个显示字符不等于一个 Char”的问题。Kotlin 的 Char 被精确定义为“一个 16 位 Unicode 字符”,即一个 UTF-16 码元。但读者感知为单个字符的字母,可能需要两个码元:一个基字母加一个叠加渲染的组合标记。例如天城文的 क़(qa)由 क(KA)加组合 nukta 符号组成,是两个码点、两个 Char,因此 'क़' 根本无法作为字符字面量编译。
同样的限制也适用于基本多文种平面之外的任何单个码点:它在 UTF-16 中需要代理对表示,因此也是两个 Char 伪装成一个字形。
主要能力
技能指出若干具体陷阱:
- 以 Char 为键的映射无法为组合序列保存条目,因为字面量本身就无法编译。
- 印地语中的波斯-阿拉伯语借词发音写作基辅音加 nukta,源码不尝试用 'क़' 映射到 "q"(会被直接拒绝),而是把 nukta 保留为独立的 Char 常量,将其视为查找单独消费的修饰符。
- 映射中的每个键都是纯基辅音;标记通过窥视字符串中下一个索引来查找,从不作为键的一部分。标记单独作为字面量可以正常编译,只有基字母加标记的组合才无法被单个 Char 容纳。
技能还澄清:大多数带重音的字母是单个预组合 Char,本技能讨论的是组合序列,而非一般意义上的变音符号。越南语的 ế/à/ộ 等在其常规(NFC)形式下各是一个码点,U+1EBF 可放入一个 UTF-16 单元,因此 'ế' 可作为普通字面量编译。两个 Char 的限制特定于通常表示法将标记与基字母分开的文字,或来自外部源的预分解(NFD)文本——后者是运行时风险,因为没人会把分解序列写成源码字面量。
使用方式
kotlin-stdlib 的 commonMain 提供公开的 expect val Char.category: CharCategory,其值包括 NON_SPACING_MARK、COMBINING_SPACING_MARK 和 ENCLOSING_MARK,无需 ICU,无平台拆分,可用于纯 Kotlin Multiplatform 模块。通用的“此码点是否为组合标记”测试只需读取一个属性。但 category 无法提供字素簇分段(UAX #29:有多少标记附着到此基字母、一个显示字符在哪里结束)或“哪个标记、起什么作用”的语义——知道一个码点是标记,并不能说明它是改变辅音发音的 nukta 还是不改变发音的元音符号。因此脚本使用的标记被硬编码为常量:此处恰好四个(DEVANAGARI_VIRAMA、GURMUKHI_VIRAMA、DEVANAGARI_NUKTA、GURMUKHI_NUKTA——两个 virama、两个 nukta)。同样修饰辅音的元音符号不是常量,它们位于 DEVANAGARI_MATRAS/GURMUKHI_MATRAS 映射中,用 containsKey 识别,按相等性匹配。
修复方案是在 String 上按索引遍历的循环,绝不用 for (c in text) 或逐 Char 的映射查找。一旦某个脚本需要“在决定当前码元含义前窥视下一个码元”,整个例程就必须是手写的 while (index < text.length),按可变步长前进——无标记字母一步,后跟组合标记时两步。逐字符应用的 Map<Char, String> 无法前瞻,因此只能看到基字母,永远看不到改变它的标记。
任何其他将字符串拆分为单个 Char 逐个处理的地方都会继承同样的风险,不仅限于查找表。卡拉 OK 风格渲染器若通过 word.toCharArray() 逐字形着色或动画,并为每个条目创建一个文本节点,出于同样的底层原因,会把基字母与其组合标记拆成两个独立样式单元而非一个。标记自身没有宽度,因此不会大声失败;它只是渲染为不完整字母旁的散落重音,或与下一个字母重叠。失败模式不同于编译错误,但原因相同:把“一个 Char”当作“一个显示字符”的代理。
按索引进行字符串切片和截断也继承同样风险。text.take(n) 或 text.substring 在 Char 边界而非字素边界切割——在固定长度截断预览或省略号,同样可能落在基字母加标记对(或代理对)中间,静默丢弃标记,在截断输出中留下裸基字母。
适用场景
适用于处理天城文、古木基文等组合序列文字,或处理可能来自外部源的预分解文本的 Kotlin 项目,尤其是涉及字符映射、逐字符渲染、字符串截断的场景。
限制
技能未提供关于收费、价格、平台或效果的信息。验证命令涉及具体仓库路径和 Gradle 缓存,需在相应环境中运行。