热门问题
时间线
聊天
视角

Zig

来自维基百科,自由的百科全书

Zig
Remove ads

Zig(也稱為 Ziglang)[12] 是一種命令式通用靜態類型編譯語言系統編程語言,由 Andrew Kelley 設計。[13]它旨在作為 C 的繼任者,提供更輕量更簡單的編程體驗,同時提供更豐富的功能。[14] 它是自由及開放源代碼軟件,在MIT許可證下發布。

事实速览 編程範型, 設計者 ...

Zig 語言的簡化涉及到流控制函數調用導入、變量聲明Unicode 支持。然而,Zig 不使用 預處理 指令。Zig 從現代語言引入的特性包括 編譯時泛型編程數據類型,允許函數編譯時處理各種數據,並添加了一小套新的 編譯器 指令,以允許通過 反射編程 訪問和修改這些類型的信息。

Zig 的另一組新增功能旨在提高代碼安全性。像 C 一樣,Zig 沒有實現 垃圾回收,其 內存管理 是手動的。對於包括此種情況的錯誤處理,Zig 引入了 可選類型,他們的 語法 很簡單,而 Zig 內置於語言中的 單元測試 框架也使提前預知錯誤變得簡單和方便。

Remove ads

描述

目標

Zig 的主要目標是成為解決目前由 C 解決的各種任務的更好的解決方案。對此的主要關注點是可讀性;Zig 儘可能使用現有概念和語法,避免為相似概念添加不同的語法。此外,它的設計目標是「穩健性、最佳化和可維護性」,包括各種提高安全性、優化和測試的功能。簡潔的語法是維護的重要組成部分,因為該語言的目標是允許維護人員在不需要學習他們可能不熟悉的語言細節的情況下調試代碼。[15] 即使有這些更改,Zig 也可以編譯並與現有的 C 代碼一起使用;可以在 Zig 項目中包含 C 頭文件並調用它們的函數,並且,通過包含編譯器生成的頭文件,Zig 代碼可以被鏈接到 C 項目。[16]

為了保持整體設計理念的簡潔和易讀,Zig 系統整體相比於 C 及其他類似 C 的語言也包含了一些風格上的變化。例如,Rust 語言具有 運算符重載,這意味着類似 a = b + c 的語句實際上可能是對類型的重載版本的加運算符的函數調用。此外,該函數可能會引發 panic,從而可能中斷任何後續代碼。在 Zig 中,如果某個東西調用了一個函數,那麼它看起來就像一個函數調用;如果沒有調用,它看起來就不會像一個函數調用。如果它可能拋出錯誤,則一定會在代碼中顯式表示它可能拋出錯誤,[16] 而錯誤處理可以通過錯誤類型處理,也可以通過 catch 或 try 處理。

Zig 的目標與同時期設計的許多其他語言(如 GoRustCarbonNim)的目標形成對比。通常,這些語言更複雜,他們添加了運算符重載、看起來像值(屬性)調用的函數等許多功能,這些旨在幫助構建大型程序。並且這些特性更接近於 C++ 的特性,而這些語言也更像 C++。[16] Zig 在類型系統的擴展上更為保守,支持編譯時泛型,並通過 comptime 特性實現了一種 鴨子類型 形式的用法。

Remove ads

內存處理

C 程序中錯誤的主要來源之一是基於malloc內存管理系統。malloc 會為代碼使用分配一塊內存,並返回該內存的引用作為一個 指針,但C沒有一套系統來確保在程序不再需要內存時釋放該內存,這可能導致內存泄漏以至於耗費完所有的可用內存。更常見的是懸空指針,這些指針引用未正確分配的內存地址。[17]

解決這些問題的常見方法是垃圾回收(GC),它會檢查程序中指向先前分配過內存的指針,並刪除所有的不再有指針指向它們的內存塊。儘管這在很大程度上減少甚至消除了內存錯誤,但GC系統的速度相對較慢,[來源請求],並且具有不可預測的性能,使其不適用於系統編程。另一種解決方案是自動引用計數(ARC),其通過維護指向一個內存塊的指針數,並在指針創建和銷毀時更新計數來實現基本相同的功能,這意味着其不需要執行詳盡的指針搜索,而僅增加每個指針在執行創建和銷毀操作時更新引用計數器的開銷。[17]

