ziglang 30 分鐘速成

這份 zig 簡明教程適合已經有編程基礎知識的同學快速瞭解 zig 語言,同時也適合沒有編程經驗但是懂得善用搜索引擎的同學, 該文章詳細介紹 Zig 編程語言各種概念,主要包括基礎知識、函數、結構體、枚舉、數組、切片、控制結構、錯誤處理、指針、元編程和堆管理等內容。

文|zouyee

基本用法

命令 zig run my_code.zig 將編譯並立即運行你的 Zig 程序。每個單元格都包含一個 Zig 程序,你可以嘗試運行它們(其中一些包含編譯時錯誤,可以註釋掉後再嘗試)。

首先聲明一個 main() 函數來運行代碼。

下面的代碼什麼都不會做,只是簡單的示例:

// comments look like this and go to the end of the line
pub fn main() void {}

可以使用內置函數 @import 導入標準庫,並將命名空間賦值給一個 const 值。Zig 中的幾乎所有東西都必須明確地被賦予標識符。你也可以通過這種方式導入其他 Zig 文件,類似地,你可以使用 @cImport 導入 C 文件。

const std = @import("std");
pub fn main() void {
    std.debug.print("hello world!\n", .{});
}

注意:後續會在結構部分部分解釋 print 語句中的第二個參數。

一般用 var 來聲明變量,同時在大多數情況下,需要帶上聲明變量類型。

const std = @import("std");
pub fn main() void {
    var x: i32 = 47; // declares "x" of type i32 to be 47.
    std.debug.print("x: {}\n", .{x});
}

const 聲明一個變量的值是不可變的。

pub fn main() void {
    const x: i32 = 47;
    x = 42; // error: cannot assign to constant
}

Zig 非常嚴苛,不允許你從外部作用域屏蔽標識符,以防止混淆:

const x: i32 = 47;
pub fn main() void {
    var x: i32 = 42;  // error: redefinition of 'x'
}

全局作用域的常量默認爲編譯時的 “comptime” 值,如果省略了類型,它們就是編譯時類型,並且可以在運行時轉換爲運行時類型。

const x: i32 = 47;
const y = -47;  // comptime integer.
pub fn main() void {
    var a: i32 = y; // comptime constant coerced into correct type
    var b: i64 = y; // comptime constant coerced into correct type
    var c: u32 = y; // error: cannot cast negative value -47 to unsigned integer
}

如果希望在後面設置它,也可以明確選擇將其保留爲未定義。如果你在調試模式下意外使用它引發錯誤,Zig 將使用 0XAA 字節填充一個虛擬值,以幫助檢測錯誤。

const std = @import("std");
pub fn main() void {
  var x: i32 = undefined;
  std.debug.print("undefined: {}\n", .{x});
}

在某些情況下,如果 Zig 可以推斷出類型信息,才允許你省略類型信息。

const std = @import("std");
pub fn main() void {
    var x: i32 = 47;
    var y: i32 = 47;
    var z = x + y; // declares z and sets it to 94.
    std.debug.print("z: {}\n", .{z});
}

但是需要注意,整數字面值是編譯時類型,所以下面的示例是行不通的:

pub fn main() void {
    var x = 47; // error: variable of type 'comptime_int' must be const or comptime
}

函數與結構體

函數

函數可以帶參數和返回值,使用 fn 關鍵字聲明。pub 關鍵字表示函數可以從當前作用域導出,使其它地方可以調用。下面示例是一個不返回任何值的函數(foo)。pub 關鍵字表示該函數可以從當前作用域導出,這就是爲什麼 main 函數必須是 pub 的。你可以像大多數編程語言中一樣調用函數:

const std = @import("std");
fn foo() void {
    std.debug.print("foo!\n", .{});
    //optional:
    return;
}
pub fn main() void {
    foo();
}

下面示例是一個返回整數值的函數:

const std = @import("std");
fn foo() i32 {
    return 47;
}
pub fn main() void {
    var result = foo();
    std.debug.print("foo: {}\n", .{result});
}

Zig 不允許你忽略函數的返回值:

fn foo() i32 {
    return 47;
}
pub fn main() void {
    foo(); // error: expression value is ignored
}

但是你可以將其賦值給丟棄變量 _

fn foo() i32 {
    return 47;
}
pub fn main() void {
  _ = foo();
}

也可以聲明函數時帶上參數的類型,這樣函數調用時可以傳入參數:

const std = @import("std");
fn foo(x: i32) void {
    std.debug.print("foo param: {}\n", .{x});
}
pub fn main() void {
    foo(47);
}

結構體

結構體通過使用 const 關鍵字分配一個名稱來聲明,它們的賦值順序可以是任意的,並且可以使用常規的點語法進行解引用。

