0
0
Lập trình
TT

Xử lý lỗi trong Zig: Cách đơn giản và hiệu quả

Đăng vào 8 tháng trước

• 7 phút đọc

Chủ đề:

KungFuTech

Giữ cho mọi thứ đơn giản

Không có gì bí mật khi tôi rất thích Zig và đã sử dụng nó nhiều gần đây. Một trong những tính năng đặc biệt và kỳ quặc của nó là cách xử lý lỗi. Zig có một cách tiếp cận độc đáo đối với việc xử lý lỗi, khác với nhiều ngôn ngữ lập trình khác. Trong bài viết này, tôi sẽ giải thích cách thức hoạt động của xử lý lỗi trong Zig và tại sao nó rất phù hợp với mô hình tư duy lập trình của tôi.

Tập hợp lỗi là gì?

Trong Zig, lỗi được đại diện dưới dạng một loại đặc biệt gọi là tập hợp lỗi. Một tập hợp lỗi về cơ bản là một enum có thể chứa nhiều giá trị lỗi của những lỗi có thể mà một hàm có thể trả về. Ví dụ, một hàm đọc tệp có thể trả về một tập hợp lỗi chứa các lỗi như "tệp không tìm thấy", "quyền truy cập bị từ chối", và "lỗi đọc".

Ví dụ đơn giản

zig Copy
const std = @import("std");

const FileError = error{
    FileNotFound,
    PermissionDenied,
    ReadError,
};

// Một hàm đọc tệp và trả về một byte slice hoặc một FileError
fn readFile(path: []const u8) FileError![]u8 {
    // Ví dụ: luôn trả về lỗi để minh họa
    return FileError.FileNotFound;
}

Đơn giản, rõ ràng và đi thẳng vào vấn đề. Hàm readFile trả về một kiểu kết quả có thể là một byte slice (nội dung tệp) hoặc một lỗi từ tập hợp FileError.

Nhưng cái ký hiệu ! trong kiểu trả về là gì?

Ký hiệu ! trong Zig được sử dụng để chỉ ra rằng một hàm có thể trả về một lỗi. Trong trường hợp này, nó thực sự là điều duy nhất mà nó làm trong ví dụ trên.

Miễn là enum của bạn được định nghĩa như một tập hợp lỗi, nó là một kiểu trả về hợp lệ cho một hàm có tiền tố kiểu trả về với !.

Mẹo: Bạn cũng có thể sử dụng !void như một kiểu trả về cho các hàm không trả về giá trị nào nhưng vẫn có thể thất bại.

Mẹo #2: Bạn có thể hợp nhất các tập hợp bằng || nếu hàm của bạn xử lý nhiều nguồn lỗi. Chúng ta sẽ xem xét điều đó ngay bây giờ.

Liên minh lỗi

Các hàm có thể trả về một giá trị hoặc lỗi. Đó là !T nơi T là kiểu trả về. Đây được gọi là liên minh lỗi.

Bạn có thể sử dụng nó như sau:

zig Copy
fn readNumber(str: []const u8) !u32 {
    if (str.len == 0) return error.EmptyString;
    return std.fmt.parseInt(u32, str, 10);
}

const num = try readNumber("123"); // lỗi sẽ tràn lên
const bad = readNumber("abc") catch |err| {
    std.debug.print("Caught: {}\n", .{err});
    return err;
};

Trong ví dụ này, readNumber cố gắng phân tích một chuỗi thành một số. Nếu chuỗi rỗng, nó trả về một lỗi EmptyString. Nếu việc phân tích thất bại, nó trả về bất kỳ lỗi nào mà std.fmt.parseInt tạo ra.

Từ khóa try được sử dụng để truyền đạt lỗi lên ngăn xếp gọi. Nếu readNumber trả về một lỗi, lỗi đó cũng sẽ được trả về từ hàm gọi.

Không có điều gì bí mật, không có ngoại lệ, không có ma thuật, chỉ đơn giản là xử lý lỗi rõ ràng và đơn giản.

Lưu ý: Tôi cá nhân thích các ngôn ngữ lập trình coi lỗi như là giá trị. Điều này giúp việc lý luận về mã dễ dàng hơn rất nhiều.

Bubbling Up Lỗi là gì?

Ở trên, chúng ta đã đề cập đến việc truyền lỗi lên ngăn xếp gọi với try. Đây là một mẫu phổ biến trong Zig.

Khi bạn sử dụng try, nếu hàm trả về một lỗi, lỗi đó sẽ được trả về ngay lập tức từ hàm gọi. Điều này cho phép bạn truyền đạt lỗi lên ngăn xếp gọi mà không cần phải xử lý chúng một cách rõ ràng ở mỗi cấp độ.

Dưới đây là một ví dụ:

zig Copy
fn deepFunction() !void {
    return error.SomethingWentWrong;
}

fn middleFunction() !void {
    try deepFunction();
}

pub fn main() void {
    middleFunction() catch |err| {
        std.debug.print("Error: {}\n", .{err});
    };
}

