Chapter 45Text Formatting And Unicode

文本、格式化与 Unicode

概览

在掌握了结构化数据的集合之后(44),现在转向文本——人机交互的基本媒介。本章探讨用于格式化与解析的std.fmt、用于 ASCII 字符操作的std.ascii、用于处理 UTF-8/UTF-16 的std.unicode,以及base64等编码工具。fmt.zigascii.zig

不同于隐藏编码复杂性的高级语言,Zig 直陈其机制:你在[]const u8(字节切片)与正确的 Unicode 码点迭代之间做选择,控制数值格式化精度,并显式处理编码错误。

在 Zig 中进行文本处理需意识到字节与字符边界、用于动态格式化的分配器使用,以及不同字符串操作的性能影响。到本章末,你将能以自定义精度格式化数字、安全解析整数与浮点数、高效处理 ASCII、遍历 UTF-8 序列,并为传输对二进制数据进行编码——全部符合 Zig 的显式特性且无隐藏成本。unicode.zig

学习目标

  • 使用 Writer.print() 和格式说明符格式化整数、浮点数和自定义类型的值。Writer.zig
  • 将字符串解析为整数(parseInt)和浮点数(parseFloat),并正确处理错误。
  • 使用 std.ascii 进行字符分类(isDigitisAlphatoUppertoLower)。
  • 使用 std.unicode 遍历 UTF-8 序列,理解码点与字节的区别。
  • 对二进制数据进行 Base64 编码和解码,实现二进制到文本的转换。base64.zig
  • 在 Zig 0.15.2 中使用 {f} 说明符为用户定义的类型实现自定义格式化器。

使用 std.fmt 进行格式化

Zig 的格式化围绕 Writer.print(fmt, args),它将格式化的输出写入任何 Writer 实现。格式字符串使用 {} 占位符和可选说明符:{d} 表示十进制,{x} 表示十六进制,{s} 表示字符串,{any} 表示调试表示,{f} 表示自定义格式化器。

最简单的模式:用std.io.fixedBufferStream获取一个缓冲,然后向其中print

Zig
const std = @import("std");

pub fn main() !void {
    var buffer: [100]u8 = undefined;
    var fbs = std.io.fixedBufferStream(&buffer);
    const writer = fbs.writer();

    try writer.print("Answer={d}, pi={d:.2}", .{ 42, 3.14159 });

    std.debug.print("Formatted: {s}\n", .{fbs.getWritten()});
}
构建与运行
Shell
$ zig build-exe format_basic.zig && ./format_basic
输出
Shell
Formatted: Answer=42, pi=3.14

std.io.fixedBufferStream 提供一个由固定缓冲区支持的 Writer。无需分配。对于动态输出,使用 std.ArrayList(u8).writer()fixed_buffer_stream.zig

格式说明符

Zig 的格式说明符可控制数值进制、精度、对齐与填充。

Zig
const std = @import("std");

pub fn main() !void {
    const value: i32 = 255;
    const pi = 3.14159;
    const large = 123.0;

    std.debug.print("Decimal: {d}\n", .{value});
    std.debug.print("Hexadecimal (lowercase): {x}\n", .{value});
    std.debug.print("Hexadecimal (uppercase): {X}\n", .{value});
    std.debug.print("Binary: {b}\n", .{value});
    std.debug.print("Octal: {o}\n", .{value});
    std.debug.print("Float with 2 decimals: {d:.2}\n", .{pi});
    std.debug.print("Scientific notation: {e}\n", .{large});
    std.debug.print("Padded: {d:0>5}\n", .{42});
    std.debug.print("Right-aligned: {d:>5}\n", .{42});
}
构建与运行
Shell
$ zig build-exe format_specifiers.zig && ./format_specifiers
输出
Shell
Decimal: 255
Hexadecimal (lowercase): ff
Hexadecimal (uppercase): FF
Binary: 11111111
Octal: 377
Float with 2 decimals: 3.14
Scientific notation: 1.23e2
Padded: 00042
Right-aligned:    42

使用 {d} 表示十进制,{x} 表示十六进制,{b} 表示二进制,{o} 表示八进制。精度(.N)和宽度适用于浮点数和整数。使用 0 填充创建零填充字段。

解析字符串

Zig 提供 parseIntparseFloat 用于将文本转换为数字,对无效输入返回错误而不是崩溃或静默失败。

