Trang chủ

Lỗi

Ném lỗi, cho sập, và chọn giữa hai bên

Swift có bốn cơ chế riêng biệt để xử lý chuyện hỏng hóc, và dùng nhầm cơ chế chính là cách biến một tình huống khôi phục được thành một báo cáo sập ứng dụng.

Cơ chế Dùng cho
throws Thất bại lường trước được mà bên gọi xử lý được: dữ liệu vào sai, thiếu tệp, mạng từ chối
Optional Sự vắng mặt không cần giải thích: không có khoá đó, không có phần tử đầu
precondition / fatalError Lỗi của lập trình viên: một bất biến do chính code bạn bảo đảm đã bị phá
assert Như trên, nhưng chỉ ở bản debug

Ranh giới đáng vạch ra: nếu thất bại do thế giới bên ngoài gây ra thì throw. Nếu do lập trình viên gây ra thì cho sập. Một bộ phân tích cú pháp nhận dữ liệu hỏng thì nên throw; nhưng nếu chính nó phát hiện độ lệch nội bộ của mình vượt phạm vi thì nên sập, vì không có cách khôi phục đúng đắn nào từ một bất biến đã gãy.

Lỗi là giá trị

Error không có yêu cầu nào. Kiểu nào cũng làm lỗi được, và enum là lựa chọn tự nhiên:

enum LoadError: Error {
    case notFound(path: String)
    case corrupted(offset: Int)
    case tooLarge(bytes: Int, limit: Int)
}

Hãy đính kèm bối cảnh ngay tại nơi ném lỗi, chỗ mà nó còn được biết. Một lỗi chỉ nói .corrupted và không gì khác buộc mọi tầng phía trên phải đoán.

Với thứ người dùng có thể nhìn thấy, hãy thêm LocalizedError:

extension LoadError: LocalizedError {
    var errorDescription: String? {
        switch self {
        case .notFound(let path): "Không có tệp nào ở \(path)."
        case .corrupted(let offset): "Tệp bị hỏng tại byte \(offset)."
        case .tooLarge(let bytes, let limit): "\(bytes) byte vượt giới hạn \(limit) byte."
        }
    }
}

Typed throws

Cho tới gần đây, mọi throws đều mang nghĩa “ném ra cái gì đó, tự lo lấy”. Swift 6 cho phép gọi tên nó:

func load(_ path: String) throws(LoadError) -> Data

Giờ trình biên dịch biết trọn bộ các thất bại có thể xảy ra, catch vét cạn được mà không cần default, và lỗi có thể là một enum đóng băng trong ngữ cảnh nhúng, không cần hộp existential nào.

Hãy dùng nó ở nơi tập lỗi thật sự đóng và là một phần của hợp đồng — một trình phân tích cú pháp, một bộ giải mã, một thư viện nhỏ. Tránh dùng ở ranh giới API còn sẽ mở rộng: thêm một case vào hàm typed throws là thay đổi phá vỡ mã nguồn với mọi catch vét cạn, đúng cái bẫy của enum không đóng băng. throws không kiểu vẫn là lựa chọn mặc định đúng cho code ứng dụng.

rethrows

rethrows nói rằng “tôi chỉ ném lỗi nếu closure bạn đưa tôi ném lỗi”:

extension Sequence {
    func count(where predicate: (Element) throws -> Bool) rethrows -> Int {
        var total = 0
        for element in self where try predicate(element) { total += 1 }
        return total
    }
}

Bên gọi truyền closure không ném lỗi thì không cần try. Không có rethrows, mọi người gọi mọi hàm bậc cao đều bị ép phải xử lý lỗi mà họ không hề cần.

defer

defer chạy trên đường ra khỏi phạm vi, ra kiểu gì cũng chạy — return, throw, hay rơi hết thân hàm:

func process(_ path: String) throws -> Report {
    let handle = try FileHandle(forReadingFrom: URL(filePath: path))
    defer { try? handle.close() }

    let header = try readHeader(handle)
    guard header.isSupported else { throw LoadError.corrupted(offset: 0) }
    return try buildReport(from: handle, header: header)
}

Lệnh đóng nằm ngay cạnh lệnh mở, và chỉ có đúng một lệnh đóng dù sau này hàm mọc thêm bao nhiêu lối ra. Nhiều khối defer chạy theo thứ tự ngược, khớp với cách các tài nguyên lồng nhau.

Cảnh báo

defer lấy giá trị tại thời điểm khối lệnh chạy, không phải lúc nó được viết ra. Một defer { print(count) } đặt ở đầu hàm sẽ in ra count cuối cùng, không phải số 0. Điều này gần như luôn là thứ bạn muốn, và đôi khi gây bối rối dữ dội.

Result, và khi nào nên dùng

Result<Success, Failure> biến một lỗi thành giá trị mà bạn lưu được, truyền đi được, hoặc đưa vào completion handler. Khi async/await đã phủ gần hết các API kiểu callback, chỗ dùng còn lại của nó khá hẹp: lưu đệm một thất bại, gom kết quả của một lô việc, hoặc đi qua một API không thể async.

let outcomes: [Result<Data, LoadError>] = paths.map { path in
    Result { try load(path) }.mapError { $0 as! LoadError }
}

Nếu bạn thấy mình vừa tạo Result xong đã đổi ngược về throws bằng try result.get(), thì có lẽ ngay từ đầu hàm đó nên là throws.