Zig 基础知识参考字典

用法说明

本篇是 Zig 的条目式速查,按用途分六组:错误与可空、分配器、集合与内存视图、格式化与调试、文件与进程、测试。每条给签名、一句话语义、最小示例和一个坑;条目里的语法原理(错误集、tagged union、comptime)不展开,见语法要点篇;示例中的示意函数(lookup、useVal、parseU32 等)请替换成你自己的同名逻辑再跑。版本基线:示例以 0.14.1(2025-05-23)口径编写并在该线长期稳定;0.15.1(2025-08-20 发布)对 IO 与集合有两处破坏性改动,涉及条目单独标注两版口径。速查表在文末,可只搜表定位条目。

工具链一分钟

命令 用途 易踩的点
zig run file.zig 单文件编译运行 只适合脚本级验证,正经项目用 build
zig init 生成 build.zig 骨架 产物在 zig-out/,不是当前目录
zig build 按 build.zig 编译 加 -Doptimize=ReleaseFast 才是发布优化档
zig build test 跑项目内全部 test 块 未被引用的私有函数不进测试,见测试组
zig fmt 官方格式化,无配置项 没有风格之争,CI 里用它判定
zig cc main.c 把 Zig 当 C 编译器用 真正的卖点是可以 -target 交叉编译

错误与可空

这一组的概念本身在语法要点篇讲过,这里只收日常高频写法。共性与易混点:错误集(error set)和可选类型(optional,?T)都”可能是另一种情况”,但错误携带”为什么失败”,可空只有”有没有”——只有失败原因值得传达时才用错误集。

try / catch:上抛与兜底

fn readLimit(raw: []const u8) !u32 {
    const n = try std.fmt.parseInt(u32, raw, 10); // try:失败则带着错误从本函数上抛
    if (n == 0) return error.Empty;               // 自定义错误随 return 交出去
    return n;
}
const limit = readLimit("80") catch 80;           // catch:就地兜底,给默认值
const v = readLimit("") catch |err| blk: {        // catch 还能捕获错误值
    std.debug.print("why: {s}\n", .{@errorName(err)});
    break :blk 0;
};

⚠️ 常见坑:catch unreachable 把失败断言成”不可能”,一旦发生就是 panic 且无诊断信息——只允许出现在逻辑不变量为真的内部代码,外部输入路径禁止。

errdefer:出错路径的清理

fn makeBuffer(a: std.mem.Allocator) ![]u8 {
    const buf = try a.alloc(u8, 128);
    errdefer a.free(buf);          // 从这里之后任何 return error 都会先执行本行
    if (buf.len != 128) return error.ShortAlloc;
    return buf;                    // 成功返回时 errdefer 不执行,所有权移交调用方
}

⚠️ 常见坑:errdefer 只管”本函数内出错”的路径,成功路径的释放责任在调用方——所有权约定要靠函数签名与注释讲清楚,编译器不替你追踪跨函数的释放。

@errorName:错误转可打印文本

catch |err| std.debug.print("failed: {s}\n", .{@errorName(err)});
// => failed: FileNotFound。错误是编译期值,没有运行时 message 字段

⚠️ 常见坑:想给错误附加上下文(哪个文件、第几行)得自己设计——常见做法是返回结构体包着错误码加上下文字段,而不是指望 @errorName。

orelse 与 .?:可空解包三出口

const name: ?[]const u8 = lookup("id");
const a = name orelse "guest";   // 兜底值
const b = name orelse return;    // 没有就提前退出本函数
if (name) |n| useVal(n);         // 有才执行
const c = name.?;                // 赌非空:空则 panic,仅限内部不变量

⚠️ 常见坑:orelse 后面接的是表达式或控制流语句(return/continue/break),接不了带副作用的块——要副作用用 if 捕获或标签块。

分配器

Zig 标准库凡是要内存的函数都要你传 std.mem.Allocator——这不是风格,是这门语言的核心训练:内存在哪申请、谁负责还,看签名即知。共性易混点:四种内置分配器不是四选一,而是按生命周期搭配用。

alloc / free:一切的底层

const buf = try allocator.alloc(u8, 256);  // 返回 []u8,失败返回 error.OutOfMemory
defer allocator.free(buf);                 // free 的长度信息来自切片本身,不用传大小
const copy = try allocator.dupe(u8, "hi"); // dupe:拷贝一份到分配器内存
const zero = try allocator.alloc(u8, 64);  // alloc 只给未初始化内存,不清零

⚠️ 常见坑:alloc 给的是未初始化内存,直接读是未定义行为——要零值用 allocSentinel/dupe 或立刻写满;释放时传进去的切片必须是原切片(别名了部分区间再 free 属于未定义行为)。

ArenaAllocator:一次分配全场结账

var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
defer arena.deinit();                      // 一次性归还 arena 里的全部内存
const a = arena.allocator();               // 之后所有 alloc 都记在 arena 账上
const line = try a.dupe(u8, "kept until deinit");

