cv.cm

Lập trình · Ngôn ngữ lập trình

Hướng dẫn học lập trình Zig cơ bản cho người mới

8 phút đọc

Trả lời nhanh

Zig là ngôn ngữ lập trình hệ thống đa dụng kèm bộ công cụ, được xem như một lựa chọn hiện đại thay cho C. Nó không có luồng điều khiển ẩn, không có bộ thu gom rác và không cấp phát bộ nhớ ngầm: bạn truyền allocator một cách tường minh, xử lý lỗi bằng error union và try, và dọn dẹp bằng defer. Việc thực thi mã lúc biên dịch (comptime) thay thế macro và generic. Chạy một tệp bằng zig run hello.zig. Zig vẫn đang ở trước phiên bản 1.0, nên chi tiết của ngôn ngữ và thư viện chuẩn thay đổi giữa các bản phát hành.

Zig là gì và ai dùng nó

Zig do Andrew Kelley tạo ra và được phát triển bởi tổ chức phi lợi nhuận Zig Software Foundation. Nó nhắm tới cùng loại công việc như C: thành phần hệ điều hành, firmware nhúng, công cụ làm game, cơ sở dữ liệu và công cụ dòng lệnh nhanh. Trình biên dịch Zig cũng biên dịch được mã C và C++ (zig cc) và biên dịch chéo sang nhiều nền tảng, nên một số dự án chỉ dùng nó như bộ công cụ C.

Các dự án nổi tiếng viết bằng Zig gồm môi trường chạy JavaScript Bun, cơ sở dữ liệu tài chính TigerBeetle và trình giả lập terminal Ghostty.

Cài Zig và chạy Hello, world

Tải về gói dựng sẵn cho hệ thống của bạn từ ziglang.org/download, giải nén và thêm thư mục vào PATH, hoặc cài Zig bằng trình quản lý gói của hệ thống. Không có trình cài đặt hay môi trường chạy riêng.

// 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 ghi ra đầu ra lỗi chuẩn và là cách đơn giản nhất để in trong mọi phiên bản gần đây. Đối số thứ hai là một tuple các giá trị cho các chỗ giữ chỗ định dạng; .{} là tuple rỗng. API ghi ra đầu ra chuẩn đã thay đổi giữa các bản phát hành, nên hãy xem tài liệu thư viện chuẩn của phiên bản bạn dùng.

Biến và các kiểu dữ liệu cơ bản

const khai báo một giá trị không bao giờ đổi và var khai báo một biến khả biến. Trình biên dịch rất nghiêm ngặt: biến cục bộ không dùng là lỗi, và var không bao giờ bị sửa cũng là lỗi, nên hãy mặc định dùng const.

KiểuVí dụGhi chú
Số nguyên có dấui8, i32, i64Cho phép mọi độ rộng, ví dụ i7
Số nguyên không dấuu8, u32, usizeusize có kích thước bằng con trỏ, dùng cho độ dài và chỉ số
Số thựcf32, f64IEEE 754
booltrue, falseKhông chuyển đổi ngầm từ số nguyên
Array[3]u8Độ dài cố định biết ngay lúc biên dịch
Slice[]const u8Con trỏ cộng độ dài; chuỗi là slice các byte
Optional?i32Một giá trị hoặc null
Error union!u32Một giá trị hoặc một lỗi
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});
}

Các chỗ giữ chỗ định dạng cho biết cách in: {d} cho số, {s} cho chuỗi, {c} cho một byte dưới dạng ký tự, {any} để gỡ lỗi mọi giá trị.

Điều khiển luồng

if, switch, while và for đều có thể dùng làm biểu thức. switch phải bao quát mọi giá trị có thể, nên thường bạn thêm một nhánh 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{ ... } để trình biên dịch tự đếm độ dài mảng. for (items, 0..) lặp qua các phần tử và một biến đếm cùng lúc; các giá trị giữa hai dấu gạch đứng gọi là capture.

Hàm và comptime

