Trang chủ

Enum và so khớp mẫu

Làm cho những trạng thái phi pháp không biểu diễn được

Enum của Swift không phải enum của C. Nó là một hợp có nhãn: mỗi case có thể mang theo payload riêng, thuộc kiểu riêng, và trình biên dịch bảo đảm tại một thời điểm có đúng một case đang giữ giá trị.

Chính lời bảo đảm đó là toàn bộ giá trị. Một kiểu có thể ở đúng một trong bốn trạng thái, mỗi trạng thái mang đúng dữ liệu mà nó cần, thì không thể bị đẩy vào trạng thái thứ năm bởi bất kỳ đoạn code nào bạn hay ai khác viết.

enum LoadState {
    case idle
    case loading(progress: Double)
    case loaded(items: [Item])
    case failed(Error)
}

Hãy so với hình dạng mà nó thay thế:

struct LoadState {
    var isLoading: Bool
    var progress: Double?
    var items: [Item]?
    var error: Error?
}

Cái struct đó có mười sáu tổ hợp nil và không nil, trong đó bốn cái là có nghĩa. isLoading == true kèm một error khác nil là biểu diễn được, nên ở đâu đó có một nhánh xử lý nó, hoặc — nhiều khả năng hơn — là không có, và nó xảy ra mỗi tháng một lần trong môi trường thật.

Tính vét cạn mới là tính năng

Một switch trên một enum phải xử lý mọi case, và chính phép kiểm tra đó khiến enum an toàn để thay đổi. Thêm một case, và trình biên dịch liệt kê mọi chỗ cần cập nhật.

switch state {
case .idle:
    return AnyView(StartButton())
case .loading(let progress):
    return AnyView(ProgressView(value: progress))
case .loaded(let items):
    return AnyView(ItemList(items: items))
case .failed(let error):
    return AnyView(ErrorView(error: error))
}

Cảnh báo

Một mệnh đề default: vứt bỏ tất cả những thứ đó. Nó làm trình biên dịch im lặng về những case còn chưa tồn tại, mà đó đúng là lời cảnh báo bạn muốn có. Chỉ dùng default cho phần dư thật sự không đáng quan tâm, và hãy ưu tiên liệt kê các case dùng chung một thân: case .idle, .failed: …

Ngoại lệ là một enum đến từ module khác, thứ có thể mọc thêm case mà code của bạn không được biên dịch lại. Những cái đó cần @unknown default: — một mệnh đề vẫn cảnh báo khi bạn biên dịch với phiên bản mới hơn, nên bạn giữ được thông báo mà không làm hỏng bản build.

Giá trị đi kèm và so khớp mẫu

Mẫu đi sâu hơn một tầng, và đó là chỗ switch thôi là một cái if hào nhoáng:

switch (oldState, newState) {
case (.loading, .loaded(let items)) where items.isEmpty:
    showEmptyState()
case (.loading, .loaded(let items)):
    show(items)
case (_, .failed(let error as URLError)) where error.code == .notConnectedToInternet:
    showOfflineBanner()
case (_, .failed(let error)):
    show(error)
default:
    break
}

Tuple, mẫu lồng nhau, mệnh đề where và ép kiểu đều ghép được trong cùng một biểu thức. Một máy trạng thái viết theo cách này đọc như một bảng chuyển trạng thái, và nó đúng là như thế.

Để khớp một case duy nhất, if case và guard case giúp tránh cả một switch:

guard case .loaded(let items) = state else { return }

Enum đệ quy

Bình thường một case của enum không thể chứa một giá trị thuộc chính kiểu của nó — kích thước sẽ vô hạn. indirect thêm một lớp gián tiếp để nó làm được:

indirect enum Expression {
    case number(Double)
    case add(Expression, Expression)
    case multiply(Expression, Expression)
    case negate(Expression)
}

func evaluate(_ expression: Expression) -> Double {
    switch expression {
    case .number(let value):           value
    case .add(let a, let b):           evaluate(a) + evaluate(b)
    case .multiply(let a, let b):      evaluate(a) * evaluate(b)
    case .negate(let inner):          -evaluate(inner)
    }
}

Đó là một cây biểu thức hoàn chỉnh cùng bộ tính giá trị của nó trong mười lăm dòng, với trình biên dịch bảo đảm bộ tính xử lý mọi loại nút. indirect đóng hộp payload trên heap, nên nó tốn một lần cấp phát cho mỗi nút — đáng biết, hiếm khi đáng tránh.

Trình biên dịch làm gì với chúng

Cách bố trí bộ nhớ của enum khéo hơn vẻ ngoài của nó, và điều đó giải thích vì sao Optional là miễn phí.

Một enum không có giá trị đi kèm được lưu bằng một byte duy nhất. Một enum mà các case mang payload thì lớn bằng payload lớn nhất của nó cộng thêm, thường là, một byte nhãn.

Trường hợp thú vị là khi một payload có các mẫu bit thừa. Trong Swift một tham chiếu không bao giờ null, nên mẫu toàn số không là chưa dùng đến — và Optional<SomeClass> dùng nó cho .none. Kết quả là một tham chiếu class optional vẫn đúng tám byte như tham chiếu thường, không có nhãn nào cả.

MemoryLayout<UnsafeRawPointer>.size          // 8
MemoryLayout<UnsafeRawPointer?>.size         // 8 — miễn phí
MemoryLayout<Bool>.size                      // 1
MemoryLayout<Bool?>.size                     // 2 — Bool chỉ dùng hai trong 256 mẫu,
                                             //     nhưng cách bố trí ở đây vẫn thêm một byte

Điều này đáng biết chủ yếu để bạn thôi đi tối ưu nó. Làm cho một thuộc tính hết optional để “tiết kiệm bộ nhớ” thường chẳng tiết kiệm được gì và lấy đi của bạn khả năng biểu diễn sự vắng mặt.

Chỗ struct là câu trả lời tốt hơn

Enum dành cho một trong nhiều, struct dành cho tất cả những thứ này cùng lúc. Sai lầm là với tới enum khi các case dùng chung phần lớn dữ liệu:

// sai: mọi case đều mang đúng ba trường như nhau
enum User {
    case admin(name: String, email: String, since: Date)
    case member(name: String, email: String, since: Date)
}

// đúng
struct User {
    let name: String
    let email: String
    let since: Date
    let role: Role       // enum { case admin, member }
}

Phép thử: nếu hai case mang cùng một payload thì khác biệt giữa chúng là một trường, không phải một case.