跳到主要内容

Kotlin 2.5 Unicode 代码点 API 实战:用 CodePoint 告别代理对(Surrogate Pair)的噩梦

· 阅读需 6 分钟
重庆 Kotlin User Group

你遇到过这种场景吗?统计一个 emoji 字符串的"字符数",结果数字对不上;或者遍历一个包含罕见汉字的文本,发现一个字被拆成了两个 Char。

来看一段"经典翻车"代码:

val text = "Hello 🌍"
println(text.length) // 7?还是 8?
println(text[6]) // 期望拿到 🌍,实际是一个代理对的高位
println(text[7]) // 这才是代理对的低位

Char 在 Kotlin 中表示一个 UTF-16 编码单元,而像 🌍(U+1F30D)这样的字符超出了 BMP(基本多文种平面),需要两个 Char(即代理对)来表示。直接按 Char 遍历或统计,就会把一个字符拆成两半。

旧方案:手动处理代理对​

在 Kotlin 2.5 之前,如果你需要正确处理代码点,只能借助 Java 的 Character 和 String API:

val text = "Hello 🌍"

// 统计代码点数量
val codePointCount = text.codePointCount(0, text.length)
println(codePointCount) // 7 ✅

// 遍历代码点
val codePoints = text.codePoints().toArray()
for (cp in codePoints) {
print("${Character.toString(cp)} ")
}
// H e l l o 🌍 ✅

这套方案能用,但问题很明显:

  1. 不够 Kotlin —— 依赖 Java 的 Character 类和命令式 API
  2. 不支持多平台 —— codePointCount、codePoints() 都是 JVM 专属 API,在 Kotlin/Native、Kotlin/JS、Kotlin/Wasm 上不可用
  3. 类型不安全 —— 代码点用 Int 表示,跟普通整数混在一起,编译器无法区分

新方案:CodePoint 值类​

Kotlin 2.5.0-Beta2 在 kotlin.text.unicode 包中引入了 CodePoint 内联值类,从语言层面解决了上述问题。

CodePoint 是什么​

@JvmInline
value class CodePoint(val value: Int)

CodePoint 用 Int 作为底层类型,表示一个完整的 Unicode 代码点。它是一个值类(value class),运行时不会引入额外的对象开销,同时提供了类型安全——你不能再把一个普通 Int 误当代码点使用。

基本用法​

@OptIn(ExperimentalUnicodeApi::class)
fun main() {
val text = "Hello 🌍"

// 按代码点遍历
for (cp in text.codePoints()) {
print("$cp ")
}
// 输出: H e l l o 🌍

// 统计代码点数量
println(text.codePointCount()) // 7 ✅

// 创建 CodePoint
val earth = CodePoint(0x1F30D)
println(earth.isLetter()) // false
println(earth.isWhitespace()) // false
println(earth.category) // CodePoint.Category.OTHER_SYMBOL
}

实用 API 一览​

CodePoint 提供了丰富的属性和方法:

@OptIn(ExperimentalUnicodeApi::class)
fun main() {
val cp = CodePoint('A'.code)

// 基本属性
println(cp.isLetter()) // true
println(cp.isDigit()) // false
println(cp.isWhitespace()) // false
println(cp.isUpperCase()) // true
println(cp.category) // UPPERCASE_LETTER
println(cp.utf16CodeUnitCount) // 1(BMP 内的字符只需 1 个 Char)

// 大小写转换
println(cp.lowercase()) // a
println(cp.uppercase()) // A

// 比较和运算
val cp2 = CodePoint('B'.code)
println(cp < cp2) // true
println((cp2 - cp).value) // 1(CodePoint 支持算术运算)

// 范围
val range = cp..cp2
println('A' in range.map { CodePoint(it.code) }) // true
}

文本操作​

CodePoint 不仅能检查单个字符,还能直接在字符串上做文本操作:

@OptIn(ExperimentalUnicodeApi::class)
fun main() {
val text = "Kotlin 🚀 真棒"

// 在指定位置插入代码点
println(text.insertCodePoint(7, CodePoint('!'.code)))
// Kotlin !🚀 真棒

// 替换代码点
println(text.replaceCodePoint(7, CodePoint('🎉'.code)))
// Kotlin 🎉 真棒

// 删除代码点
println(text.deleteCodePoint(7))
// Kotlin 真棒

// 追加代码点
println(text.appendCodePoint(CodePoint('!'.code)))
// Kotlin 🚀 真棒!
}

实战场景​

场景一:正确统计用户昵称长度​

