Biến đổi
Một luồng vào, một luồng ra — và quyết định flatMap
Những toán tử đơn giản hành xử đúng như các toán tử cùng tên bên Sequence, chỉ khác là chúng áp lên
các giá trị khi chúng đến chứ không áp lên một tập hợp đã có sẵn:
publisher
.map { $0 * 2 } // biến đổi từng giá trị
.filter { $0 > 10 } // cho một số đi qua
.compactMap { Int($0) } // biến đổi, bỏ các nil
.removeDuplicates() // bỏ các giá trị bằng nhau liên tiếp
.replaceNil(with: 0) // luồng Optional thành không optional
Hai toán tử không có tương đương trực tiếp bên Sequence thì đáng biết:
scan cộng dồn và phát ra tổng chạy ở mỗi lần, trong khi reduce chỉ phát ra ở cuối:
taps.scan(0) { count, _ in count + 1 } // 1, 2, 3, 4 …
collect đi hướng ngược lại, gom các giá trị vào một mảng. collect() trần chờ luồng kết thúc,
mà với một luồng vô hạn thì nghĩa là chờ mãi mãi — collect(3) hoặc
collect(.byTime(scheduler, .seconds(1))) mới là những dạng hữu ích.
Quyết định quan trọng: flatMap hay switchToLatest
Cả hai nhận một luồng giá trị rồi biến mỗi giá trị thành một publisher mới — một từ khóa tìm kiếm thành một request mạng, một id người dùng thành một lượt tải hồ sơ. Chúng khác nhau ở chỗ cái trước đó sẽ ra sao, và chọn sai là lỗi nghiêm trọng phổ biến nhất trong code Combine.
flatMap giữ lại tất cả. Mọi publisher con đều chạy đến khi xong, và giá trị của chúng đan xen
nhau theo đúng thứ tự chúng đến.
switchToLatest hủy cái trước đó ngay khi một giá trị mới đến từ nguồn trên.
// Sai cho tìm kiếm gõ tới đâu tra tới đó
searchTerms
.flatMap { api.search($0) }
.sink { showResults($0) }
// Đúng
searchTerms
.map { api.search($0) }
.switchToLatest()
.sink { showResults($0) }
Gõ “sw”, “swi”, “swif”, “swift” thì phiên bản đầu bắn đi bốn request và hiển thị cái nào về sau cùng. Với độ trễ mạng vốn thất thường, cái về sau cùng thường lại là kết quả cho “swi” — người dùng thấy kết quả của một truy vấn mà họ đã gõ qua từ lâu, và không lượng debounce nào chữa được hẳn, vì debounce giảm số request chứ không sắp thứ tự cho chúng.
Quy tắc: nếu chỉ kết quả mới nhất là có nghĩa, hãy dùng switchToLatest. Tìm kiếm, gợi ý tự
động, một màn hình chi tiết đi theo lựa chọn, bất cứ thứ gì được điều khiển bởi “người dùng hiện đang
xem X”.
Hãy dùng flatMap khi mọi kết quả đều quan trọng — tải lên một hàng đợi file, xử lý một lô, bắn
các sự kiện phân tích. Và khi dùng, hãy đặt giới hạn đồng thời:
uploads
.flatMap(maxPublishers: .max(3)) { upload($0) }
Cảnh báo
Mặc định maxPublishers của flatMap là .unlimited. Cho một nghìn id đi qua nó sẽ khởi động
một nghìn request cùng lúc, thứ sẽ vắt kiệt bể kết nối của URLSession và có thể khiến bạn bị
chặn vì vượt hạn mức. Không có tình huống nào mà giá trị mặc định không giới hạn ấy là một quyết
định có cân nhắc.
flatMap và kiểu lỗi
flatMap đòi Failure của publisher con phải khớp với Failure của publisher ngoài. Đây là nguồn
gốc của rất nhiều đau khổ với bộ kiểm tra kiểu, và cách chữa thường là xử lý lỗi bên trong ngay trong
closure:
searchTerms // Failure == Never
.map { term in
api.search(term) // Failure == APIError
.catch { _ in Just([Result]()) } // giờ Failure == Never
}
.switchToLatest()
Đặt catch vào bên trong không phải một lựa chọn phong cách. Đặt bên ngoài, request hỏng đầu tiên sẽ
kết liễu cả luồng searchTerms và ô tìm kiếm sẽ ngừng hoạt động trong suốt phần còn lại của phiên
làm việc. Đặt bên trong, một request hỏng thì trả về mảng rỗng, và phím gõ tiếp theo vẫn chạy bình
thường.
setFailureType(to:) chuyển một publisher có Failure == Never sang một kiểu lỗi khi bạn cần chiều
ngược lại, và nó không sinh ra hành vi nào lúc chạy — nó tồn tại thuần túy để làm vừa lòng bộ kiểm
tra kiểu.
share() và nhiều subscriber
Một publisher lạnh làm việc của nó một lần cho mỗi subscriber:
let request = api.fetchProfile()
request.sink { updateHeader($0) }.store(in: &cancellables)
request.sink { updateBody($0) }.store(in: &cancellables) // request mạng thứ hai
share() cho ra một subscription lên nguồn trên mà cả hai subscriber cùng quan sát:
let request = api.fetchProfile().share()
Điểm gài: share() hành xử như một PassthroughSubject, nên một subscriber đến sau khi giá trị đã
được phát ra sẽ lỡ hẳn. Với một request có thể đã hoàn tất rồi,
.multicast { CurrentValueSubject(…) } hoặc tự lưu kết quả lại là hình dạng an toàn hơn.