const std = @import("std");
const Vec2 = struct {
    x: f64,
    y: f64
};
pub fn main() void {
    var v = Vec2{.y = 1.0, .x = 2.0};
    std.debug.print("v: {}\n", .{v});
}

結構體可以有默認值;結構體也可以是匿名的,並且可以強制轉換爲另一個結構體,只要所有的值都能確定:

const std = @import("std");
const Vec3 = struct{
    x: f64 = 0.0,
    y: f64,
    z: f64
};
pub fn main() void {
    var v: Vec3 = .{.y = 0.1, .z = 0.2};  // ok
    var w: Vec3 = .{.y = 0.1}; // error: missing field: 'z'
    std.debug.print("v: {}\n", .{v});
}

可以將函數放入結構體中,使其像面向對象編程中的對象一樣工作。這裏有一個語法糖,如果你定義的函數的第一個參數爲對象的指針,我們稱之爲” 面向對象編程”,類似於 Python 帶 self 參數的函數。一般約定是通過將變量命名爲 self 來表示。

const std = @import("std");
const LikeAnObject = struct{
    value: i32,
    fn print(self: *LikeAnObject) void {
        std.debug.print("value: {}\n", .{self.value});
    }
};
pub fn main() void {
    var obj = LikeAnObject{.value = 47};
    obj.print();
}

我們一直傳遞給 std.debug.print 的第二個參數是一個元組,它是一個帶有數字字段的匿名結構體。在編譯時,std.debug.print 會找出元組中參數的類型,並生成一個針對你提供的參數字符串的版本,這就是爲何 Zig 知道如何將打印的內容變得漂亮的原因。

const std = @import("std");
pub fn main() void {
    std.debug.print("{}\n", .{1, 2}); #  error: Unused arguments
}

枚舉、數組與切片

枚舉

枚舉通過使用 const 關鍵字將枚舉組以類型方式來聲明。

注意:在某些情況下,可以簡化枚舉的名稱。其可以將枚舉的值設置爲整數,但它不會自動強制轉換,你必須使用 @enumToInt 或 @intToEnum 來進行轉換。

const std = @import("std");
const EnumType = enum{
    EnumOne,
    EnumTwo,
    EnumThree = 3
};
pub fn main() void {
    std.debug.print("One: {}\n", .{EnumType.EnumOne});
    std.debug.print("Two?: {}\n", .{EnumType.EnumTwo == .EnumTwo});
    std.debug.print("Three?: {}\n", .{@enumToInt(EnumType.EnumThree) == 3});
}

數組

Zig 有數組概念,它們是具有在編譯時已知長度的連續內存。你可以通過在前面聲明類型並提供值列表來初始化它們,同時可以通過數組的 len 字段訪問它們的長度。

注意:Zig 中的數組也是從零開始索引的。

const std = @import("std");
pub fn main() void {
    var array: [3]u32 = [3]u32{47, 47, 47};
    // also valid:
    // var array = [_]u32{47, 47, 47};
    var invalid = array[4]; // error: index 4 outside array of size 3.
    std.debug.print("array[0]: {}\n", .{array[0]});
    std.debug.print("length: {}\n", .{array.len});
}

切片

跟 golang 類似,Zig 也有切片(slices),它們的長度在運行時已知。你可以使用切片操作從數組或其他切片構造切片。與數組類似,切片有一個 len 字段,告訴它的長度。

注意:切片操作中的間隔參數是開口的(不包含在內)。嘗試訪問超出切片範圍的元素會引發運行時 panic。

const std = @import("std");
pub fn main() void {
    var array: [3]u32 = [_]u32{47, 47, 47};
    var slice: []u32 = array[0..2];
    // also valid:
    // var slice = array[0..2];
    var invalid = slice[3]; // panic: index out of bounds
    std.debug.print("slice[0]: {}\n", .{slice[0]});
    std.debug.print("length: {}\n", .{slice.len});
}

字符串文字是以 null 結尾的 utf-8 編碼的 const u8 字節數組。Unicode 字符只允許在字符串文字和註釋中使用。

注意:長度不包括 null 終止符(官方稱爲”sentinel termination”)。訪問 null 終止符是安全的。索引是按字節而不是 Unicode 字符。

const std = @import("std");
const string = "hello 世界";
const world = "world";
pub fn main() void {
    var slice: []const u8 = string[0..5];
    std.debug.print("string {}\n", .{string});
    std.debug.print("length {}\n", .{world.len});
    std.debug.print("null {}\n", .{world[5]});
    std.debug.print("slice {}\n", .{slice});
    std.debug.print("huh? {}\n", .{string[0..7]});
}

const 數組可以強制轉換爲 const 切片。

