Swift Combine: Для чего нужны эти многоадресные функции и как их использовать?
Борясь с некоторыми проблемами объединения, я натолкнулся на раздел "Работа с несколькими подписчиками" в https://developer.apple.com/documentation/combine/publisher:
func multicast<S>(() -> S) -> Publishers.Multicast<Self, S>
func multicast<S>(subject: S) -> Publishers.Multicast<Self, S>
Однако, когда я попытался подтвердить свое предположение о необходимости многоадресной рассылки при отправке нескольким подписчикам, я обнаружил, что в этом нет необходимости при использовании кода этой игровой площадки (изменено с https://github.com/AvdLee/CombineSwiftPlayground/blob/master/Combine.playground/Pages/Combining%20Publishers.xcplaygroundpage/Contents.swift) (запуск 10.14.5 в Xcode Version 11.0 beta 3 (11M362v)):
enum FormError: Error { }
let usernamePublisher = PassthroughSubject<String, FormError>()
let passwordPublisher = PassthroughSubject<String, FormError>()
let validatedCredentials = Publishers.CombineLatest(usernamePublisher, passwordPublisher)
.map { (username, password) -> (String, String) in
return (username, password)
}
.map { (username, password) -> Bool in
!username.isEmpty && !password.isEmpty && password.count > 12
}
.eraseToAnyPublisher()
let firstSubscriber = validatedCredentials.sink { (valid) in
print("First Subscriber: CombineLatest: Are the credentials valid: \(valid)")
}
let secondSubscriber = validatedCredentials.sink { (valid) in
print("Second Subscriber: CombineLatest: Are the credentials valid: \(valid)")
}
// Nothing will be printed yet as `CombineLatest` requires both publishers to have send at least one value.
usernamePublisher.send("avanderlee")
passwordPublisher.send("weakpass")
passwordPublisher.send("verystrongpassword")
Это печатает:
First Subscriber: CombineLatest: Are the credentials valid: false
Second Subscriber: CombineLatest: Are the credentials valid: false
First Subscriber: CombineLatest: Are the credentials valid: true
Second Subscriber: CombineLatest: Are the credentials valid: true
таким образом, кажется, что многоадресная рассылка не требуется для адресации нескольких подписчиков. Или я не прав?
Итак, для чего нужны эти многоадресные функции и как бы я их использовал? Некоторый пример кода был бы хорош.
Спасибо,
Lars
1 ответ
PassthroughSubject - не очень хороший пример для тестирования, потому что это класс и дает вам эталонную семантику. Следовательно, в простом случае два подписчика могут подписаться на него напрямую и получать одни и те же значения в одно и то же время всякий раз, когда субъект излучает одно.
Но вот лучший тестовый пример (вдохновленный обсуждением какао с любовью):
let pub1 = Timer.publish(every: 1, on: .main, in: .default).autoconnect()
let sub = CurrentValueSubject<Int,Never>(0)
let scan = sub.scan(10) {i,j in i+j}
pub1.sink { _ in let i = sub.value; sub.value = i+1 }.store(in:&storage)
scan.sink { print("a", $0) }.store(in:&storage)
delay(3) {
scan.sink { print("b", $0) }.store(in:&self.storage)
}
Это дает явно странный результат, когда второй sink
приходит в качестве нового подписчика на этот конвейер:
a 10
a 11
a 13
a 16
b 13
a 20
b 17
a 25
b 22
a 31
b 28
a 38
b 35
Раковины a
а также b
получают разные серии чисел друг от друга, потому что scan
это структура. Если мы хотим, чтобы они получали одинаковые числа, мы можем использовать многоадресную рассылку:
let scan = sub.scan(10) {i,j in i+j}.multicast {PassthroughSubject()}.autoconnect()
Это дает
a 10
a 11
a 13
a 16
a 20
b 20
a 25
b 25
который согласован.
Однако это еще не доказывает, что вам нужноmulticast
, потому что вы могли бы сделать то же самое, сказав .share()
вместо. Я не понимаю, в чем разница междуmulticast
а также share
.
Ответ / ссылка на быстрых форумах подразумевает, что многоадресные методы предназначены для использования оператора.share(). Из поста Филиппа:
В этом случае он используется для подключения восходящего потока к объекту PassthroughSubject, а затем автоматически подключается. Обычно, когда подписчик получает подписку, он отменяет любые дополнительные подписки после первой, многоадресная передача выдает аварийный штрих для этого поведения и обрабатывает несколько подписок.
На практике, если вы хотите разделить поток и обновления многоадресных событий по нескольким конвейерам в Combine, кажется, что наиболее прагматичным способом является создание свойства @Published, когда любой восходящий конвейер обновляет его с помощью.assign() или внутри.sink(), а затем настройте дополнительные конвейеры с подписчиками из свойства @Published.