⚠️ 常见坑:arena 里的单项 free 基本是空操作,别指望它提前还内存;arena 适合”活一轮、清一次”的请求处理与命令行程序,长驻服务的热路径用它会内存只增不减。

DebugAllocator(0.14 前的 GPA):查泄漏的默认搭档

var gpa = std.heap.DebugAllocator(.{}){};
defer _ = gpa.deinit();                    // 返回值是泄漏检查结果,必须 _ = 接住
const a = gpa.allocator();

⚠️ 常见坑:0.14.0 起 GeneralPurposeAllocator 重写并改名 DebugAllocator,老教程里的 GPA 写法对应的就是它;它是调试档(能报泄漏与双重释放),发布档性能敏感场景换 smp_allocator。

page_allocator 与 smp_allocator

const a1 = std.heap.page_allocator;  // 直接向 OS 要整页,无簿记,零依赖
const a2 = std.heap.smp_allocator;   // 0.14 新增:多线程友好的发布档通用分配器

⚠️ 常见坑:page_allocator 每次分配至少一页(4KB),拿它分配几十字节是典型浪费;smp_allocator 在 Debug 构建里不工作,调试期请留在 DebugAllocator。

集合与内存视图

数组 [N]T 与切片 []T

var arr: [4]u8 = .{ 1, 2, 3, 4 };
const view: []u8 = arr[0..2];       // 切片 = 指针 + 长度,指向原数组
const all: []u8 = &arr;             // 数组指针可整体转切片
// arr 与 view 共享内存:改 view[0] 就是改 arr[0]

⚠️ 常见坑:切片是”视图”不是拷贝,函数返回局部数组的切片等于返回悬垂指针——要么让调用方传缓冲区进来,要么 dupe 到分配器。

std.ArrayList:动态数组(0.15 大改)

// 0.14.1 口径:managed 风格,分配器在 init 时定死
var list = std.ArrayList(u8).init(allocator);
defer list.deinit();
try list.appendSlice("todo");
try std.debug.print("{s} len={d}\n", .{ list.items, list.items.len });

// 0.15.1 口径:默认 unmanaged,每次调用传分配器
// var list: std.ArrayList(u8) = .{};
// try list.append(allocator, 'x');

⚠️ 常见坑:0.15.1(2025-08-20 发布)把默认形态从 managed 换成 unmanaged,.init(allocator) 与 .append(x) 都要改写——网上教程按版本对号,本条示例两版口径都给了。

StringHashMap / AutoHashMap

var counts = std.StringHashMap(u32).init(allocator);
defer counts.deinit();
try counts.put("GET", 3);                 // 键是 []const u8 时用 StringHashMap
const n = counts.get("GET");              // 返回 ?u32,注意判空
_ = n;
var byId = std.AutoHashMap(u32, []const u8).init(allocator);
defer byId.deinit();                      // AutoHashMap:整数/布尔等可位比较的键

⚠️ 常见坑:get 返回可空类型不是值本身;StringHashMap 的键字符串必须在 map 存活期间有效——键来自临时缓冲区时先 dupe 再 put。

std.mem 高频工具

if (std.mem.eql(u8, cmd, "add")) {}                 // 字节级比较,字符串相等就靠它
if (std.mem.startsWith(u8, line, "# ")) {}          // 前缀判断
const trimmed = std.mem.trim(u8, line, " \t\r");    // 去两端指定字符
var it = std.mem.splitScalar(u8, text, '\n');       // 按单字节分隔的迭代器
while (it.next()) |seg| useSeg(seg);

⚠️ 常见坑:splitScalar 只按单字节切,多字节分隔符用 splitSequence;eql 比较的是字节不是”字符”,UTF-8 下按字素处理是另一回事。

格式化与调试

std.debug.print:最顺手的输出

std.debug.print("{s} {d} {x}\n", .{ "hello", 42, 255 }); // 打到 stderr
std.debug.print("{{literal}} {any}\n", .{Shape{ .circle = 1.0 }}); // {{ 转义大括号
格式符 吃什么 坑
{s} 字符串与字节数组 给数字用它是运行时 panic
{d} 整数与浮点(十进制) 给字符串用它是运行时 panic
{c} 单字节字符 超过单字节的 UTF-8 码点别用它
{x} 十六进制 {x} 小写、{X} 大写;前缀行为以 std.fmt 文档为准
{any} 几乎任何东西的默认打印 struct 输出按字段展开,长对象别拿它调日志

⚠️ 常见坑:debug.print 只走 stderr,写正式产物(重定向到文件的内容)要用 std.fs.File 的 writer——注意 0.15.1 的 Writer 接口重构,见下组。

@TypeOf / @typeName / @sizeOf

const T = @TypeOf(42);              // 表达式的类型,编译期
std.debug.print("{s}\n", .{@typeName(T)});   // => comptime_int(类型转文本)
std.debug.print("{d}\n", .{@sizeOf(Shape)}); // 类型字节数

⚠️ 常见坑:@TypeOf 拿到的是”表达式类型”,整数字面量给 comptime_int 不是 u32——想知道变量落定后的类型,看赋值目标的类型。

文件与进程