const std = @import("std");
fn foo() []const u8 {  // note function returns a slice
    return "foo";      // but this is a const array.
}
pub fn main() void {
    std.debug.print("foo: {}\n", .{foo()});
}

流程控制與錯誤處理

流程控制

Zig 提供了與其他語言類似的 if 語句、switch 語句、for 循環和 while 循環。示例:

const std = @import("std");
fn foo(v: i32) []const u8 {
    if (v < 0) {
        return "negative";
    }
    else {
        return "non-negative";
    }
}
pub fn main() void {
    std.debug.print("positive {}\n", .{foo(47)});
    std.debug.print("negative {}\n", .{foo(-47)});
}

switch

const std = @import("std");
fn foo(v: i32) []const u8 {
    switch (v) {
        0 => return "zero",
        else => return "nonzero"
    }
}
pub fn main() void {
    std.debug.print("47 {}\n", .{foo(47)});
    std.debug.print("0 {}\n", .{foo(0)});
}

for-loop

const std = @import("std");
pub fn main() void {
    var array = [_]i32{47, 48, 49};
    for (array) | value | {
        std.debug.print("array {}\n", .{value});
    }
    for (array) | value, index | {
        std.debug.print("array {}:{}\n", .{index, value});
    }
    var slice = array[0..2];
    for (slice) | value | {
        std.debug.print("slice {}\n", .{value});
    }
    for (slice) | value, index | {
        std.debug.print("slice {}:{}\n", .{index, value});
    }
}

while loop

const std = @import("std");
pub fn main() void {
    var array = [_]i32{47, 48, 49};
    var index: u32 = 0;
    while (index < 2) {
        std.debug.print("value: {}\n", .{array[index]});
        index += 1;
    }
}

錯誤處理

錯誤是特殊的聯合類型,你可以在函數前面加上 ! 來表示該函數可能返回錯誤。你可以通過簡單地將錯誤作爲正常返回值返回來拋出錯誤。

const MyError = error{
    GenericError,
    OtherError
};
pub fn main() !void {
    return MyError.GenericError;
}
fn wrap_foo(v: i32) void {
    if (foo(v)) |value| {
        std.debug.print("value: {}\n", .{value});
    } else |err| {
        std.debug.print("error: {}\n", .{err});
    }
}

如果你編寫一個可能出錯的函數,當它返回時你必須決定如何處理錯誤。兩個常見的選擇是 try 和 catch。try 方式很擺爛,它只是簡單地將錯誤轉發爲函數的錯誤。而 catch 需要處理錯誤。

try 其實就是 catch | err | {return err} 的語法糖。

const std = @import("std");
const MyError = error{
    GenericError
};
fn foo(v: i32) !i32 {
    if (v == 42) return MyError.GenericError;
    return v;
}
pub fn main() !void {
    // catch traps and handles errors bubbling up
    _ = foo(42) catch |err| {
        std.debug.print("error: {}\n", .{err});
    };
    // try won't get activated here.
    std.debug.print("foo: {}\n", .{try foo(47)});
    // this will ultimately cause main to print an error trace and return nonzero
    _ = try foo(42);
}

我們也可以使用 if 來檢查錯誤。

const std = @import("std");
const MyError = error{
    GenericError
};
fn foo(v: i32) !i32 {
    if (v == 42) return MyError.GenericError;
    return v;
}
// note that it is safe for wrap_foo to not have an error ! because
// we handle ALL cases and don't return errors.
fn wrap_foo(v: i32) void {    
    if (foo(v)) | value | {
        std.debug.print("value: {}\n", .{value});
    } else | err | {
        std.debug.print("error: {}\n", .{err});
    }
}
pub fn main() void {
    wrap_foo(42);
    wrap_foo(47);
}

指針

Zig 使用 * 表示指針類型,可以通過.* 語法訪問指針指向的值。示例:

const std = @import("std");
pub fn printer(value: *i32) void {
    std.debug.print("pointer: {}\n", .{value});
    std.debug.print("value: {}\n", .{value.*});
}
pub fn main() void {
    var value: i32 = 47;
    printer(&value);
}

注意:在 Zig 中,指針需要正確對齊到它所指向的值的對齊方式。 對於結構體,類似於 Java,您可以解引用指針並一次獲取字段,使用 . 運算符。需要注意的是,這僅適用於一層間接引用,因此如果您有指向指針的指針,您必須首先解引用外部指針。

const std = @import("std");
const MyStruct = struct {
    value: i32
};
pub fn printer(s: *MyStruct) void {
    std.debug.print("value: {}\n", .{s.value});
}
pub fn main() void {
    var value = MyStruct{.value = 47};
    printer(&value);
}

