Tikhonov Aleksandr
Tikhonov Aleksandr

Reputation: 14349

Understanding share() in Combine

As we know, usually publishers are struct. What will change if it is a class?

Let's consider we have 1 publisher which emits 1 value and 2 subscribers, which subscribes to it.

let p1 = Just(20)
let s1 = p1.print().sink { _ in }
let s2 = p1.print().sink { _ in }

// s1 - receive value: (20)
// s2 - receive value: (20)

in print logs, we can see that both subscribers got value (20).

If we open the documentation of share() operator, we will see

share() - Returns a publisher as a class instance.

So it just changes semantic of the publisher from value to reference. In our example, we don't pass p1 publisher to any function or assign to any object and that's why for me there is no difference publisher is struct or class ... But if I add share() operator behavior will be different, s2 won't get value.

let p1 = Just(20).share() // !
let s1 = p1.print().sink { _ in }
let s2 = p1.print().sink { _ in }

// s1 - receive value: (20)

I saw some examples with URLSession.shared.dataTaskPublisher(\_: URL) or with some "delayed" publishers, when s2 also gets value, but it's still unclear for me how just changing semantic of publisher change its behaviour in such way.

Upvotes: 10

Views: 4768

Answers (2)

Changwei
Changwei

Reputation: 672

In my option, if you don't add .share(), there will be two event streams subscribed by two subscribers. If you add .share(), there will be only one event stream subscribed by two subscribers.

Upvotes: 3

matt
matt

Reputation: 535889

The problem is that you are not using a pipeline where it does make a difference. Consider this example (based on a Cocoa With Love article) where a second subscriber comes online after the publisher has been publishing for some time:

let pub1 = Timer.publish(every: 1, on: .main, in: .default)
let c1 = pub1.connect()
let scan = Publishers.Scan(upstream: pub1, initialResult: 0) { (a, b) -> Int in
    a + 1
}
scan.sink { print("a:", $0) }.store(in:&storage)
delay(3) {
    scan.sink { print("b:", $0) }.store(in:&self.storage)
}

The point is, there is only one scan and it is producing 1, 2, 3 when after a delay another subscriber comes along. What will that subscriber get? Will it just pick up where we are now? No. We get this:

a: 1
a: 2
a: 3
a: 4
b: 1
a: 5
b: 2
a: 6
b: 3
...

So in effect we start all over again with our second subscription, because the publisher is a new copy. But if we promote the publisher to a class, we get completely different results:

let pub1 = Timer.publish(every: 1, on: .main, in: .default)
let c1 = pub1.connect()
let scan = Publishers.Scan(upstream: pub1, initialResult: 0) { (a, b) -> Int in
    a + 1
}
let scan2 = scan.share() // <--
scan2.sink { print("a:", $0) }.store(in:&storage)
delay(3) {
    scan2.sink { print("b:", $0) }.store(in:&self.storage)
}

Now we get this:

a: 1
a: 2
a: 3
a: 4
b: 4
a: 5
b: 5
a: 6
b: 6
a: 7
b: 7

Obviously that's a very significant difference. You can see the same sort of thing if your publisher is a Subject, because that's a class, not a struct.

Upvotes: 23

Related Questions