解析整数

parseInt(T, buf, base) 将字符串转换为指定进制(2-36,或 0 表示自动检测)下类型为 T 的整数。

Zig
const std = @import("std");

pub fn main() !void {
    const decimal = try std.fmt.parseInt(i32, "42", 10);
    std.debug.print("Parsed decimal: {d}\n", .{decimal});

    const hex = try std.fmt.parseInt(i32, "FF", 16);
    std.debug.print("Parsed hex: {d}\n", .{hex});

    const binary = try std.fmt.parseInt(i32, "111", 2);
    std.debug.print("Parsed binary: {d}\n", .{binary});

    // 自动检测带前缀的基数
    const auto = try std.fmt.parseInt(i32, "0x1234", 0);
    std.debug.print("Auto-detected (0x): {d}\n", .{auto});

    // 错误处理
    const result = std.fmt.parseInt(i32, "not_a_number", 10);
    if (result) |_| {
        std.debug.print("Unexpected success\n", .{});
    } else |err| {
        std.debug.print("Parse error: {}\n", .{err});
    }
}
构建与运行
Shell
$ zig build-exe parse_int.zig && ./parse_int
输出
Shell
Parsed decimal: 42
Parsed hex: 255
Parsed binary: 7
Auto-detected (0x): 4660
Parse error: InvalidCharacter

parseInt 返回 error{Overflow, InvalidCharacter}。始终显式处理这些错误或使用 try 传播。基数为 0 时自动检测 0x(十六进制)、0o(八进制)、0b(二进制)前缀。

解析浮点数

parseFloat(T, buf) 将字符串转换为浮点数,处理科学计数法和特殊值(naninf)。

Zig
const std = @import("std");

pub fn main() !void {
    const pi = try std.fmt.parseFloat(f64, "3.14159");
    std.debug.print("Parsed: {d}\n", .{pi});

    const scientific = try std.fmt.parseFloat(f64, "1.23e5");
    std.debug.print("Scientific: {d}\n", .{scientific});

    const infinity = try std.fmt.parseFloat(f64, "inf");
    std.debug.print("Special (inf): {d}\n", .{infinity});
}
构建与运行
Shell
$ zig build-exe parse_float.zig && ./parse_float
输出
Shell
Parsed: 3.14159
Scientific: 123000
Special (inf): inf

parseFloat 支持十进制记数法(3.14)、科学记数法(1.23e5)、十六进制浮点数(0x1.8p3)和特殊值(naninf-inf)。parse_float.zig

ASCII 字符操作

std.ascii 为 7 位 ASCII 提供快速的字符分类和大小写转换。函数通过返回 false 或保持不变来优雅地处理 ASCII 范围之外的值。

字符分类

测试字符是否为数字、字母、空白字符等。

Zig
const std = @import("std");

pub fn main() void {
    const chars = [_]u8{ 'A', '5', ' ' };

    for (chars) |c| {
        std.debug.print("'{c}': alpha={}, digit={}, ", .{ c, std.ascii.isAlphabetic(c), std.ascii.isDigit(c) });

        if (c == 'A') {
            std.debug.print("upper={}\n", .{std.ascii.isUpper(c)});
        } else if (c == '5') {
            std.debug.print("upper={}\n", .{std.ascii.isUpper(c)});
        } else {
            std.debug.print("whitespace={}\n", .{std.ascii.isWhitespace(c)});
        }
    }
}
构建与运行
Shell
$ zig build-exe ascii_classify.zig && ./ascii_classify
输出
Shell
'A': alpha=true, digit=false, upper=true
'5': alpha=false, digit=true, upper=false
' ': alpha=false, digit=false, whitespace=true

ASCII 函数对字节(u8)进行操作。非 ASCII 字节(>127)在分类检查中返回 false

大小写转换

在 ASCII 字符的大写和小写之间进行转换。

Zig
const std = @import("std");

pub fn main() void {
    const text = "Hello, World!";
    var upper_buf: [50]u8 = undefined;
    var lower_buf: [50]u8 = undefined;

    _ = std.ascii.upperString(&upper_buf, text);
    _ = std.ascii.lowerString(&lower_buf, text);

    std.debug.print("Original: {s}\n", .{text});
    std.debug.print("Uppercase: {s}\n", .{upper_buf[0..text.len]});
    std.debug.print("Lowercase: {s}\n", .{lower_buf[0..text.len]});
}
构建与运行
Shell
$ zig build-exe ascii_case.zig && ./ascii_case
输出
Shell
Original: Hello, World!
Uppercase: HELLO, WORLD!
Lowercase: hello, world!