Zig 旨在提供與 C 相似或更好的性能,因此 GC 和 ARC 不是合適的解決方案。相反,它使用一種現代的(截至 2022 年),被稱為可選類型的概念。與指針可以指向空值或 nil 不同,這種方案使用一個單獨的類型來指示可能為空的數據。這類似於使用一個指針和一個布爾值的結構來指示指針是否有效,但布爾值的狀態由語言隱式管理,不需要程序員顯式管理。因此,例如,當聲明指針時,它被設置為「未分配」,而當該指針從 malloc 接收一個值時,如果 malloc 成功,它將被設置為「已分配」。[18]

這種模型的優點是它具有非常低甚至是零的開銷;儘管這使得編譯器必須創建在操作指針時傳遞可選類型,而不是一個簡單的指針的代碼,但這允許它在編譯時直接檢查出可能的內存問題,而無需延後到運行時檢查。例如,在 C 中創建一個具有空值的指針並嘗試使用它是完全可以接受的,儘管這會導致空指針錯誤。相比之下,使用可選類型的語言可以保證所有代碼只會在指針有效時嘗試使用相應的指針。雖然這不能消除所有潛在問題,但當在運行時發生問題時,錯誤可以被更精確地定位和解釋。[19]

Zig 中內存管理的另一變化是:實際分配操作是通過 struct 描述相應的操作來處理的,而不是直接調用 libc 中的內存管理函數。例如,在 C 中,如果想寫一個包含多個副本的字符串的函數,該函數可能如下所示:

const char* repeat(const char* original, size_t times);

在代碼中,該函數會檢查 original 的大小,然後 malloc times 的內存來為它將構建的字符串預留空間。但這個 malloc 對調用它的函數是不可見的,所以,如果像 repeat 這樣的函數沒有正常釋放內存,就會發生內存泄漏。而在 Zig 中,這種操作可能通過如下函數處理:

fn repeat(allocator: *std.mem.Allocator, original: []const u8, times: usize) std.mem.Allocator.Error![]const u8;

在此代碼中,allocator 變量傳遞了一個內存分配器,並且 repeat 函數則返回結果字符串或 Allocator.Error。通過直接將分配器作為輸入傳參,使得內存分配不會被「隱藏」起來,即調用鏈上的所有內存分配都會通過這個分配接口進行分配。順帶一提,Zig 的標準庫內沒有進行任何隱式內存分配。此外,由於結構可以指向任何東西,可以使用替代分配器,甚至是程序中編寫的分配器。這使不使用通常分配整個內存頁的操作系統函數的小對象分配器等操作成為了可能。[20]

可選類型是一個提供通用功能但仍然簡單和通用的語言特性。它不僅可以用於解決空指針問題,還可以明確地描述「無值」場景。考慮一個名為 countTheNumberOfUsers 的函數,它返回一個整數,以及一個整數變量,theCountedUsers,它保存結果。在許多語言中,會在 theCountedUsers 中放置一個 魔術數字 以表示 countTheNumberOfUsers 尚未被調用,而許多實現則會將其設置為零。在 Zig 中,這可以實現為 var theCountedUsers: ?i32 = null,將變量設置為明確的「未被調用」的值。[20]

Zig 另一個有助於管理內存問題的更通用特性是 defer 的概念,由它標記的代碼在函數結束時無論如何都會被執行,包括可能的運行時錯誤。如果某個特定函數分配了一些內存,然後在操作完成時釋放它,可以添加一行代碼,推遲一個 free,以確保無論發生什麼都會釋放它。[20] 但需要注意的是,defer 並不完全等價於 RAII,在某些場景下,會有許多區別之處。[21]

Zig 內存管理避免了隱式分配。即分配不直接由語言本身進行管理。相反,用戶需要通過 標準庫 明確地進行堆訪問來進行顯式內存管理。[22]

Remove ads

與 C 的直接交互

Zig 提倡一種將新 Zig 代碼與現有 C 代碼結合的漸進方法。為此,它可以儘可能無縫地與現有 C 庫進行交互。Zig 使用 @import 指令導入庫,通常如下所示:

const std = @import("std");

如此便可以調用 std 內的函數,例如:

std.debug.print("Hello, world!\n", .{});

要加載 C 代碼,只需將 @import 替換為 @cImport

const c = @cImport(@cInclude("soundio/soundio.h"));

然後我們就可以像調用本地 Zig 代碼一樣調用 soundio 庫中的函數。由於 Zig 使用新數據類型,它們是顯式定義的,不像 C 的更通用的 intfloat,我們需要使用少量指令在 C 和 Zig 類型之間移動數據,包括 @intCast@ptrCast[20]

