Trang chủ

Generic

Ràng buộc, chuyên biệt hoá, và cái giá của từng thứ

Generic cho phép viết một thuật toán một lần mà vẫn giữ trọn thông tin kiểu. Khác với cơ chế xoá kiểu của Java hay macro của C, generic trong Swift được kiểm tra kiểu đúng một lần ngay tại chỗ định nghĩa, rồi có thể được chuyên biệt hoá cho từng kiểu cụ thể tại nơi gọi — bạn chỉ phải bảo trì một bản cài đặt, mà mã máy sinh ra thường chặt chẽ như thể bạn viết tay riêng cho kiểu đó.

func firstIndex<C: Collection>(of element: C.Element, in collection: C) -> C.Index?
    where C.Element: Equatable
{
    var index = collection.startIndex
    while index != collection.endIndex {
        if collection[index] == element { return index }
        collection.formIndex(after: &index)
    }
    return nil
}

Hãy đọc các ràng buộc như một giao diện. C: Collection nói có sẵn những thao tác nào; mệnh đề where nói các phần tử so sánh được với nhau. Thứ gì không được nêu ra thì không dùng được — và đó chính là điểm mấu chốt, vì nó cũng là thứ cho phép trình biên dịch kiểm tra thân hàm một lần thay vì kiểm tra lại ở từng nơi gọi.

Ràng buộc nên đặt ở mệnh đề where

Hai cách viết gần như giống nhau:

extension Array where Element: Numeric {
    func total() -> Element { reduce(0, +) }
}

extension Collection where Element: Numeric {
    func total() -> Element { reduce(0, +) }
}

Bản thứ hai chạy được trên Array, Set, ArraySlice, Range, String.UTF8View và mọi collection mà bất kỳ ai viết ra. Hãy ràng buộc vào protocol ít cụ thể nhất còn đủ cho những gì thân hàm thật sự làm. Nó không tốn gì thêm mà nhân rộng phạm vi sử dụng của đoạn code.

Tuân thủ có điều kiện

Một kiểu generic chỉ tuân thủ protocol khi tham số kiểu của nó tuân thủ:

struct Pair<T> {
    let first: T
    let second: T
}

extension Pair: Equatable where T: Equatable {}
extension Pair: Hashable where T: Hashable {}
extension Pair: Codable where T: Codable {}

Pair<Int> giờ là Hashable và làm khoá từ điển được; Pair<(Int) -> Void> thì không, và trình biên dịch sẽ báo ngay tại chỗ dùng sai thay vì hỏng sâu bên trong một bảng băm. Array cũng vận hành đúng như vậy — [Int]Equatable, [() -> Void] thì không.

Kiểu trả về mờ

Trả về some Collection giấu kiểu cụ thể đi mà vẫn giữ tính tĩnh:

func evens(upTo limit: Int) -> some Collection<Int> {
    stride(from: 0, to: limit, by: 2).lazy.map { $0 }
}

Bên gọi có trọn vẹn hiệu năng và dùng được mọi phương thức của Collection, nhưng không thể phụ thuộc vào việc giá trị trả về là LazyMapSequence<StrideTo<Int>, Int> — nhờ vậy bạn đổi cài đặt mà không làm gãy ai. Đó chính xác là thứ any Collection<Int> không hứa được, và cũng chính xác là thứ một kiểu trả về cụ thể không giấu được.

Mẹo

Một phép thử hữu ích: nếu đổi map thành compactMap bên trong mà bên gọi gãy, thì kiểu trả về đang quá cụ thể. Nếu một vòng lặp nóng lộ ra lời gọi động mà bạn không ngờ tới, thì nó đang quá existential.

Parameter pack

Trước Swift 5.9, “một hàm nhận số lượng tuỳ ý các đối số khác kiểu nhau” nghĩa là viết cùng một overload bảy lần — đó là lý do thư viện chuẩn từng chỉ có zip cho hai sequence và không có gì cho ba. Parameter pack giải quyết chuyện đó:

struct Tuple<each T> {
    let values: (repeat each T)

    init(_ values: repeat each T) {
        self.values = (repeat each values)
    }
}

func allEqual<each T: Equatable>(_ left: repeat each T, _ right: repeat each T) -> Bool {
    for (l, r) in repeat (each left, each right) where l != r { return false }
    return true
}

each T là một gói kiểu; repeat khai triển trên gói đó. Bạn sẽ không viết chúng thường xuyên, nhưng sẽ đọc chúng trong code thư viện, và chúng giải thích vì sao một số chữ ký trong thư viện chuẩn tự dưng mọc thêm repeat each.

Cái giá của chuyên biệt hoá

Chuyên biệt hoá là lý do generic trong Swift chạy nhanh, và nó có giá: trình biên dịch sinh ra một bản sao riêng của thân hàm cho từng kiểu cụ thể mà nó chuyên biệt hoá. Một module dùng generic dày đặc có thể phình kích thước nhị phân thấy rõ và làm chậm quá trình build. Khi một hàm generic vừa lớn, vừa hiếm khi nóng, lại vừa dùng với nhiều kiểu, việc cố ý đẩy nó qua existential — any Shape thay vì some Shape — đánh đổi một chút tốc độ lấy rất nhiều dung lượng code. Hãy đo trước khi cho rằng mình cần cái nào.