std.ascii 函数逐字节操作,仅影响 ASCII 字符。对于完整的 Unicode 大小写映射,请使用专用的 Unicode 库或手动处理 UTF-8 序列。

Unicode 与 UTF-8

Zig 字符串是 []const u8 字节切片,通常采用 UTF-8 编码。std.unicode 提供用于验证 UTF-8、解码码点以及在 UTF-8 和 UTF-16 之间转换的工具。

UTF-8 校验

检查字节序列是否为有效的 UTF-8。

Zig
const std = @import("std");

pub fn main() void {
    const valid = "Hello, 世界";
    const invalid = "\xff\xfe";

    if (std.unicode.utf8ValidateSlice(valid)) {
        std.debug.print("Valid UTF-8: {s}\n", .{valid});
    }

    if (!std.unicode.utf8ValidateSlice(invalid)) {
        std.debug.print("Invalid UTF-8 detected\n", .{});
    }
}
构建与运行
Shell
$ zig build-exe utf8_validate.zig && ./utf8_validate
输出
Shell
Valid UTF-8: Hello, 世界
Invalid UTF-8 detected

使用 std.unicode.utf8ValidateSlice 验证整个字符串。无效的 UTF-8 可能在假设格式良好的序列的代码中导致未定义行为。

遍历码点

使用 std.unicode.Utf8View 将 UTF-8 字节序列解码为 Unicode 码点。

Zig
const std = @import("std");

pub fn main() !void {
    const text = "Hello, 世界";

    var view = try std.unicode.Utf8View.init(text);
    var iter = view.iterator();

    var byte_count: usize = 0;
    var codepoint_count: usize = 0;

    while (iter.nextCodepoint()) |codepoint| {
        const len: usize = std.unicode.utf8CodepointSequenceLength(codepoint) catch unreachable;
        const c = iter.bytes[iter.i - len .. iter.i];
        std.debug.print("Code point: U+{X:0>4} ({s})\n", .{ codepoint, c });
        byte_count += c.len;
        codepoint_count += 1;
    }

    std.debug.print("Byte count: {d}, Code point count: {d}\n", .{ text.len, codepoint_count });
}
构建与运行
Shell
$ zig build-exe utf8_iterate.zig && ./utf8_iterate
输出
Shell
Code point: U+0048 (H)
Code point: U+0065 (e)
Code point: U+006C (l)
Code point: U+006C (l)
Code point: U+006F (o)
Code point: U+002C (,)
Code point: U+0020 ( )
Code point: U+4E16 (世)
Code point: U+754C (界)
Byte count: 13, Code point count: 9

UTF-8 是可变宽度编码:ASCII 字符为 1 字节,但许多 Unicode 字符需要 2-4 字节。当字符语义重要时,始终遍历码点而不是字节。

Base64 编码

Base64 将二进制数据编码为可打印的 ASCII,用于在文本格式(JSON、XML、URL)中嵌入二进制数据。Zig 提供标准、URL 安全和自定义的 Base64 变体。

编码与解码

将二进制数据编码为 Base64 并将其解码回原样。

Zig
const std = @import("std");

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();
    const allocator = gpa.allocator();

    const original = "Hello, World!";

    // 编码
    const encoded_len = std.base64.standard.Encoder.calcSize(original.len);
    const encoded = try allocator.alloc(u8, encoded_len);
    defer allocator.free(encoded);
    _ = std.base64.standard.Encoder.encode(encoded, original);

    std.debug.print("Original: {s}\n", .{original});
    std.debug.print("Encoded: {s}\n", .{encoded});

    // 解码
    var decoded_buf: [100]u8 = undefined;
    const decoded_len = try std.base64.standard.Decoder.calcSizeForSlice(encoded);
    try std.base64.standard.Decoder.decode(&decoded_buf, encoded);

    std.debug.print("Decoded: {s}\n", .{decoded_buf[0..decoded_len]});
}
构建与运行
Shell
$ zig build-exe base64_basic.zig && ./base64_basic
输出
Shell
Original: Hello, World!
Encoded: SGVsbG8sIFdvcmxkIQ==
Decoded: Hello, World!

