Zig 语法要点:显式一切的设计哲学

要点

前置阅读是开山篇:定位、安装与 hello world 已讲过,本篇只讲语言核心。Zig 的语法坎不在”看不懂”,而在”别的语言藏在运行时或语法糖里的事,全部摊开写在代码里”——分配器要传参、错误要写进返回类型、类型转换必须手动。按”变量与编译期已知值 → 整数与溢出 → 控制流 → 错误处理 → 可空类型 → 自定义类型 → comptime”的顺序过掉高频误解。示例按片段给出,补上 fn 包装与 @import 即可跑。版本基线:0.14.1(2025-05-23 发布)仍是多数教程与在役代码的基准;0.15.1 已于 2025-08-20 发布(0.15.0 被取消,直接出 0.15.1),重点是 IO 接口重构与 ArrayList 改为默认无管理(unmanaged)——本篇示例主体在 0.14/0.15 一致,个别差异单独标注。

变量、常量与编译期已知值

规则一条主线:const 不可变,var 可变,能用 const 就用 const。类型可以省略让编译器推断,但整数字面量默认推断成 comptime_int(编译期整数,任意精度),赋给具体类型时才落定:

const std = @import("std");

const max_users: u32 = 1000;      // 顶层常量,编译期已知
const greeting = "hello";         // 类型 *const [5:0]u8:数组指针带长度与哨兵

pub fn main() void {
    var count: usize = 0;         // var 不允许"从未修改",改不改得到编译器检查
    _ = &count;                   // 确要保留未用的 var,取引用避免报错
    const n: u8 = 42 + max_users; // 两个编译期已知值相加,编译期算完

    var buf: [16]u8 = undefined;  // undefined:显式声明"内容未初始化"
    buf[0] = 'z';
    std.debug.print("{c}{s} n={d}\n", .{ buf[0], greeting, n });
}

两个高频误解。第一,undefined 不是零值:它是”我保证赋值前不读”的显式声明,读了就是未定义行为(undefined behavior)——C 程序员熟悉的坑,Zig 只是把它从”无意踩到”变成”必须亲口承认”。第二,var 声明后从未修改会被编译器拦下,这是”可变性最小化”的语言级执行。

整数是主角:溢出行为写进运算符

Zig 的整数类型是 u8/i32/usize 这种”位宽明示”风格,没有隐式转换,不同位宽混算必须手动 cast。溢出怎么办被做成了运算符选择——默认 + 在 Debug 与 ReleaseSafe 模式下溢出即 panic(安全模式兜底),想要别的语义就换运算符,选择写在代码里而不是编译选项里:

const a: u8 = 200;
// const b = a + 100;         // 编译错误:两个编译期已知值相加溢出,直接在编译期报
const wrapped = a +% 100;     // 回绕加法:结果 44,无检查
const clamped = a +| 100;     // 饱和加法:停在 255,不回绕
const widened: u16 = @intCast(a); // 跨类型转换必须显式,@intCast 运行时检查范围
写法 语义 适用场景
a + b 溢出时 panic(Debug/ReleaseSafe),ReleaseFast 未定义 默认选择:宁可崩也不静默出错
a +% b 按位回绕,无检查 哈希、校验和等刻意回绕的算法
a +| b 饱和到边界值 计数器、水位线等”到顶就停”的量
@intCast 运行时范围检查的显式转换 所有跨位宽转换,编译器不猜

控制流:if、switch 都是表达式

Zig 没有三元运算符,因为 if 本身是表达式;switch 同样是表达式,且必须穷尽所有情况,没有隐式贯穿(fallthrough)。配合标签块(labeled block),任何”求值并提前退出”的写法都有归宿:

const kind = if (score >= 60) "pass" else "fail";   // if 表达式,替代 ?:

const label = switch (code) {
    200 => "ok",
    404 => "missing",
    else => "unknown",   // 整数 switch 需要 else 兜底;枚举 switch 不写 else 也必须穷尽
};

// 标签块:块内 break :blk 返回值,比临时变量 + 多层 if 干净
const first = blk: {
    for (rows) |row| {
        if (row.ok) break :blk row;  // 从块里"返回"值
    }
    break :blk null;
};

var i: usize = 0;
while (i < 10) : (i += 1) {   // while 的第三段是 continue 表达式,i += 1 不会漏写
    if (i % 2 == 0) continue;
}

反模式点名:把 C 的 switch 直觉带过来。Zig 的 switch 分支之间没有贯穿,忘写 break 不会串分支——但反过来,靠”漏写 break 省事”的习惯会让人在 switch 里塞副作用代码;switch 是求值的,分支里放复杂状态修改请拆函数。

错误处理:错误集是返回类型的一部分

Zig 没有异常。函数可能怎么失败,写在返回类型的错误集(error set)里,E!T 读作”要么是 T,要么是 E 里的某个错误”。机制细节开山篇已对比过 Go,这里补上日常使用的全貌——try 上抛、catch 兜底、errdefer 只在出错路径执行清理:

const LoadError = error{ TooBig, Malformed };

fn load(data: []const u8) LoadError!u32 {
    const n = std.fmt.parseInt(u32, data, 10) catch return error.Malformed;
    if (n > 65535) return error.TooBig;
    return n;
}

