Trang chủ

Bản hợp đồng

Cái bắt tay bốn bước nằm dưới mọi luồng dữ liệu

Giữa sink và giá trị đầu tiên có một cái bắt tay, và gần như mọi điều gây bất ngờ ở Combine đều được nó giải thích.

  1. Đăng ký. Subscriber được trao cho publisher qua receive(subscriber:).
  2. Subscription. Publisher tạo một Subscription — phần trạng thái riêng cho từng subscriber — rồi trao lại bằng receive(subscription:).
  3. Demand. Subscriber gọi subscription.request(_:) với số lượng giá trị nó muốn. Cho tới khi nó gọi, không gì được gửi đi cả.
  4. Giá trị, rồi kết thúc. Publisher gửi tối đa chừng ấy giá trị, mỗi giá trị lại trả về thêm demand, và cuối cùng là một .finished hoặc một .failure.
protocol Subscriber<Input, Failure> {
    func receive(subscription: Subscription)
    func receive(_ input: Input) -> Subscribers.Demand
    func receive(completion: Subscribers.Completion<Failure>)
}

Hãy để ý kiểu trả về của phương thức ở giữa. Mỗi giá trị được giao là một dịp để xin thêm, và đó là cách demand tích lũy mà không cần một kênh riêng.

Viết một cái, đúng một lần

Chẳng ai viết subscriber tùy biến trong code sản xuất. Viết một cái đúng một lần, trong playground, vẫn là cách nhanh nhất để thôi thấy Combine bí ẩn:

final class OneAtATime: Subscriber {
    typealias Input = Int
    typealias Failure = Never

    func receive(subscription: Subscription) {
        subscription.request(.max(1))          // xin đúng một cái
    }

    func receive(_ input: Int) -> Subscribers.Demand {
        print("nhận \(input)")
        return .max(1)                         // đã tiêu thụ một, xin thêm một
    }

    func receive(completion: Subscribers.Completion<Never>) {
        print("xong: \(completion)")
    }
}

(1...5).publisher.subscribe(OneAtATime())

Đổi .max(1) thành .none trong receive(_:) và luồng sẽ giao đúng một giá trị rồi dừng lại mãi mãi — không kết thúc, không lỗi, chỉ im lặng. Đó không phải lỗi; đó là bản hợp đồng đang hoạt động đúng. Publisher không được phép gửi giá trị thứ hai vì chẳng ai xin.

Mẹo

Sự im lặng đó đáng được tạo ra một lần cho có chủ đích, vì nó chính xác là hình dạng của một pipeline Combine bị kẹt trong môi trường thật: không sập, không log, các giá trị đơn giản là ngừng. Nhận ra hình dạng ấy tiết kiệm cho bạn hàng giờ về sau.

sink và assign thật ra là gì

Cả hai là Subscribers.Sink và Subscribers.Assign nấp sau một phương thức tiện lợi, và cả hai đều yêu cầu .unlimited ngay lập tức. Đó là lý do bạn không bao giờ phải nghĩ về demand khi dùng bình thường — và là lý do một publisher nhanh đổ vào một sink chậm sẽ dồn bộ đệm không giới hạn thay vì chậm lại.

assign(to:on:) ghi từng giá trị vào một key path trên một đối tượng:

model.$name
    .assign(to: \.text, on: label)
    .store(in: &cancellables)

Cảnh báo

assign(to:on:) giữ một tham chiếu mạnh tới on:. Gán vào một thuộc tính của self từ một luồng được lưu trong self là một vòng giữ tham chiếu, và là một vòng rất hay gặp. Hãy dùng sink { [weak self] in … }, hoặc assign(to: &$published) — bản nạp chồng nhận & dùng với @Published tự lo vòng đời cho bạn và không giữ tham chiếu.

Cancellable và việc hủy làm gì

sink và assign trả về một AnyCancellable. Nó có đúng một việc: lúc deinit, nó gọi cancel(), thứ tháo dỡ subscription và giải phóng cả chuỗi.

Đây là lý do cái Set<AnyCancellable> được lưu lại tồn tại, và là lý do quên .store(in:) sinh ra một pipeline chạy được đúng không giá trị nào — cái cancellable bị hủy ở cuối hàm, và subscription đi theo nó.

Việc hủy lan ngược lên trên: hủy ở sink thì mọi toán tử phía trên đều bị tháo dỡ, lên tới và bao gồm cả một request URLSession, thứ thật sự bị hủy chứ không phải chỉ bị lờ đi.

Bùng nổ kiểu, và eraseToAnyPublisher

Mỗi toán tử bọc nguồn trên của nó trong một kiểu generic mới, nên một chuỗi năm toán tử có tên kiểu đại loại như:

Publishers.RemoveDuplicates<Publishers.Debounce<Publishers.CompactMap<…>, RunLoop>>

Bạn không viết được cái đó trong một API, nên hãy xóa kiểu ở ranh giới:

func searchResults(for query: String) -> AnyPublisher<[Result], Never> {
    api.search(query)
        .replaceError(with: [])
        .receive(on: DispatchQueue.main)
        .eraseToAnyPublisher()
}

Hãy xóa kiểu ở rìa của một kiểu dữ liệu, đừng xóa giữa từng toán tử. AnyPublisher đóng hộp cả chuỗi và thêm một lớp gián tiếp cho mỗi giá trị — không đáng kể nếu làm một lần, phí phạm nếu bạn làm bốn lần trong cùng một pipeline.