Skip to content

Day 13: 字符串与字面量

欢迎来到第十三天!今天我们专注于在Zig中处理文本数据。与其他许多高级语言不同,Zig没有一个内置的、特殊的 string 类型。相反,Zig将字符串视为字节切片。最常见的字符串类型是 []const u8,即一个只读的字节切片。这种设计选择揭示了Zig的底层本质:它让你直接处理原始内存,同时提供了足够的工具来安全、高效地完成任务。默认情况下,Zig期望这些字节是UTF-8编码的。

在Zig中,你会遇到两种主要的字符串类型:

  • []const u8: 一个不可变的字节切片。这是最常见的字符串类型,字符串字面量(比如 "hello")的类型就是它。由于它是只读的,多个部分可以安全地共享它,无需担心数据被意外修改。

  • []u8: 一个可变的字节切片。如果你需要动态构建或修改字符串,你会使用这种类型。它指向的内存必须是可写的。

// 字符串字面量是 []const u8
const greeting: []const u8 = "Hello, Zig!";
// 一个可变的字符串缓冲区
var buffer: [50]u8 = undefined;
var mutable_slice: []u8 = &buffer;

字面量(Literals):

const single_line = "This is a string.";
// 多行字符串
const multi_line =
\line one
\line two
;
// 上面的多行字符串等价于 "line oneline two"
// `\\` 用于在多行文本中连接行,同时去除前导空格
// C风格的转义序列也被支持
const with_escapes = "First line\nSecond line";

连接(Concatenation):

在编译时,你可以直接将两个或多个字符串字面量并列放置来进行连接。

const full_name = "John" ++ " " ++ "Doe"; // 编译时连接
// full_name 的值是 "John Doe"

要在运行时连接字符串,你需要一个缓冲区。std.fmt.bufPrint 是一个常用的工具。

比较(Comparison):

使用 std.mem.eql 来比较两个字符串(或任何切片)的内容是否相等。

const std = @import("std");
const a: []const u8 = "hello";
const b: []const u8 = "world";
if (std.mem.eql(u8, a, b)) {
// ... they are equal
} else {
// ... they are not equal
}

其他常用操作:

标准库的 std.mem 和 std.ascii 模块提供了许多有用的函数,如 startsWith, endsWith, contains, toUpper, toLower 等。

Zig的核心和标准库都假设字符串是UTF-8编码的。这意味着一个“字符”可能由1到4个字节组成。len 属性计算的是字节数,而不是字符数。

如果你需要处理Unicode字符(码点),标准库提供了 std.unicode 模块。

const std = @import("std");
const hello_unicode = "你好, Zig!";
// 错误的方式:这只会迭代字节
for (hello_unicode) |byte| {
// ...
}
// 正确的方式:使用 utf8ToUnicode 来迭代码点
var it = std.unicode.Utf8View.init(hello_unicode);
while (it.nextCodepoint()) |codepoint| {
std.debug.print("Codepoint: {c}\n", .{codepoint});
} else |err| {
// 处理无效的UTF-8序列
}

std.fmt 模块提供了类型安全的字符串格式化功能,是Zig中构建字符串的主要方式,类似于其他语言中的 printf 或 format。

  • std.fmt.bufPrint: 将格式化后的字符串写入一个可变缓冲区([]u8)。
  • std.fmt.allocPrint: 在堆上分配一个新的字符串。
const std = @import("std");
pub fn main() !void {
const name = "Alice";
const age = 30;
// 使用 bufPrint 写入一个栈上的缓冲区
var buffer: [100]u8 = undefined;
const formatted_slice = try std.fmt.bufPrint(&buffer, "User {s} is {d} years old.", .{ name, age });
std.debug.print("{s}\n", .{formatted_slice});
}

格式化占位符:

  • {s}: 字符串
  • {d}: 十进制整数
  • {x}: 十六进制整数
  • {c}: 单个字符
  • {any}: 打印任何类型的调试表示

这个例子演示了如何按分隔符分割字符串,然后用不同的分隔符重新连接它们。

const std = @import("std");
pub fn main() !void {
const allocator = std.heap.page_allocator;
const data = "apples,oranges,bananas";
var parts = std.mem.split(u8, data, ",");
// 使用 ArrayList 来存储和连接
var list = std.ArrayList(u8).init(allocator);
defer list.deinit();
var it = parts.next();
while (it) |part| {
try list.writer().print("{s}", .{part});
it = parts.next();
if (it != null) {
try list.writer().print(" & ", .{});
}
}
std.debug.print("{s}\n", .{list.items}); // 输出: apples & oranges & bananas
}

7. 实践练习:简易JSON字符串值的解析

Section titled “7. 实践练习:简易JSON字符串值的解析”

编写一个函数 parseJsonStringValue(input: []const u8) ?[]const u8。

这个函数应该:

  1. 检查输入字符串是否以 " 开始并以 " 结束。
  2. 如果不是,返回 null。
  3. 如果是,返回引号内部的字符串切片(不包括引号本身)。
  4. 暂时不需要处理转义字符(如 \")。

例如,parseJsonStringValue("\"hello\"") 应该返回 "hello"。

问:Zig的字符串是空值终止(Null-Terminated)的吗?

答:不是。这是一个与C语言的重要区别。Zig的切片通过 len 属性知道自己的长度,所以不需要一个特殊的终止符。当你需要与C代码交互时,标准库提供了方便的方法来创建C兼容的、以空值终止的字符串,例如 std.mem.sliceTo 和 std.cstr.addz。

问:为什么字符串操作看起来比其他语言复杂?

答:因为Zig让你直面内存管理。在其他语言中,字符串的连接和修改通常会隐式地在后台进行内存分配。Zig则要求你明确地提供一个缓冲区(通过栈或堆分配器),这使得代码更冗长,但也让你对内存使用有完全的控制,避免了隐藏的性能开销和内存碎片。

今天,我们深入了解了Zig的字符串模型。我们学到,字符串本质上是字节切片(通常是 []const u8),并且标准库期望它们是UTF-8编码的。我们探索了字符串的创建、连接、比较和格式化,并强调了Zig在处理字符串时对显式内存管理的侧重。这种方式虽然需要更多的代码,但它赋予了开发者无与伦比的性能和控制力。

明天,我们将进入一个更底层、更强大的主题:指针基础与安全。