fn demo(raw: []const u8) !void {   // !void:推断错误集,具体集合由编译器汇总
    const res = load(raw) catch |err| {
        std.debug.print("load failed: {s}\n", .{@errorName(err)});
        return err;                  // 捕获后可以记日志再决定继续上抛
    };
    _ = res;
}

fn withResource() !void {
    const buf = try std.heap.page_allocator.alloc(u8, 64);
    errdefer std.heap.page_allocator.free(buf); // 只在出错路径执行,不会与成功路径双重释放
    if (buf.len != 64) return error.ShortRead;  // 此处 return 前,errdefer 已把内存还掉
    std.heap.page_allocator.free(buf);          // 成功路径自己负责释放
}

错误是值,只能沿返回值流动——没有异常栈回卷,也就没有”深层函数偷偷 throw”。代价是失败路径全部显式可见,好处是任何函数的失败方式看签名即知。与 Go 的 if err != nil 的逐项对照,见Go 错误处理。

可空类型:null 存在,但被关进类型里

Zig 有 null,但只能存在于 ?T(optional)类型中,编译器强制拆包后才能当 T 用。三个惯用解包出口对应三种意图:

fn find(id: u32) ?[]const u8 { return if (id == 1) "alice" else null; }

const a = find(1) orelse "guest";   // 意图"给兜底值":orelse
const b = find(1) orelse return;    // 意图"没有就算了":orelse 后接控制流
if (find(1)) |name| {               // 意图"有才处理":if 捕获
    std.debug.print("{s}\n", .{name});
}
// const c = find(2).?;             // .? 是"赌它非空",空则 panic,留给断言场景

反模式点名:.? 当默认拆包姿势用。它和错误处理里的 catch unreachable 同一性质——把”我确信不会发生”当常规写法,用户数据一变就 panic。外部输入一律走 orelse / if 捕获,.? 留给逻辑上确实不可能为空的内部不变量。

struct、enum 与 tagged union:没有继承的组合学

struct 没有构造函数与析构函数,字段可以给默认值,方法就是第一个参数为 Self 的普通函数;enum 与 tagged union(带标签联合)把”这个值是哪几种形状之一”变成类型,switch 穷尽它就是 Zig 的多态主力——没有虚函数表,接口用 tagged union 或泛型参数表达:

const Shape = union(enum) {   // tagged union:标签 + 载荷一体
    circle: f64,              // 载荷:半径
    rect: struct { w: f64, h: f64 },

    fn area(self: Shape) f64 {   // 方法:self 显式声明类型
        return switch (self) {
            .circle => |r| 3.14159 * r * r,   // => |r| 捕获载荷
            .rect  => |s| s.w * s.h,
        };
    }
};

想加新形状就必须改 switch,编译器会指出所有漏改的位置——运行时多态的灵活性换成了编译期穷尽的确定性,这是设计取舍不是缺陷。需要真正动态分发的插件式场景,用函数指针表自己组,Zig 标准库自己也这么做。

comptime:泛型只是它的一道菜

comptime 标记的参数与代码在编译期执行。最常见用途是拿”类型”当参数(type 类型的参数),为每个用到的类型生成专门代码——这就是 Zig 的泛型,没有单独语法:

fn Stack(comptime T: type, comptime N: usize) type {
    return struct {
        items: [N]T = undefined,
        len: usize = 0,
        pub fn push(self: *@This(), v: T) !void {
            if (self.len == N) return error.Overflow;
            self.items[self.len] = v;
            self.len += 1;
        }
    };
}
const s: Stack(u8, 4) = .{};   // 编译期实例化出一个新类型

与开山篇强调过的一样:报错发生在你自己的代码里,而不是模板展开的深渊里;代价是没有运行时多态。日常能感知 comptime 存在的另一个点是:所有顶层 const 都是编译期已知的,函数参数想强制编译期传入就写 comptime x: usize。

与 Go 的语法对照

维度 Go Zig
不可变 const 仅限编译期常量 const 通用,var 未用会被拦
数值转换 T(x) 显式,溢出静默回绕 @intCast 显式 + 溢出语义选运算符
控制流 if/switch 是语句,三元用 if/else 写 if/switch 是表达式,穷尽检查
错误 error 接口值 + if err != nil 错误集进签名 + try/catch/errdefer
空值 nil 指针与 ok 惯用法 ?T 强制拆包
多态 接口(隐式实现) tagged union / comptime 泛型
隐藏控制流 panic 可从深处冒出 无异常无运算符重载,无隐藏路径

下一步

语法过完,下一步是一份能查的索引:《Zig 基础知识参考字典》——分配器、集合、文件与测试的条目速查都在那里,跑通示例比背语法有效。之后用TODO CLI 实战把分配器与错误处理两个主题练成手感。

版本核实(以官方文档为准):写作时点最新稳定版为 0.15.1(2025-08-20 发布,0.15.0 被取消),上一稳定线 0.14.1 于 2025-05-23 发布(0.14.0 为 2025-03-05);0.14.0 起 GeneralPurposeAllocator 改名 DebugAllocator 并新增 smp_allocator;0.15.1 的重点是 std.Io 读写接口重构(Writergate)与 ArrayList 默认改为 unmanaged,本篇示例未触及这两处差异(只用 std.debug.print 与语言本体语法),在 0.14 与 0.15 下均可编译;async/await 自 0.11 移除后仍处于搁置状态;本文未引用尚未发布版本的行为。

暂无评论

发送评论 编辑评论


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