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});

    // 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});
    }
}
构建和运行
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提供快速的字符分类和大小写转换。函数会优雅地处理超出ASCII范围的值,通过返回false或保持原样。

字符分类

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

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!";

    // 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]});
}
构建和运行
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字节。
  • 构建一个简单的URL编码器/解码器,使用Base64进行编码部分,并使用自定义逻辑进行百分比编码特殊字符。

警告、替代方案、边缘情况

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