Hàm khai báo kiểu của tham số và kiểu trả về. Đối số là bất biến bên trong hàm. Mã generic được viết bằng tham số comptime: một kiểu là giá trị tồn tại lúc biên dịch, nên hàm có thể nhận một kiểu làm đối số.

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
}

Trình biên dịch tạo một phiên bản max riêng cho từng kiểu mà nó được gọi. Cơ chế tương tự được dùng để xây dựng các cấu trúc dữ liệu generic trong thư viện chuẩn: một hàm nhận một kiểu và trả về một kiểu struct mới.

Xử lý lỗi với error union, try và defer

Lỗi là giá trị. Kiểu trả về !u32 nghĩa là "một u32 hoặc một lỗi". try expr trả lỗi về cho bên gọi nếu có lỗi; catch xử lý ngay tại chỗ. Lỗi không thể bị bỏ qua một cách âm thầm.

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 chạy một câu lệnh khi khối hiện tại thoát, còn errdefer chỉ chạy khi khối thoát do lỗi. Chúng giữ mã dọn dẹp ngay cạnh mã đã lấy tài nguyên.

Bộ nhớ và allocator

Zig không có bộ thu gom rác và không có allocator toàn cục ẩn. Mọi hàm cần bộ nhớ heap đều nhận tham số std.mem.Allocator, nên bên gọi quyết định bộ nhớ đến từ đâu (page allocator, arena, bộ đệm cố định, hoặc allocator gỡ lỗi phát hiện rò rỉ).

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| bắt một con trỏ tới từng phần tử để vòng lặp có thể ghi vào nó, và slot.* giải tham chiếu con trỏ. Trong bản build Debug và ReleaseSafe, tràn số nguyên và truy cập chỉ số ngoài giới hạn sẽ dừng chương trình bằng một panic rõ ràng thay vì gây hành vi không xác định.

Một chương trình hoàn chỉnh nhỏ: trung bình các số

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

Chương trình tách một chuỗi theo dấu cách, phân tích từng phần bằng try, lưu các số trong bộ nhớ heap được giải phóng bằng defer, và trả về giá trị trung bình optional là null khi danh sách rỗng.

Học tiếp ở đâu

  • Tài liệu tham chiếu ngôn ngữ tại ziglang.org/documentation, hãy chọn trang đúng với phiên bản bạn đã cài.
  • Mục Learn của ziglang.org, gồm "Getting Started" và phần tổng quan về ngôn ngữ.
  • Ziglings, một bộ chương trình nhỏ bị hỏng để bạn sửa từng cái, là bộ bài tập cộng đồng phổ biến.
  • Đọc mã nguồn thư viện chuẩn: nó được viết bằng Zig và là tài liệu tham chiếu tốt nhất cho các API hiện hành.

Câu hỏi thường gặp

Zig đã sẵn sàng cho môi trường sản phẩm chưa?

Một số công ty dùng Zig trong sản phẩm, nhưng ngôn ngữ đang trước 1.0, nên bạn nên chuẩn bị cho các thay đổi gây lỗi khi nâng cấp trình biên dịch và dành thời gian cho việc đó.

Zig có thay thế C hay Rust không?

Zig gần với C nhất: quản lý bộ nhớ thủ công nhưng có kiểm tra an toàn tốt hơn, xử lý lỗi và mã lúc biên dịch. Rust đảm bảo an toàn bộ nhớ thông qua hệ thống ownership và borrow checker, những thứ Zig không có.

Zig có dùng được thư viện C không?

Có. Zig có thể import tệp header C và gọi trực tiếp hàm C, và trình biên dịch Zig có thể build tệp nguồn C và C++, nên việc trộn các ngôn ngữ rất đơn giản.

comptime nghĩa là gì trong Zig?

comptime đánh dấu mã hoặc tham số được đánh giá trong lúc biên dịch. Zig dùng nó cho generic, bảng hằng số và kiểm tra lúc biên dịch thay vì một ngôn ngữ macro riêng.

Tất cả hướng dẫn lập trình