# 字符串与 Unicode:字节、码点与字形
学习目标:理解 UTF-8 字节数、UTF-16 编码单元、Unicode 码点与字符截断边界,避免乱码与越界 Panic。
前置知识与范围:字符串类型与数组切片。聚焦内存中 UTF-8 与 UTF-16 字符串的度量与安全前缀截取。
# 问题与边界
以字符串 "Go语言👍" 为基准输入:
- 组成结构:包含 2 个 ASCII 字符(
'G','o')、2 个常见 CJK 汉字('语','言')和 1 个扩展平面 Emoji 符号('👍',码点 U+1F44D)。 - 度量指标:
- UTF-8 字节数:
1 + 1 + 3 + 3 + 4 = 12字节。 - UTF-16 编码单元数:ASCII 与汉字各占 1 个单元,Emoji 占用代理对(Surrogate Pair,2 个单元),总计
1 + 1 + 1 + 1 + 2 = 6个 code units。 - 实际 Unicode 码点数(Code points / Runes):共
5个码点。
- UTF-8 字节数:
- 操作契约:安全截取前 4 个 Unicode 码点,输出完整字符
"Go语言",绝不在汉字或 Emoji 的中间字节处截断。
# 关键差异
| 语言 | 字符串底层编码 | 默认 .length 或 len() |
码点计数机制 | 安全截取前 4 字符 |
|---|---|---|---|---|
| Rust | 保证合法的 UTF-8 字节流 | 字节数(12) | s.chars().count()(5) |
s.chars().take(4).collect() |
| Go | 任意字节切片,源码为 UTF-8 | 字节数(12) | utf8.RuneCountInString(s)(5) |
string([]rune(s)[:4]) |
| Java | UTF-16(内部压缩 Compact Strings) | UTF-16 单元数(6) | s.codePointCount(...)(5) |
码点偏移或 s.substring(0, 4) |
| JavaScript | UTF-16 序列 | UTF-16 单元数(6) | [...s].length 或正则(5) |
[...s].slice(0, 4).join("") |
| TypeScript | 沿用 JS | UTF-16 单元数(6) | 迭代器展开(5) | [...s].slice(0, 4).join("") |
| Kotlin | JVM UTF-16 | UTF-16 单元数(6) | s.codePointCount(...)(5) |
码点偏移或 s.take(4) |
| Python | 紧凑 Unicode(PEP 393 存储) | 码点数(5) | len(s) 直接是码点数(5) |
s[:4] 直接按码点安全切片 |
| C++ | std::string 为字节序列 |
字节数(12) | 解码 UTF-8 首字节头 | 扫描 UTF-8 前导位截断 |
# 各语言实现
# Rust
Rustfn main() {
let s = "Go语言👍";
// 字节长度
assert_eq!(s.len(), 12);
// 码点数量
assert_eq!(s.chars().count(), 5);
// 安全截取前 4 个码点
let truncated: String = s.chars().take(4).collect();
assert_eq!(truncated, "Go语言");
println!("bytes: {}, chars: {}, prefix: {truncated}", s.len(), s.chars().count());
}
Rust 的 &str 强制要求内容必须是合法的 UTF-8。直接进行字节索引切片(如 &s[0..3])若落在多字节中间会直接触发 panic,必须使用 chars() 迭代器。
# Go
Gopackage main
import (
"fmt"
"unicode/utf8"
)
func main() {
s := "Go语言👍"
// 字节数
byteLen := len(s)
// Rune(码点)数
runeLen := utf8.RuneCountInString(s)
if byteLen != 12 || runeLen != 5 {
panic("mismatch")
}
// 转为 []rune 切片截取
runes := []rune(s)
truncated := string(runes[:4])
if truncated != "Go语言" {
panic("truncation failed")
}
fmt.Printf("bytes: %d, runes: %d, prefix: %s\n", byteLen, runeLen, truncated)
}
Go 将 Unicode 码点命名为 rune(本质是 int32 别名),标准库 unicode/utf8 提供了针对 UTF-8 的编解码支持。
# Java
Javaimport java.nio.charset.StandardCharsets;
public class Main {
public static void main(String[] args) {
String s = "Go语言👍";
int byteLen = s.getBytes(StandardCharsets.UTF_8).length;
int codeUnitLen = s.length();
int codePointCount = s.codePointCount(0, s.length());
assert byteLen == 12;
assert codeUnitLen == 6; // UTF-16 包含一个代理对
assert codePointCount == 5;
// 前 4 个码点恰好是 4 个 UTF-16 单码元字符
String truncated = s.substring(0, s.offsetByCodePoints(0, 4));
assert "Go语言".equals(truncated);
System.out.println("bytes: " + byteLen + ", code_points: " + codePointCount + ", prefix: " + truncated);
}
}
Java 的 length() 测量的是 char(UTF-16 code unit)的数量,使用 codePointCount 与 offsetByCodePoints 才能准确按码点度量。
# JavaScript
JavaScriptconst assert = {
equal: (a, b) => { if (a !== b) throw new Error(`${a} !== ${b}`); },
deepEqual: (a, b) => { if (JSON.stringify(a) !== JSON.stringify(b)) throw new Error(`${JSON.stringify(a)} !== ${JSON.stringify(b)}`); },
throws: (fn, errType) => {
try { fn(); throw new Error("expected throw"); } catch (e) {
if (e.message === "expected throw") throw e;
if (errType && !(e instanceof errType)) throw e;
}
}
};
const s = "Go语言👍";
const byteLen = Buffer.byteLength(s, "utf8");
const codeUnitLen = s.length;
const codePoints = [...s];
assert.equal(byteLen, 12);
assert.equal(codeUnitLen, 6);
assert.equal(codePoints.length, 5);
const truncated = codePoints.slice(0, 4).join("");
assert.equal(truncated, "Go语言");
console.log(`bytes: ${byteLen}, code_points: ${codePoints.length}, prefix: ${truncated}`);
JavaScript 的字符串迭代器(如 [...s])遵循 Unicode 码点规范解开代理对,避免将 Emoji 拆开成无效半边代理。
# TypeScript
TypeScriptconst assert = {
equal: (a: unknown, b: unknown) => { if (a !== b) throw new Error(`${a} !== ${b}`); },
deepEqual: (a: unknown, b: unknown) => { if (JSON.stringify(a) !== JSON.stringify(b)) throw new Error(`${JSON.stringify(a)} !== ${JSON.stringify(b)}`); },
throws: (fn: () => void, errType?: unknown) => {
try { fn(); throw new Error("expected throw"); } catch (e: unknown) {
if (e instanceof Error && e.message === "expected throw") throw e;
if (typeof errType === "function" && !(e instanceof errType)) throw e;
}
}
};
const s: string = "Go语言👍";
const byteLen: number = Buffer.byteLength(s, "utf8");
const codePoints: string[] = [...s];
assert.equal(byteLen, 12);
assert.equal(codePoints.length, 5);
const truncated: string = codePoints.slice(0, 4).join("");
assert.equal(truncated, "Go语言");
console.log(`bytes: ${byteLen}, code_points: ${codePoints.length}, prefix: ${truncated}`);
TypeScript 沿用 JavaScript 运行时机制,字符串类型标注不改变底层 UTF-16 编码行为。
# Kotlin
Kotlinfun main() {
val s = "Go语言👍"
val byteLen = s.toByteArray(Charsets.UTF_8).size
val codeUnitLen = s.length
val codePoints = s.codePointCount(0, s.length)
check(byteLen == 12)
check(codeUnitLen == 6)
check(codePoints == 5)
val offset = s.offsetByCodePoints(0, 4)
val truncated = s.substring(0, offset)
check(truncated == "Go语言")
println("bytes: $byteLen, code_points: $codePoints, prefix: $truncated")
}
Kotlin 在 JVM 平台上直接继承了 Java 的字符处理语义,处理含代理对的字符时注意 offsetByCodePoints。
# Python
Pythonif __name__ == "__main__":
s = "Go语言👍"
byte_len = len(s.encode("utf-8"))
char_len = len(s)
assert byte_len == 12
assert char_len == 5 # Python 的 len() 原生返回码点数
truncated = s[:4]
assert truncated == "Go语言"
print(f"bytes: {byte_len}, code_points: {char_len}, prefix: {truncated}")
Python 3 的 str 类型抽象为由 Unicode 码点组成的序列,索引切片 s[:4] 原生以码点为单位操作。
# C++
C++#include <iostream>
#include <string>
#include <cassert>
// 检查是否是 UTF-8 字符的起始字节 (非 10xxxxxx)
bool is_utf8_start(unsigned char c) {
return (c & 0xC0) != 0x80;
}
int count_code_points(const std::string& s) {
int count = 0;
for (unsigned char c : s) {
if (is_utf8_start(c)) count++;
}
return count;
}
std::string take_code_points(const std::string& s, int n) {
int count = 0;
size_t i = 0;
for (; i < s.size() && count < n; ++i) {
if (is_utf8_start(static_cast<unsigned char>(s[i]))) {
count++;
}
}
while (i < s.size() && !is_utf8_start(static_cast<unsigned char>(s[i]))) {
i++;
}
return s.substr(0, i);
}
int main() {
std::string s = "Go语言👍";
assert(s.size() == 12);
assert(count_code_points(s) == 5);
std::string truncated = take_code_points(s, 4);
assert(truncated == "Go语言");
std::cout << "bytes: " << s.size() << ", code_points: " << count_code_points(s)
<< ", prefix: " << truncated << "\n";
}
C++ 的 std::string 仅保证字节序列。在缺少第三方库时,依靠 UTF-8 首字节位掩码(c & 0xC0 != 0x80)识别码点边界。
# 迁移误区与适用限制
- 混淆长度定义:将
s.length(JS/Java 中的 UTF-16 单元数)误认为是显示宽度或字节长度。在数据库建表或截断展示时容易造成存储溢出或乱码截断。 - 用户感知字素(Graphemes):Unicode 组合字符(如
e+\u0301->é或带肤色的 Emoji👍🏽)由多个码点组成。若需处理字素簇,需要专门的 Unicode 分段算法(如 UAX #29)。
# 运行与验证
语言版本基线见示例基线。 所有程序运行后控制台输出均为:
纯文本bytes: 12, code_points: 5, prefix: Go语言
UTF-8 字节、UTF-16 编码单元与码点数量校验全部通过。
# 相关阅读
- 相等性:值相等与身份相等
- 字符串转端口号:统一输入校验契约