Remove ads

交叉編譯

Zig 將交叉編譯視為語言的一級用例。這意味着任何 Zig 編譯器都可以為其目標平台之一生成可運行的二進制文件,這些平台包括數十種。這些不僅包括廣泛使用的現代系統,如 ARMx86-64,還包括 PowerPCSPARCMIPSRISC-V 甚至 IBM 的 z/Architectures (S390)。工具鏈可以編譯到這些目標中的任何一個,而無需安裝額外的軟件,因為所有需要的支持都在基本系統中。[20]

編譯時計算

通過使用 comptime 關鍵字,程序員可以顯式地在 編譯時 而不是 運行時 評估代碼段。能夠在編譯時運行代碼使得 Zig 具有 條件編譯 的功能,而無需單獨的 預處理器 語言。[23]

在編譯時,類型成為 一級公民。這使得在編譯時 鴨子類型 成為可能,這也是 Zig 實現泛型類型的方式。[24]

例如,在 Zig 中,一個泛型的 鍊表 類型可以使用如下函數實現:

fn LinkedList(comptime T: type) type;

這個函數接收某種類型 T,並返回一個自定義的 struct,定義了包含該數據類型的鍊表。

名稱的由來

據報道,「Zig」這個名字是通過一個涉及 Python 腳本的過程選出的,該腳本隨機組合字母,從字母「Z」開始,然後跟隨一個元音或「Y」,以生成四個字母的單詞。儘管目標長度是四個字母,但在生成的各種組合中,最終選擇了三個字母的單詞「Zig」。[25]

其他特性

Zig 支持 編譯時 泛型編程反射編程 和評估、交叉編譯 以及 手動內存管理[26] 該語言的一個主要目標是改進 C 語言[23][27] 同時也從 Rust 等語言中汲取靈感,[28][16] 等等。Zig 具有許多用於 低級編程語言 的特性,特別是緊湊結構(沒有字段之間的填充的結構)、任意寬度的整數[29] 和多種指針類型。[24]

Zig 不僅僅是一種新語言:它還包括一個 C/C++ 編譯器,可以與這兩種語言一起使用。

缺點

Zig 有一些缺點。如果內存沒有正確釋放,由於缺乏隱式控制,可能會因為忘記 defer 等必要的手動操作導致內存泄漏。[30] 對於那些不熟悉低級編程概念的人來說,學習 Zig 的曲線可能很陡峭。[30] 儘管 Zig 社區在不斷壯大,但截至 2024 年,它仍然是一個新的語言,在成熟度、生態系統和工具方面有待改進。[30] 與其他語言的互操作性可能會帶來挑戰,因為這通常需要額外的努力來管理數據編組和通信。[30] 最後,複雜用例的學習資源有限,儘管隨着興趣和採用的增加,這種情況正在逐漸改善。[30]

版本

自 0.10 版本以來,(新的默認)Zig 編譯器是用 Zig 編程語言編寫的,即它是一個 自舉編譯器,這是該版本的一個重大新特性。舊的遺留 自舉 編譯器是用 C++ 編寫的,仍然是一個選項,但在 0.11 版本中將不再是選項。當使用新的 Zig 編譯器進行編譯時,使用的內存要少得多,並且編譯速度稍快。舊的、現在的遺留 C++ 編譯器使用了 3.5 倍的內存。

Zig 的默認優化後端仍然是 LLVM[31] 並且 LLVM 是用 C++ 編寫的。帶有 LLVM 的 Zig 編譯器是 169 MiB[需要解釋],而不帶 LLVM 的則是 4.4 MiB。通常情況下,使用新的基於 Zig 語言的編譯器編譯的可執行代碼更快,其 LLVM 代碼生成更好,並修復了許多錯誤,但在 0.10 版本中也對舊的遺留編譯器進行了改進。自託管鏈接器與自託管編譯器緊密耦合。新版本還增加了一些對 AMD GPU 的實驗性(第 3 層)支持(源代碼中還有對 Nvidia GPU 和 PlayStation 4 和 5 的一些較少支持)。

