问题
I'm just learning how to use Combine. I have experience with Rx (RxSwift and RxJava) and I'm noticing that it's quite similar.
However, one thing that is quite different (and kind of annoying) is that the Publisher
protocol doesn't use generics for its Output
and Failure
types; it uses associated types instead.
What this means is that I can't specify a polymorphic Publisher
type (such as Publisher<Int, Error>
) and simply return any type that conforms to Publisher
with those types. I need to use AnyPublisher<Int, Error>
instead, and I am forced to include eraseToAnyPublisher()
all over the place.
If this is the only option, then I'll put up with it. However, I also recently learned about opaque types in Swift, and I'm wondering if I might be able to use them to get around this.
Is there a way for me to have, say, a function that returns some Publisher
and use specific types for Output
and Failure
?
This seems like a perfect case for opaque types, but I can't figure out if there's a way for me to both use an opaque type and specify the associated types.
I'm picturing something like this:
func createPublisher() -> some Publisher where Output = Int, Failure = Error {
return Just(1)
}
回答1:
Swift, as of this writing, doesn't have the feature you want. Joe Groff specifically describes what is missing in the section titled “Type-level abstraction is missing for function returns” of his “Improving the UI of generics” document:
However, it's common to want to abstract a return type chosen by the implementation from the caller. For instance, a function may produce a collection, but not want to reveal the details of exactly what kind of collection it is. This may be because the implementer wants to reserve the right to change the collection type in future versions, or because the implementation uses composed
lazy
transforms and doesn't want to expose a long, brittle, confusing return type in its interface. At first, one might try to use an existential in this situation:func evenValues<C: Collection>(in collection: C) -> Collection where C.Element == Int { return collection.lazy.filter { $0 % 2 == 0 } }
but Swift will tell you today that
Collection
can only be used as a generic constraint, leading someone to naturally try this instead:func evenValues<C: Collection, Output: Collection>(in collection: C) -> Output where C.Element == Int, Output.Element == Int { return collection.lazy.filter { $0 % 2 == 0 } }
but this doesn't work either, because as noted above, the
Output
generic argument is chosen by the caller—this function signature is claiming to be able to return any kind of collection the caller asks for, instead of one specific kind of collection used by the implementation.
It's possible that the opaque return type syntax (some Publisher
) will be extended to support this use someday.
So you have two options today:
- Change your return type to be the actual generic-heavy type of your publisher (like
Publishers.FlatMap<Publishers.CombineMany<Publishers.Just<etc etc>>>
). - Erase your publisher to
AnyPublisher
and return that.
Usually you go with the second option, because it's much easier to read and write. However, you will sometimes see a method that uses the first option. For example, Combine's own combineLatest
operator has a variant that takes a closure to transform the combined values, and it returns Publishers.Map<Publishers.CombineLatest<Self, P>, T>
instead of erasing to AnyPublisher<T, Failure>
.
If you don't like spelling out eraseToAnyPublisher
all over the place, you can give it your own, shorter name:
extension Publisher {
var erased: AnyPublisher<Output, Failure> { eraseToAnyPublisher() }
}
回答2:
With opaque return the type is defined by what is exactly returned from closure, so you can use just
func createPublisher() -> some Publisher {
return Just(1)
}
let cancellable = createPublisher()
.print()
.sink(receiveCompletion: { _ in
print(">> done")
}) { value in
print(">> \(value)")
}
// ... all other code here
and it works. Tested with Xcode 11.4.
回答3:
I had no luck with some Publisher
(annoying restriction).
One option is to use AnyPublisher
:
func a() -> AnyPublisher<(a: Int, b: String), Never> {
return Just((a: 1, b: "two")).eraseToAnyPublisher()
}
func b() -> AnyPublisher<String, Never> {
return a().map(\.b).eraseToAnyPublisher()
}
a().sink(receiveValue: {
let x = $0 // (a: 1, b: "two)
})
b().sink(receiveValue: {
let x = $0 // "two"
})
Alternatively, the "Apple way" (what they use in the standard library) seems to be type aliases (or wrapper structs):
enum PublisherUtils {
typealias A = Just<(a: Int, b: String)>
typealias B = Publishers.MapKeyPath<A, String>
// or implement a simple wrapper struct like what Combine does
}
func a() -> PublisherUtils.A {
return Just((a: 1, b: "two"))
}
func b() -> PublisherUtils.B {
return a().map(\.b)
}
a().sink(receiveValue: {
let x = $0 // (a: 1, b: "two)
})
b().sink(receiveValue: {
let x = $0 // "two"
})
This is the purpose of the Publishers
namespace in the Combine framework.
Structs are more opaque than type aliases. Type aliases can result in error messages like Cannot convert Utils.MyTypeAlias (aka 'TheLongUnderlyingTypeOf') to expected type ABC
, so the closest you can get to proper opaque types is probably to use a struct, which is essentially what the AnyPublisher
is.
来源:https://stackoverflow.com/questions/61553264/is-there-a-way-to-avoid-using-anypublisher-erasetoanypublisher-all-over-the-plac