std.base64.standard.Encoder.Decoder 提供编码/解码方法。== 填充是可选的,可以通过编码器选项控制。

自定义格式化器

为您的类型实现 format 函数来控制它们如何使用 Writer.print() 打印。

Zig
const std = @import("std");

const Point = struct {
    x: i32,
    y: i32,

    pub fn format(self: @This(), writer: *std.Io.Writer) std.Io.Writer.Error!void {
        try writer.print("({d}, {d})", .{ self.x, self.y });
    }
};

pub fn main() !void {
    const p = Point{ .x = 10, .y = 20 };
    std.debug.print("Point: {f}\n", .{p});
}
构建与运行
Shell
$ zig build-exe custom_formatter.zig && ./custom_formatter
输出
Shell
Point: (10, 20)

在 Zig 0.15.2 中,format 方法签名简化为:pub fn format(self: @This(), writer: *std.Io.Writer) std.Io.Writer.Error!void。使用 {f} 格式说明符调用自定义格式化器(例如,"{f}",而不是 "{}")。

格式化到缓冲区

对于无分配的栈上分配格式化,使用 std.fmt.bufPrint

Zig
const std = @import("std");

pub fn main() !void {
    var buffer: [100]u8 = undefined;
    const result = try std.fmt.bufPrint(&buffer, "x={d}, y={d:.2}", .{ 42, 3.14159 });
    std.debug.print("Formatted: {s}\n", .{result});
}
构建与运行
Shell
$ zig build-exe bufprint.zig && ./bufprint
输出
Shell
Formatted: x=42, y=3.14

如果缓冲区太小,bufPrint 返回 error.NoSpaceLeft。始终适当调整缓冲区大小或处理错误。

带分配的动态格式化

对于动态大小的输出,使用 std.fmt.allocPrint 分配并返回格式化的字符串。

Zig
const std = @import("std");

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();
    const allocator = gpa.allocator();

    const result = try std.fmt.allocPrint(allocator, "The answer is {d}", .{42});
    defer allocator.free(result);

    std.debug.print("Dynamic: {s}\n", .{result});
}
构建与运行
Shell
$ zig build-exe allocprint.zig && ./allocprint
输出
Shell
Dynamic: The answer is 42

allocPrint 返回一个必须使用 allocator.free(result) 释放的切片。当输出大小不可预测时使用此函数。

练习

  • 使用 std.mem.splitparseInt 编写 CSV 解析器,从逗号分隔的文件中读取数字行。mem.zig
  • 实现一个十六进制转储工具,将二进制数据格式化为十六进制并带有 ASCII 表示(类似于 hexdump -C)。
  • 创建一个字符串验证函数,检查字符串是否仅包含 ASCII 可打印字符,拒绝控制码和非 ASCII 字节。
  • 使用 Base64 构建简单的 URL 编码器/解码器进行编码部分,并使用自定义逻辑对特殊字符进行百分比编码。

注意事项、替代方案与边界情况

  • UTF-8 vs. 字节:Zig 字符串是 []const u8。始终澄清您是在处理字节(索引)还是码点(语义字符)。不匹配的假设会导致多字节字符的错误。
  • 区域设置敏感操作std.asciistd.unicode 不处理区域设置特定的大小写映射或排序。对于土耳其语的 iI 或区域设置感知排序,您需要外部库。
  • 浮点数格式化精度parseFloat 通过文本往返可能会损失非常大或非常小的数字的精度。对于精确的十进制表示,使用定点算法或专用十进制库。
  • Base64 变体:标准 Base64 使用 +/,URL 安全使用 -_。为您的用例选择正确的编码器/解码器(std.base64.standardstd.base64.url_safe_no_pad)。
  • 格式字符串安全性:格式字符串在编译时检查,但运行时构造的格式字符串不会受益于编译时验证。尽可能避免动态构建格式字符串。
  • Writer 接口:所有格式化函数都接受 anytype Writer,允许输出到文件、套接字、ArrayList 或自定义目标。确保您的 Writer 实现 write(self, bytes: []const u8) !usize

Help make this chapter better.

Found a typo, rough edge, or missing explanation? Open an issue or propose a small improvement on GitHub.