Trong ví dụ này, deepFunction trả về một lỗi. middleFunction gọi deepFunction bằng cách sử dụng try, có nghĩa là nếu deepFunction trả về một lỗi, lỗi đó sẽ được trả về ngay lập tức từ middleFunction. Cuối cùng, trong main, chúng tôi gọi middleFunction và xử lý lỗi bằng cách sử dụng catch.

Vì vậy, tóm lại: Lỗi sâu -> giữa truyền đạt -> chính xử lý.

Đơn giản và dễ hiểu.

Những thực tiễn tốt nhất trong xử lý lỗi trong Zig

Dưới đây là một số thực tiễn tốt nhất cho việc xử lý lỗi trong Zig:

  1. Sử dụng tập hợp lỗi rõ ràng: Định nghĩa các tập hợp lỗi rõ ràng và cụ thể cho hàm của bạn. Điều này giúp dễ dàng hiểu những lỗi nào có thể xảy ra và cách xử lý chúng.
  2. Đặt tên lỗi một cách mô tả: Sử dụng các tên mô tả cho lỗi của bạn để làm rõ điều gì đã sai.
  3. Xử lý tất cả các trường hợp: Khi sử dụng catch, hãy đảm bảo xử lý tất cả các trường hợp lỗi có thể xảy ra. Điều này đảm bảo rằng chương trình của bạn có thể phục hồi một cách mềm mại từ các lỗi.
  4. Truyền đạt thay vì bỏ qua: Sử dụng try để truyền đạt lỗi lên ngăn xếp gọi thay vì bỏ qua chúng. Điều này giúp đảm bảo rằng các lỗi không bị bỏ qua một cách êm thấm và có thể được xử lý một cách thích hợp.
  5. Sử dụng defer để dọn dẹp: Sử dụng defer để đảm bảo rằng các tài nguyên được dọn dẹp đúng cách, ngay cả khi có lỗi xảy ra.

Bây giờ đây chỉ là một số hướng dẫn mà tôi tuân thủ, còn của bạn có thể khác và điều đó hoàn toàn bình thường.

Ví dụ thực tế

Hãy xem xét một ví dụ phức tạp hơn kết hợp nhiều nguồn lỗi và minh họa việc truyền đạt lỗi.

zig Copy
const std = @import("std");

const ProcessError = error{
    FileNotFound,
    InvalidFormat,
    AccessDenied,
} || std.fs.File.OpenError || std.fs.File.ReadError;

fn processDocument(path: []const u8) ProcessError!void {
    const file = try std.fs.cwd().openFile(path, .{});
    defer file.close();

    var buffer: [1024]u8 = undefined;
    const size = try file.readAll(&buffer);

    if (size < 10) return ProcessError.InvalidFormat;

    std.debug.print("Processed {} bytes from {}\n", .{size, path});
}

pub fn main() void {
    const files = [_][]const u8{"doc1.txt", "doc2.txt"};
    for (files) |file| {
        processDocument(file) catch |err| {
            switch(err) {
                ProcessError.FileNotFound => continue,
                ProcessError.InvalidFormat => continue,
                ProcessError.AccessDenied => continue,
                else => continue,
            }
        };
    }
}

Trong ví dụ này, processDocument cố gắng mở và đọc một tệp. Nó có thể trả về

  • FileNotFound nếu tệp không tồn tại,
  • InvalidFormat nếu nội dung tệp không như mong đợi,
  • AccessDenied nếu có vấn đề về quyền truy cập,
  • hoặc bất kỳ lỗi nào từ std.fs.File.OpenErrorstd.fs.File.ReadError. Trong main, chúng ta cố gắng xử lý nhiều tài liệu, xử lý lỗi một cách thích hợp dựa trên loại của chúng.

Chúng tôi sử dụng try để truyền đạt lỗi từ các thao tác tệp và các kiểm tra tùy chỉnh, và catch trong main để xử lý chúng.
Sau đó, chúng tôi sử dụng defer để đảm bảo tệp được đóng đúng cách, ngay cả khi có lỗi xảy ra trong quá trình đọc.

Kết luận

Mô hình xử lý lỗi của Zig là đơn giản, rõ ràng và mạnh mẽ. Bằng cách coi lỗi như là giá trị và sử dụng các cấu trúc như trycatch, Zig cho phép các nhà phát triển viết mã rõ ràng và dễ bảo trì mà xử lý lỗi một cách mềm mại. Dù bạn đang viết mã hệ thống cấp thấp hay logic ứng dụng cấp cao, mô hình xử lý lỗi của Zig cung cấp các công cụ cần thiết để quản lý lỗi một cách hiệu quả.

Hy vọng bài viết này đã giúp bạn hiểu rõ hơn về cách thức hoạt động của xử lý lỗi trong Zig và tại sao nó là một lựa chọn tuyệt vời cho nhiều kịch bản lập trình.

Để xem thêm các bài viết tương tự, hãy truy cập trang web của tôi tại dayvster.com hoặc theo dõi tôi trên Twitter @Dayvster.

Gợi ý câu hỏi phỏng vấn
Không có dữ liệu

Không có dữ liệu

Bài viết được đề xuất
Bài viết cùng tác giả

Bình luận

Chưa có bình luận nào

Chưa có bình luận nào