首页/开发/正则与编码速查/UTF-16 代理对与增补平面

UTF-16 代理对与增补平面

UTF-16 的基本单元是 16 位码元,BMP 之外的字符要用一对高位代理(D800-DBFF)与低位代理(DC00-DFFF)组合表示;字符串长度按码元计数时,一个 emoji 会被算成 2,这是长度与截断 bug 的常见来源。

正则与编码速查开发更新于 2026-10-17
​‌‌​​‌​​​‌‌​‌‌‌‌​‌‌​​​‌‌​‌‌‌​​‌‌​​‌​‌‌‌​​‌‌‌​​​‌​‌‌​‌​​‌​‌‌‌​​​‌​‌‌​‌​​‌​‌‌​​​‌‌​‌‌‌​‌​​​‌‌​‌‌‌‌​​‌​‌‌‌​​‌‌​​​‌‌​‌‌​‌‌‌‌​‌‌​‌‌​‌​​‌​‌‌‌‌​‌‌‌​​‌​​‌‌​​‌​‌​‌‌​​‌‌‌​‌‌​​‌​‌​‌‌‌‌​​​​​‌​‌‌‌‌​‌‌‌​‌​‌​‌‌‌​‌​​​‌‌​​‌‌​​​‌‌​​​‌​​‌‌​‌‌​​​‌​‌‌​‌​‌‌‌​​‌‌​‌‌‌​‌​‌​‌‌‌​​‌​​‌‌‌​​‌​​‌‌​‌‌‌‌​‌‌​​‌‌‌​‌‌​​​​‌​‌‌‌​‌​​​‌‌​​‌​‌​​‌​‌‌​‌​‌‌‌​​​​​‌‌​​​​‌​‌‌​‌​​‌​‌‌‌​​‌​​​‌​‌‌‌​​‌‌​‌​​​​‌‌‌​‌​​​‌‌​‌‌​‌​‌‌​‌‌​​

代理对的来历与算法

UTF-16 的编码单位是 16 位,只能表达 0-0xFFFF 共 65536 个值,而 Unicode 码位要到 0x10FFFF。解决办法是把 U+10000 以上的码位减去 0x10000 得到 20 位值,前 10 位加到 0xD800 形成高位代理,后 10 位加到 0xDC00 形成低位代理,两个码元合起来表示一个字符。因此代理区 D800-DFFF 本身不是合法字符,单独出现即非法。字符串按码元计数时,U+10000 以上字符会被算作长度 2。

基本信息

编码单位16 位码元(code unit)
高位代理区间U+D800-U+DBFF
低位代理区间U+DC00-U+DFFF
覆盖范围基本平面 1 个码元,增补平面 2 个码元
典型语言Java 的 char / String、.NET 的 char、JavaScript 字符串
长度陷阱按码元计的长度会把一个增补平面字符算成 2

典型使用场景

  • emoji 相关逻辑:判断字符串里是否含 emoji、做长度限制与截断时必须按码点或字形簇而非码元
  • Java/.NET 字符串遍历:用 char 循环会拆散代理对,需要用码点遍历接口
  • 前后端交互:JavaScript 侧按 length 计算的长度与后端按码点计算的口径不一致,容易触发校验失败
  • 数据校验:检测孤立代理者(lone surrogate),它是损坏数据的典型形态
  • 跨编码转换:UTF-16 与 UTF-8 互转时代理对必须成对处理,否则产出非法序列

代理对的观察与处理

代理对的观察与处理
python -c "s='\U0001F600'; print(len(s), [hex(ord(c)) for c in s])"   # 单个码点,Python3 按码点计数
python -c "print([hex(ord(c)) for c in '\U0001F600'.encode('utf-16-le').decode('utf-16-le')])"
# Java:按码元遍历会拆开代理对,正确做法是按码点遍历
# for (int i = 0; i < s.length(); ) { int cp = s.codePointAt(i); i += Character.charCount(cp); }
python -c "print('\U0001F600'.encode('utf-16-le'))"   # 小端下 4 字节:3d d8 00 de
python -c "s='\ud83d'; print(repr(s.encode('utf-8','surrogatepass')))"  # 孤立代理者在严格模式下无法编码