@OptIn(ExperimentalUnicodeApi::class)
fun validateNickname(nickname: String): Boolean {
// 按代码点统计,而不是 Char 数量
val length = nickname.codePointCount()
return length in 2..20
}

fun main() {
println(validateNickname("Alice")) // true(5 个代码点)
println(validateNickname("🌟Star🌟")) // true(6 个代码点)
// 如果用 .length 会得到 8,可能误判为超长
}

场景二:安全地反转字符串​

@OptIn(ExperimentalUnicodeApi::class)
fun String.reverseByCodePoints(): String {
return buildString {
for (cp in this@reverseByCodePoints.codePoints().toList().reversed()) {
appendCodePoint(cp)
}
}
}

fun main() {
val text = "abc🌍def"
println(text.reversed()) // ❌ fed🌍̊cba(代理对被拆散)
println(text.reverseByCodePoints()) // ✅ fed🌍cba
}

场景三:过滤 emoji 表情​

@OptIn(ExperimentalUnicodeApi::class)
fun String.stripEmoji(): String {
return buildString {
for (cp in this@stripEmoji.codePoints()) {
if (!cp.isEmoji()) {
appendCodePoint(cp)
}
}
}
}

fun main() {
val message = "Hello 🌍! Kotlin 🚀 is awesome 💯"
println(message.stripEmoji())
// Hello ! Kotlin is awesome
}

场景四:文本编辑器中的光标移动​

@OptIn(ExperimentalUnicodeApi::class)
fun moveCursorRight(text: String, currentCodePointIndex: Int): Int {
// 按代码点移动,而不是按 Char 移动
// 避免光标停在代理对中间
return currentCodePointIndex + 1
}

fun getVisibleCharCount(text: String): Int {
return text.codePointCount()
}

如何启用​

这些 API 目前还是实验性的,需要显式 opt-in:

方式一:注解标记​

@OptIn(ExperimentalUnicodeApi::class)
fun myFunction() {
val cp = CodePoint(0x1F600)
println(cp.isEmoji())
}

方式二:编译器参数(全项目启用)​

// build.gradle.kts
kotlin {
compilerOptions {
freeCompilerArgs.add("-opt-in=kotlin.text.unicode.ExperimentalUnicodeApi")
}
}

方式三:文件级标记​

@file:OptIn(ExperimentalUnicodeApi::class)
package com.example.text

// 文件中所有 CodePoint API 调用都不需要逐个标记

与 Java API 的对比​

功能Kotlin CodePointJava Character/String
类型安全✅ CodePoint 值类❌ 裸 Int
多平台支持✅ KMP 全平台❌ 仅 JVM
属性访问cp.isLetter()Character.isLetter(cp)
字符串操作text.appendCodePoint(cp)new StringCharacterIterator(...)
大小写转换cp.lowercase()Character.toLowerCase(cp)
Kotlin 惯用✅ 扩展函数风格❌ 静态方法调用

注意事项​

  1. 位置索引仍基于 UTF-16 编码单元 —— CodePoint API 让你按代码点思考和操作,但字符串的位置索引(如 substring 的 startIndex)仍然使用 UTF-16 编码单元偏移。这是为了与现有 API 保持兼容。

  2. 实验性标记 —— ExperimentalUnicodeApi 意味着 API 可能在后续版本中变化。生产环境使用时建议封装一层,方便后续迁移。

  3. 组合字符(Grapheme Cluster) —— CodePoint 处理的是 Unicode 代码点,不是用户感知的"字符"(grapheme cluster)。例如,带声调的字符 é 可能由 e(U+0065)和 ◌́(U+0301)两个代码点组合而成。如果需要按"视觉字符"处理,还需借助 BreakIterator 或 ICU4X 等库。

总结​

Kotlin 2.5 引入的 CodePoint API 填补了多平台 Unicode 处理的空白:

  1. CodePoint 值类 —— 类型安全、零开销地表示 Unicode 代码点
  2. 丰富的属性和方法 —— isLetter()、isEmoji()、lowercase() 等,Kotlin 风格
  3. 文本操作扩展 —— appendCodePoint、insertCodePoint、replaceCodePoint 等
  4. 多平台支持 —— 不再局限于 JVM,Native/JS/Wasm 都能用

如果你正在开发需要处理国际化文本、emoji、罕见汉字的应用,这套 API 值得关注。虽然还是实验性阶段,但方向已经很清晰——Kotlin 正在让 Unicode 处理变得像操作普通集合一样自然。