cv.cm

編碼 · 程式語言

Zig 入門教學

閱讀約 9 分鐘

簡答

Zig 是一種通用的系統程式語言與工具鏈,定位為 C 的現代替代方案。它沒有隱藏的控制流程、沒有垃圾回收,也沒有隱藏的記憶體配置:你要明確傳入 allocator,用 error union 與 try 處理錯誤,並以 defer 做清理。編譯期程式碼執行(comptime)取代了巨集與泛型。用 zig run hello.zig 即可執行檔案。Zig 仍在 1.0 版之前,因此語言與標準函式庫的細節會隨版本而變動。

Zig 是什麼、誰在使用

Zig 由 Andrew Kelley 創造,由非營利組織 Zig Software Foundation 開發。它瞄準與 C 相同的領域:作業系統元件、嵌入式韌體、遊戲引擎、資料庫與快速的命令列工具。Zig 編譯器也能編譯 C 與 C++ 程式碼(zig cc),並可交叉編譯到許多目標平台,所以有些專案只把它當作 C 工具鏈使用。

以 Zig 撰寫的知名專案包括 Bun JavaScript 執行環境、TigerBeetle 金融資料庫,以及 Ghostty 終端機模擬器。

安裝 Zig 並執行 Hello, world

從 ziglang.org/download 下載適合你系統的預先建置壓縮檔,解壓縮後把資料夾加入 PATH,或使用系統的套件管理工具安裝 Zig。沒有另外的安裝程式或執行環境。

// hello.zig
const std = @import("std");
pub fn main() void {
    std.debug.print("Hello, world!\n", .{});
}
zig run hello.zig         # compile and run
# Hello, world!
zig build-exe hello.zig   # produce an executable named hello
zig init                  # create a project with build.zig

std.debug.print 會寫到標準錯誤輸出,是目前所有版本中最簡單的列印方式。第二個引數是格式佔位符對應值所組成的元組;.{} 是空元組。寫入標準輸出的 API 在各版本之間有變動,請查閱你所用版本的標準函式庫文件。

變數與基本型別

const 宣告永不改變的值,var 宣告可變的變數。編譯器很嚴格:未使用的區域變數是錯誤,從未被修改的 var 也是錯誤,所以預設請用 const。

型別範例說明
有號整數i8, i32, i64允許任意位元寬度,例如 i7
無號整數u8, u32, usizeusize 與指標同寬,用於長度與索引
浮點數f32, f64IEEE 754
booltrue, false不會從整數隱含轉換
陣列[3]u8長度固定,於編譯期已知
Slice[]const u8指標加長度;字串就是位元組 slice
Optional?i32一個值或 null
Error union!u32一個值或一個錯誤
const std = @import("std");
pub fn main() void {
    const age: u32 = 30;
    var count: i32 = 0;
    count += 1;
    const name = "Ana"; // string literal, coerces to []const u8
    std.debug.print("{s} is {d}, count = {d}\n", .{ name, age, count });
    var maybe: ?i32 = null;
    maybe = 5;
    const value = maybe orelse 0;
    std.debug.print("value = {d}\n", .{value});
}

格式佔位符說明如何列印:{d} 用於數字,{s} 用於字串,{c} 把單一位元組當字元,{any} 用於除錯任何值。

流程控制

if、switch、while 與 for 都可以當成運算式使用。switch 必須涵蓋所有可能的值,所以通常要加上 else 分支。

const std = @import("std");
pub fn main() void {
    const n: i32 = -2;
    const sign: []const u8 = if (n > 0) "positive" else if (n < 0) "negative" else "zero";
    const score: u8 = 85;
    const letter: u8 = switch (score) {
        90...100 => 'A',
        80...89 => 'B',
        else => 'C',
    };
    std.debug.print("{s} {c}\n", .{ sign, letter }); // negative B
    var i: u32 = 0;
    while (i < 3) : (i += 1) {
        std.debug.print("{d} ", .{i}); // 0 1 2
    }
    const items = [_]u8{ 10, 20, 30 };
    for (items, 0..) |item, index| {
        std.debug.print("\n{d}: {d}", .{ index, item });
    }
    std.debug.print("\n", .{});
}

[_]u8{ ... } 讓編譯器自行計算陣列長度。for (items, 0..) 會同時走訪元素與一個計數器;直線符號之間的值稱為 capture。

函式與 comptime

函式要宣告參數的型別與回傳型別。引數在函式內是不可變的。泛型程式碼以 comptime 參數撰寫:型別是在編譯期存在的值,所以函式可以接收型別作為引數。