码元与码点口径对照

口径emoji 的计数说明
字节数(UTF-8)4存储与带宽估算用
码元数(UTF-16)2Java length、JS length 的结果
码点数1Python3 的 len,最接近字符语义
字形簇数1 或更多带肤色、ZWJ 组合的 emoji 可能由多个码点组成
孤立代理者(lone surrogate)是数据损坏的信号:它常出现在按 UTF-16 码元截断字符串、或用错误编码往返转换之后。严格编码器(如 UTF-8 严格模式)会直接拒绝,宽松实现会替换成 U+FFFD,两者行为差异会掩盖问题,排查时应先用严格模式验证。
长度校验是代理对最容易出问题的地方:前端按 JS 的 length 限制 10 个字符,后端按 Python 的 len 也限制 10,同一个含 emoji 的输入可能一边通过一边失败。接口文档应写明按字节、码点还是字形簇计数,并在两端使用同一口径。
代理区码位不会被直接分配给字符(U+D800-U+DFFF 永远保留),所以任何声称「这个字符就是 U+D83D」的数据都是损坏或人为构造的,可用于校验过滤与安全检测。

易错点

  • 按码元截断字符串:截出半个代理对,产生非法序列,后续编码或写库直接报错 → 按码点边界截断,或截断后校验是否留下孤立代理
  • 用 char 循环遍历含 emoji 的字符串:代理对被拆成两个无意义字符,长度与内容都错 → 使用码点遍历接口
  • 前后端长度口径不同:同一个输入一边超长一边通过,出现只在特定用户处复现的校验失败 → 统一口径并在接口层做同一套校验
  • 把代理对当两个字符做替换或屏蔽:替换其中一半,破坏字节序列 → 按码点处理匹配与替换
  • 宽松解码掩盖损坏数据:孤立代理者被替换为 U+FFFD,上线后才在下游暴露 → 数据管道开启严格校验并在入湖前拦截

数据来源与参考

  • Unicode 官方:UTF-16 与代理对(Surrogates)章节
  • RFC 2781:UTF-16 编码定义
  • Oracle Java 文档:Character 类的码点相关接口

整理校对:其其网ICT文档中心 · 本页地址:docs.qiqicto.com/regex/utf16-surrogate-pair.html

​‌‌​​‌​​​‌‌​‌‌‌‌​‌‌​​​‌‌​‌‌‌​​‌‌​​‌​‌‌‌​​‌‌‌​​​‌​‌‌​‌​​‌​‌‌‌​​​‌​‌‌​‌​​‌​‌‌​​​‌‌​‌‌‌​‌​​​‌‌​‌‌‌‌​​‌​‌‌‌​​‌‌​​​‌‌​‌‌​‌‌‌‌​‌‌​‌‌​‌​​‌​‌‌‌‌​‌‌‌​​‌​​‌‌​​‌​‌​‌‌​​‌‌‌​‌‌​​‌​‌​‌‌‌‌​​​​​‌​‌‌‌‌​‌‌‌​‌​‌​‌‌‌​‌​​​‌‌​​‌‌​​​‌‌​​​‌​​‌‌​‌‌​​​‌​‌‌​‌​‌‌‌​​‌‌​‌‌‌​‌​‌​‌‌‌​​‌​​‌‌‌​​‌​​‌‌​‌‌‌‌​‌‌​​‌‌‌​‌‌​​​​‌​‌‌‌​‌​​​‌‌​​‌​‌​​‌​‌‌​‌​‌‌‌​​​​​‌‌​​​​‌​‌‌​‌​​‌​‌‌‌​​‌​​​‌​‌‌‌​​‌‌​‌​​​​‌‌‌​‌​​​‌‌​‌‌​‌​‌‌​‌‌​​