本组按 0.14.1 口径编写。0.15.1 的 Writergate 重构把 std.io 的 Reader/Writer 换成了新接口(更多样板但更明确),File 的 reader()/writer() 系列方法签名全部变动——迁移以官方 0.15.1 release notes 为准;下述 openFile/createFile/writeAll 在两代之间保持稳定。

读文件

// 0.14.1:一次性读入分配器内存,max 是防失控的上限
const data = try std.fs.cwd().readFileAlloc(allocator, "todo.txt", 1 << 20);
defer allocator.free(data);

⚠️ 常见坑:文件不存在返回 error.FileNotFound,是普通错误值——CLI 第一次运行时”文件不存在”往往应该当空数据而不是崩溃,动手前想清楚语义。

写文件

const f = try std.fs.cwd().createFile("todo.txt", .{ .truncate = true });
defer f.close();
try f.writeAll("id\tdone\ttext\n");  // truncate = true:整体覆盖写

⚠️ 常见坑:createFile 默认 truncate(清空重写),追加写要 .{ .truncate = false } 并 seek 到末尾——”以为在追加其实在覆盖”是数据丢失类事故。

命令行参数

// 0.14.1:取参数要分配器
const args = try std.process.argsAlloc(allocator);
defer std.process.argsFree(allocator, args);
if (args.len < 2) return error.Usage;   // args[0] 是程序名,业务参数从 1 起

⚠️ 常见坑:0.15 起 std.process.args 不再需要分配器(签名变了),跨版本迁移时这里是必改点。

测试

test 块与断言

const std = @import("std");
const testing = std.testing;

test "parse ok" {
    const n = try parseU32("42");
    try testing.expectEqual(@as(u32, 42), n);      // 值相等
}
test "parse rejects garbage" {
    try testing.expectError(error.InvalidChar, parseU32("x!")); // 指定错误必须出现
}
test "string compare" {
    try testing.expectEqualStrings("ok", render(200));
}

⚠️ 常见坑:Zig 的测试是”跑编译也是跑测试”——测试块里的编译错误会在测试编译阶段报出来,所以测试顺带兜住了死代码;断言失败打印两侧实际值,先看打印再改代码。

refAllDecls:让私有函数进测试

test {
    std.testing.refAllDecls(@This());  // 引用本文件全部声明,防"未被引用就不编译"
}

⚠️ 常见坑:Zig 按需编译,没被引用的函数不进产物,测试也就测不到——每个测试文件放一条 refAllDecls 是惯用兜底。

速查表

条目 一句话用途 有什么坑
try / catch 错误上抛 / 就地兜底 catch unreachable 只限内部不变量
errdefer 出错路径专属清理 成功路径释放责任在调用方
@errorName 错误转打印文本 没有 message 字段,上下文自己带
orelse / .? 可空解包兜底 / 赌非空 .? 空则 panic
alloc / free 分配器基础读写 未初始化内存不可读;free 要原切片
dupe 拷贝进分配器内存 想改调用方只读的字符串必须先 dupe
ArenaAllocator 整批申请整批还 单项 free 是空操作;不适合长驻热路径
DebugAllocator 调试档分配器(查泄漏) 0.14 起由 GPA 改名而来
smp_allocator 0.14+ 发布档通用分配器 Debug 构建不工作
[N]T vs []T 定长数组 vs 指针长度视图 切片共享内存,别返回局部数组切片
ArrayList 动态数组 0.15.1 起默认 unmanaged,教程要对版本
StringHashMap 字符串键哈希表 get 返回可空;键要 dupe 进去
std.mem.eql 字节级比较 是字节不是字素,UTF-8 语义另算
std.debug.print stderr 调试输出 {s}/{d} 给错类型运行时 panic
@TypeOf / @typeName 编译期取类型 / 类型名 字面量是 comptime_int
readFileAlloc 整文件读入内存 FileNotFound 当不当错误要想清楚
createFile + writeAll 覆盖写文件 默认 truncate,追加要显式
argsAlloc 0.14 取命令行参数 0.15 起签名去掉分配器
testing.expectEqual 值断言 浮点用 expectApproxEqAbs
refAllDecls 强制编译全部声明 没有它私有函数测不到

下一步

查完条目,把分配器与错误处理两组放进真实项目里练:《用 Zig 写一个 TODO CLI》就是按这两组组织的实战。系列定位入口仍是开山篇;学完了去哪,见Zig 学习路线。

版本核实(以官方文档为准):0.14.0 发布于 2025-03-05,GeneralPurposeAllocator 改名 DebugAllocator 并新增 smp_allocator;0.14.1 发布于 2025-05-23;0.15.1 发布于 2025-08-20(0.15.0 被取消),其 IO(Reader/Writer)接口重构与 ArrayList 默认 unmanaged 均为该版公开变更,涉及条目已标注两版口径;std.debug.print、文件读写、std.mem 工具、测试断言等条目在 0.14/0.15 均为已发布行为;async/await 自 0.11 起移除后仍未回归,本文未引用后续未发布版本的行为。

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