AI Library · 知识库

# 字符串与 Unicode:字节、码点与字形

学习目标:理解 UTF-8 字节数、UTF-16 编码单元、Unicode 码点与字符截断边界,避免乱码与越界 Panic。

前置知识与范围:字符串类型与数组切片。聚焦内存中 UTF-8 与 UTF-16 字符串的度量与安全前缀截取。

# 问题与边界

以字符串 "Go语言👍" 为基准输入:

# 关键差异

语言 字符串底层编码 默认 .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)识别码点边界。

# 迁移误区与适用限制

# 运行与验证

语言版本基线见示例基线。 所有程序运行后控制台输出均为:

纯文本bytes: 12, code_points: 5, prefix: Go语言

UTF-8 字节、UTF-16 编码单元与码点数量校验全部通过。

# 相关阅读