Zig 允許任何類型(不僅僅是指針)可爲空,但請注意它們是基本類型和特殊值 null 的聯合體。要訪問未包裝的可選類型,請使用 .? 字段:

const std = @import("std");
pub fn main() void {
    var value: i32 = 47;
    var vptr: ?*i32 = &value;
    var throwaway1: ?*i32 = null;
    var throwaway2: *i32 = null; // error: expected type '*i32', found '(null)'
    std.debug.print("value: {}\n", .{vptr.*}); // error: attempt to dereference non-pointer type
    std.debug.print("value: {}\n", .{vptr.?.*});
}

注意:當我們使用來自 C ABI 函數的指針時,它們會自動轉換爲可爲空指針。 獲得未包裝的可選指針的另一種方法是使用 if 語句:

const std = @import("std");
fn nullChoice(value: ?*i32) void {
    if (value) | v | {
        std.debug.print("value: {}\n", .{v.*});
    } else {
        std.debug.print("null!\n", .{});
    }
}
pub fn main() void {
    var value: i32 = 47;
    var vptr1: ?*i32 = &value;
    var vptr2: ?*i32 = null;
    nullChoice(vptr1);
    nullChoice(vptr2);
}

元編程

Zig 的元編程受幾個基本概念驅動:

下面是元編程的一個示例:

const std = @import("std");
fn foo(x : anytype) @TypeOf(x) {
    // note that this if statement happens at compile-time, not runtime.
    if (@TypeOf(x) == i64) {
        return x + 2;
    } else {
        return 2 * x;
    }
}
pub fn main() void {
    var x: i64 = 47;
    var y: i32 =  47;
    std.debug.print("i64-foo: {}\n", .{foo(x)});
    std.debug.print("i32-foo: {}\n", .{foo(y)});
}

以下是泛型類型的一個示例:

const std = @import("std");
fn Vec2Of(comptime T: type) type {
    return struct{
        x: T,
        y: T
    };
}
const V2i64 = Vec2Of(i64);
const V2f64 = Vec2Of(f64);
pub fn main() void {
    var vi = V2i64{.x = 47, .y = 47};
    var vf = V2f64{.x = 47.0, .y = 47.0};
    std.debug.print("i64 vector: {}\n", .{vi});
    std.debug.print("f64 vector: {}\n", .{vf});
}

通過這些概念,我們可以構建非常強大的泛型類型!

堆管理

Zig 爲我們提供了與堆交互的多種方式,通常要求您明確選擇使用哪種方式。它們都遵循下述相同的模式:

  1. 創建一個分配器工廠結構體。

  2. 檢索由分配器工廠創建的 std.mem.Allocator 結構體。

  3. 使用 alloc/free 和 create/destroy 函數來操作堆。

  4. (可選)銷燬分配器工廠。

這麼處理的目的是:

好的,但是你也可以偷點懶。你是不是想一直使用 jemalloc? 只需選擇一個全局分配器,並在所有地方使用它(請注意,某些分配器是線程安全的,而某些則不是)。

在這個示例中,我們將使用 std.heap.GeneralPurposeAllocator 工廠創建一個具有多種特性(包括泄漏檢測)的分配器,並看看它是如何組合在一起的。

最後一件事,這裏使用了 defer 關鍵字,它非常類似於 Go 語言中的 defer 關鍵字!還有一個 errdefer 關鍵字,如果需要了解更多信息,請查閱 Zig 文檔。

const std = @import("std");
// factory type
const Gpa = std.heap.GeneralPurposeAllocator(.{});
pub fn main() !void {
    // instantiates the factory
    var gpa = Gpa{};
    // retrieves the created allocator.
    var galloc = &gpa.allocator;
    // scopes the lifetime of the allocator to this function and
    // performs cleanup; 
    defer _ = gpa.deinit();
    var slice = try galloc.alloc(i32, 2);
    // uncomment to remove memory leak warning
    // defer galloc.free(slice);
    var single = try galloc.create(i32);
    // defer gallo.destroy(single);
    slice[0] = 47;
    slice[1] = 48;
    single.* = 49;
    std.debug.print("slice: [{}, {}]\n", .{slice[0], slice[1]});
    std.debug.print("single: {}\n", .{single.*});
}

總結

現在我們已經掌握了相當大的 Zig 基礎知識。沒有覆蓋的一些(非常重要的)內容包括:

如果想要了解更多細節,請查閱最新的文檔。

由於筆者時間、視野、認知有限,本文難免出現錯誤、疏漏等問題,期待各位讀者朋友、業界專家指正交流。

參考文獻

1.https://ziglang.org/documentation/master/.

2.https://gist.github.com/ityonemo/769532c2017ed9143f3571e5ac104e50

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/DmCh2eIffJg2mZA-CQermw