概览
在掌握了结构化数据的集合之后(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进行字符分类(isDigit、isAlpha、toUpper、toLower)。 - 使用
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。
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()});
}
$ zig build-exe format_basic.zig && ./format_basicFormatted: Answer=42, pi=3.14std.io.fixedBufferStream 提供一个由固定缓冲区支持的 Writer。无需分配。对于动态输出,使用 std.ArrayList(u8).writer()。fixed_buffer_stream.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});
}
$ zig build-exe format_specifiers.zig && ./format_specifiersDecimal: 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 提供 parseInt 和 parseFloat 用于将文本转换为数字,对无效输入返回错误而不是崩溃或静默失败。
解析整数
parseInt(T, buf, base) 将字符串转换为指定进制(2-36,或 0 表示自动检测)下类型为 T 的整数。
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});
}
}
$ zig build-exe parse_int.zig && ./parse_intParsed decimal: 42
Parsed hex: 255
Parsed binary: 7
Auto-detected (0x): 4660
Parse error: InvalidCharacterparseInt 返回 error{Overflow, InvalidCharacter}。始终显式处理这些错误或使用 try 传播。基数为 0 时自动检测 0x(十六进制)、0o(八进制)、0b(二进制)前缀。
解析浮点数
parseFloat(T, buf) 将字符串转换为浮点数,处理科学计数法和特殊值(nan、inf)。
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});
}
$ zig build-exe parse_float.zig && ./parse_floatParsed: 3.14159
Scientific: 123000
Special (inf): infparseFloat 支持十进制记数法(3.14)、科学记数法(1.23e5)、十六进制浮点数(0x1.8p3)和特殊值(nan、inf、-inf)。parse_float.zig
ASCII 字符操作
std.ascii 为 7 位 ASCII 提供快速的字符分类和大小写转换。函数通过返回 false 或保持不变来优雅地处理 ASCII 范围之外的值。
字符分类
测试字符是否为数字、字母、空白字符等。
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)});
}
}
}
$ zig build-exe ascii_classify.zig && ./ascii_classify'A': alpha=true, digit=false, upper=true
'5': alpha=false, digit=true, upper=false
' ': alpha=false, digit=false, whitespace=trueASCII 函数对字节(u8)进行操作。非 ASCII 字节(>127)在分类检查中返回 false。
大小写转换
在 ASCII 字符的大写和小写之间进行转换。
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]});
}
$ zig build-exe ascii_case.zig && ./ascii_caseOriginal: 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。
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", .{});
}
}
$ zig build-exe utf8_validate.zig && ./utf8_validateValid UTF-8: Hello, 世界
Invalid UTF-8 detected使用 std.unicode.utf8ValidateSlice 验证整个字符串。无效的 UTF-8 可能在假设格式良好的序列的代码中导致未定义行为。
遍历码点
使用 std.unicode.Utf8View 将 UTF-8 字节序列解码为 Unicode 码点。
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 });
}
$ zig build-exe utf8_iterate.zig && ./utf8_iterateCode 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: 9UTF-8 是可变宽度编码:ASCII 字符为 1 字节,但许多 Unicode 字符需要 2-4 字节。当字符语义重要时,始终遍历码点而不是字节。
Base64 编码
Base64 将二进制数据编码为可打印的 ASCII,用于在文本格式(JSON、XML、URL)中嵌入二进制数据。Zig 提供标准、URL 安全和自定义的 Base64 变体。
编码与解码
将二进制数据编码为 Base64 并将其解码回原样。
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]});
}
$ zig build-exe base64_basic.zig && ./base64_basicOriginal: Hello, World!
Encoded: SGVsbG8sIFdvcmxkIQ==
Decoded: Hello, World!std.base64.standard.Encoder 和 .Decoder 提供编码/解码方法。== 填充是可选的,可以通过编码器选项控制。
自定义格式化器
为您的类型实现 format 函数来控制它们如何使用 Writer.print() 打印。
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});
}
$ zig build-exe custom_formatter.zig && ./custom_formatterPoint: (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。
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});
}
$ zig build-exe bufprint.zig && ./bufprintFormatted: x=42, y=3.14如果缓冲区太小,bufPrint 返回 error.NoSpaceLeft。始终适当调整缓冲区大小或处理错误。
带分配的动态格式化
对于动态大小的输出,使用 std.fmt.allocPrint 分配并返回格式化的字符串。
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});
}
$ zig build-exe allocprint.zig && ./allocprintDynamic: The answer is 42allocPrint 返回一个必须使用 allocator.free(result) 释放的切片。当输出大小不可预测时使用此函数。
练习
- 使用
std.mem.split和parseInt编写 CSV 解析器,从逗号分隔的文件中读取数字行。mem.zig - 实现一个十六进制转储工具,将二进制数据格式化为十六进制并带有 ASCII 表示(类似于
hexdump -C)。 - 创建一个字符串验证函数,检查字符串是否仅包含 ASCII 可打印字符,拒绝控制码和非 ASCII 字节。
- 使用 Base64 构建简单的 URL 编码器/解码器进行编码部分,并使用自定义逻辑对特殊字符进行百分比编码。
注意事项、替代方案与边界情况
- UTF-8 vs. 字节:Zig 字符串是
[]const u8。始终澄清您是在处理字节(索引)还是码点(语义字符)。不匹配的假设会导致多字节字符的错误。 - 区域设置敏感操作:
std.ascii和std.unicode不处理区域设置特定的大小写映射或排序。对于土耳其语的i与I或区域设置感知排序,您需要外部库。 - 浮点数格式化精度:
parseFloat通过文本往返可能会损失非常大或非常小的数字的精度。对于精确的十进制表示,使用定点算法或专用十进制库。 - Base64 变体:标准 Base64 使用
+/,URL 安全使用-_。为您的用例选择正确的编码器/解码器(std.base64.standard与std.base64.url_safe_no_pad)。 - 格式字符串安全性:格式字符串在编译时检查,但运行时构造的格式字符串不会受益于编译时验证。尽可能避免动态构建格式字符串。
- Writer 接口:所有格式化函数都接受
anytypeWriter,允许输出到文件、套接字、ArrayList 或自定义目标。确保您的 Writer 实现write(self, bytes: []const u8) !usize。