概述
在掌握集合数据结构后,第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});
// Auto-detect base with prefix
const auto = try std.fmt.parseInt(i32, "0x1234", 0);
std.debug.print("Auto-detected (0x): {d}\n", .{auto});
// Error handling
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提供快速的字符分类和大小写转换。函数会优雅地处理超出ASCII范围的值,通过返回false或保持原样。
字符分类
测试字符是否为数字、字母、空白等。
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!";
// Encode
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});
// Decode
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字节。
- 构建一个简单的URL编码器/解码器,使用Base64进行编码部分,并使用自定义逻辑进行百分比编码特殊字符。
警告、替代方案、边缘情况
- UTF-8与字节:Zig字符串是
[]const u8。请务必澄清您是在处理字节(索引)还是代码点(语义字符)。不匹配的假设会导致多字节字符的错误。 - 区域设置敏感操作:
std.ascii和std.unicode不处理特定于区域设置的大小写映射或排序规则。对于土耳其语的i与I或区域设置感知的排序,您需要外部库。 - 浮点数格式化精度:通过文本往返的
parseFloat对于非常大或非常小的数字可能会丢失精度。对于精确的十进制表示,请使用定点算术或专用的十进制库。 - Base64变体:标准Base64使用
+/,URL安全使用-_。为您的用例选择正确的编码器/解码器(std.base64.standardvs.std.base64.url_safe_no_pad)。 - 格式字符串安全:格式字符串是经过
comptime检查的,但运行时构造的格式字符串无法从编译时验证中受益。尽可能避免动态构建格式字符串。 - Writer接口:所有格式化函数都接受
anytype的Writers,允许输出到文件、套接字、ArrayLists或自定义目的地。请确保您的Writer实现了write(self, bytes: []const u8) !usize。