const std = @import("std");
fn add(a: i32, b: i32) i32 {
    return a + b;
}
fn max(comptime T: type, a: T, b: T) T {
    return if (a > b) a else b;
}
pub fn main() void {
    std.debug.print("{d}\n", .{add(2, 3)});            // 5
    std.debug.print("{d}\n", .{max(i32, 3, 7)});       // 7
    std.debug.print("{d}\n", .{max(f64, 1.5, 0.5)});   // 1.5
}

編譯器會針對每個呼叫時使用的型別,各建立一個獨立版本的 max。標準函式庫也用同樣的機制建立泛型資料結構:一個接收型別、並回傳新 struct 型別的函式。

使用 error union、try 與 defer 處理錯誤

錯誤是值。回傳型別 !u32 表示「一個 u32 或一個錯誤」。try expr 在有錯誤時會把錯誤回傳給呼叫者;catch 則當場處理。錯誤不能被默默忽略。

const std = @import("std");
fn parseAge(s: []const u8) !u32 {
    const n = try std.fmt.parseInt(i32, s, 10);
    if (n < 0) return error.Negative;
    return @intCast(n);
}
pub fn main() void {
    const age = parseAge("-5") catch |err| {
        std.debug.print("error: {s}\n", .{@errorName(err)});
        return;
    };
    std.debug.print("age {d}\n", .{age});
}
// error: Negative

defer 會在目前區塊結束時執行一個陳述式,而 errdefer 只在區塊因錯誤而結束時才執行。它們讓清理程式碼緊貼著取得資源的程式碼。

記憶體與 allocator

Zig 沒有垃圾回收,也沒有隱藏的全域 allocator。任何需要堆積記憶體的函式都會接收一個 std.mem.Allocator 參數,因此由呼叫者決定記憶體的來源(page allocator、arena、固定緩衝區,或是能偵測洩漏的除錯用 allocator)。

const std = @import("std");
pub fn main() !void {
    const allocator = std.heap.page_allocator;
    const buf = try allocator.alloc(u32, 5);
    defer allocator.free(buf);
    for (buf, 0..) |*slot, i| {
        slot.* = @intCast(i * i);
    }
    std.debug.print("{any}\n", .{buf}); // { 0, 1, 4, 9, 16 }
}

|*slot| 擷取每個元素的指標,讓迴圈可以寫入,slot.* 則對指標解參考。在 Debug 與 ReleaseSafe 建置中,整數溢位與索引越界會以清楚的 panic 停止程式,而不是造成未定義行為。

完整的小程式:數字平均值

const std = @import("std");
fn average(numbers: []const i64) ?f64 {
    if (numbers.len == 0) return null;
    var sum: i64 = 0;
    for (numbers) |n| sum += n;
    const total: f64 = @floatFromInt(sum);
    const len: f64 = @floatFromInt(numbers.len);
    return total / len;
}
pub fn main() !void {
    const allocator = std.heap.page_allocator;
    const buf = try allocator.alloc(i64, 16);
    defer allocator.free(buf);
    var it = std.mem.tokenizeScalar(u8, "4 8 15 16 23 42", ' ');
    var count: usize = 0;
    while (it.next()) |word| : (count += 1) {
        buf[count] = try std.fmt.parseInt(i64, word, 10);
    }
    if (average(buf[0..count])) |avg| {
        std.debug.print("count={d} avg={d:.2}\n", .{ count, avg });
    }
}
// count=6 avg=18.00

這個程式以空格切開字串,用 try 解析每一段,把數字存進由 defer 釋放的堆積記憶體,並回傳一個 optional 平均值,空串列時為 null。

接下來可以學什麼

  • ziglang.org/documentation 上的語言參考,請選擇與你安裝版本相符的頁面。
  • ziglang.org 的 Learn 區,包括「Getting Started」與語言概覽。
  • Ziglings 是一組小型的壞掉程式,要你逐一修好,是社群中很受歡迎的練習集。
  • 閱讀標準函式庫的原始碼:它是用 Zig 寫的,也是目前 API 最好的參考資料。

常見問題

Zig 準備好用於正式環境了嗎?

有些公司已在正式環境使用 Zig,但這門語言尚未到 1.0,因此升級編譯器時要預期會有破壞性變更,並預留時間處理。

Zig 是 C 或 Rust 的替代品嗎?

Zig 最接近 C:手動管理記憶體,但有更好的安全檢查、錯誤處理與編譯期程式碼。Rust 透過所有權系統與借用檢查器保證記憶體安全,這是 Zig 所沒有的。

Zig 可以使用 C 函式庫嗎?

可以。Zig 能匯入 C 標頭檔並直接呼叫 C 函式,Zig 編譯器也能建置 C 與 C++ 原始碼,因此混用這些語言相當簡單。

Zig 的 comptime 是什麼意思?

comptime 標記在編譯時求值的程式碼或參數。Zig 用它來處理泛型、常數表與編譯期檢查,而不是另外一套巨集語言。

全部編碼教學