舊的 自舉 ("stage1") 編譯器是用 Zig 和 C++ 編寫的,使用 LLVM 13 作為後端,[32][33] 支持其許多本機目標。[34] 編譯器也是 自由及開放源代碼軟件,在 MIT 許可證 下發布。[35] Zig 編譯器提供了類似於 Clang 的編譯 C 和 C++ 的能力,命令為 zig cczig c++[36] 提供了包括 C 標準庫 (libc) 和 C++ 標準庫 (libcxx) 在內的許多不同平台的頭文件,允許 Zig 的 ccc++ 子命令作為開箱即用的 交叉編譯器[37][38]

此外,官方支持(並記錄)的操作系統(主要是桌面操作系統)上可以製作(最小)應用程序,這些應用程序也可以為 Android(使用 Android NDK)和 iOS 編程。

Remove ads

包管理

在0.11.0版本之前,Zig沒有內置的包管理器,但在0.11.0版本中包含了一個實驗性的包管理器,並在0.12.0中進一步擴展。

Zig沒有官方的包倉庫;相反,包只是一個指向歸檔文件Git倉庫的URL。理想情況下,當解壓縮時,包括一個標準的build.zig文件(Zig 編譯器按慣例使用該文件來編譯源代碼)和一個build.zig.zon文件,用於定義包的名稱和版本。

Zig 的開發由 Zig 軟件基金會(ZSF)資助,ZSF 是一個由 Andrew Kelley 擔任總裁的非營利公司,接受捐贈並僱傭多名全職員工。[39][40][41]

示例

Hello World

const std = @import("std");

pub fn main() !void {
    const stdout = std.io.getStdOut().writer();
    try stdout.print("Hello, {s}!\n", .{"world"});
}

泛型鍊表

const std = @import("std");
const stdout = std.io.getStdOut().writer();

fn LinkedList(comptime T: type) type {
    return struct {
        const Self = @This();
        pub const Node = struct {
            next: ?*Node = null,
            data: T,
        };

        first: ?*Node = null,

        pub fn prepend(
            list: *Self,
            new_node: *Node,
        ) void {
            new_node.next = list.first;
            list.first = new_node;
        }
        pub fn format(
            list: Self,
            comptime fmt: []const u8,
            options: std.fmt.FormatOptions,
            out_stream: anytype,
        ) !void {
            try out_stream.writeAll("( ");
            var it = list.first;
            while (it) |node| : (it = node.next) {
                try std.fmt.formatType(
                    node.data,
                    fmt,
                    options,
                    out_stream,
                    1,
                );
                try out_stream.writeAll(" ");
            }
            try out_stream.writeAll(")");
        }
    };
}

pub fn main() !void {
    const ListU32 = LinkedList(u32);
    var list = ListU32{};
    var node1 = ListU32.Node{ .data = 1 };
    var node2 = ListU32.Node{ .data = 2 };
    var node3 = ListU32.Node{ .data = 3 };
    list.prepend(&node1);
    list.prepend(&node2);
    list.prepend(&node3);
    try stdout.print("{}\n", .{list});
    try stdout.print("{b}\n", .{list});
}
  • 輸出
    ( 3 2 1 )
    ( 11 10 1 )
    

帶分配器的字符串重複

const std = @import("std");

fn repeat(
    allocator: *std.mem.Allocator,
    original: []const u8,
    times: usize,
) std.mem.Allocator.Error![]const u8 {
    var buffer = try allocator.alloc(
        u8,
        original.len * times,
    );

    for (0..times) |i| {
        std.mem.copyForwards(
            u8,
            buffer[(original.len * i)..],
            original,
        );
    }

    return buffer;
}

pub fn main() !void {
    const stdout = std.io.getStdOut().writer();

    var arena = std.heap.ArenaAllocator.init(
        std.heap.page_allocator,
    );
    defer arena.deinit();

    var allocator = arena.allocator();

    const original = "Hello ";
    const repeated = try repeat(
        &allocator,
        original,
        3,
    );

    // 输出 "Hello Hello Hello "
    try stdout.print("{s}\n", .{repeated});
}
  • 輸出
    Hello Hello Hello
    

社區

Zig 軟件基金會(ZSF)有一個非常活躍的貢獻者社區,並且仍處於早期發展階段。[42] 儘管如此,2024 年的一項 Stack Overflow 調查發現,Zig 軟件開發人員的平均年薪為 103,000 美元,使其成為薪資最高的編程語言之一。[43] 然而,只有 0.83% 的受訪者表示他們精通 Zig。[42]

項目

參見

參考文獻

外部連結

Loading related searches...

Wikiwand - on

Seamless Wikipedia browsing. On steroids.

Remove ads