有没有办法避免到处使用 AnyPublisher/eraseToAnyPublisher ?
Is there a way to avoid using AnyPublisher/eraseToAnyPublisher all over the place?
我正在学习如何使用 Combine。我有使用 Rx(RxSwift 和 RxJava)的经验,我注意到它们非常相似。
但是,有一点非常不同(并且有点烦人),那就是 Publisher
协议不为其 Output
和 Failure
类型使用泛型;它改用关联类型。
这意味着我不能指定多态 Publisher
类型(例如 Publisher<Int, Error>
),而只是 return 任何符合 Publisher
的类型那些类型。我需要改用 AnyPublisher<Int, Error>
,而且我被迫到处都包含 eraseToAnyPublisher()
。
如果这是唯一的选择,那我就忍了。但是,我最近还在 Swift 中了解了不透明类型,我想知道我是否可以使用它们来解决这个问题。
有没有办法让我拥有一个函数 returns some Publisher
并为 Output
和 Failure
使用特定类型?
这似乎是不透明类型的完美案例,但我不知道是否有办法让我既使用不透明类型又指定关联类型。
我正在想象这样的事情:
func createPublisher() -> some Publisher where Output = Int, Failure = Error {
return Just(1)
}
使用不透明 return 时,类型由闭包 return 中的内容定义,因此您可以只使用
func createPublisher() -> some Publisher {
return Just(1)
}
let cancellable = createPublisher()
.print()
.sink(receiveCompletion: { _ in
print(">> done")
}) { value in
print(">> \(value)")
}
// ... all other code here
并且有效。使用 Xcode 11.4.
测试
我没有遇到 some Publisher
(烦人的限制)。
一种选择是使用 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 = [=10=] // (a: 1, b: "two)
})
b().sink(receiveValue: {
let x = [=10=] // "two"
})
或者,"Apple way"(他们在标准库中使用的)似乎是类型别名(或包装器结构):
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 = [=11=] // (a: 1, b: "two)
})
b().sink(receiveValue: {
let x = [=11=] // "two"
})
这是 Combine 框架中 Publishers
命名空间的用途。
结构比类型别名更不透明。类型别名可能会导致错误消息,如 Cannot convert Utils.MyTypeAlias (aka 'TheLongUnderlyingTypeOf') to expected type ABC
,因此最接近正确不透明类型的方法可能是使用结构,这实际上就是 AnyPublisher
。
Swift,截至撰写本文时,还没有您想要的功能。 Joe Groff 特别描述了他的 “Improving the UI of generics” document:
标题为“Type-level 函数 returns 缺少抽象”的部分中缺少的内容
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 { [=10=] % 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 { [=11=] % 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.
有朝一日,不透明的 return 类型语法 (some Publisher
) 可能会得到扩展以支持这种用法。
今天你有三个选择。为了理解它们,让我们考虑一个具体的例子。假设您要从 URL 中获取一个整数文本列表,每行一个,并将每个整数发布为单独的输出:
return dataTaskPublisher(for: url)
.mapError { [=12=] as Error }
.flatMap { data, response in
(response as? HTTPURLResponse)?.statusCode == 200
? Result.success(data).publisher
: Result.failure(URLError(.resourceUnavailable)).publisher
}
.compactMap { String(data: [=12=], encoding: .utf8) }
.map { data in
data
.split(separator: "\n")
.compactMap { Int([=12=]) }
}
.flatMap { [=12=].publisher.mapError { [=12=] as Error } }
选项 1:拼出 return 类型
您可以使用完整、复杂的 return 类型。看起来像这样:
extension URLSession {
func ints(from url: URL) -> Publishers.FlatMap<
Publishers.MapError<
Publishers.Sequence<[Int], Never>,
Error
>,
Publishers.CompactMap<
Publishers.FlatMap<
Result<Data, Error>.Publisher,
Publishers.MapError<
URLSession.DataTaskPublisher,
Error
>
>,
[Int]
>
> {
return dataTaskPublisher(for: url)
... blah blah blah ...
.flatMap { [=13=].publisher.mapError { [=13=] as Error } }
}
}
我自己没弄清楚 return 类型。我将 return 类型设置为 Int
然后编译器告诉我 Int
不是正确的 return 类型,错误消息包括正确的 return类型。这不太好,如果您更改实现,您将不得不找出新的 return 类型。
选项 2:使用 AnyPublisher
在发布者末尾添加.eraseToAnyPublisher()
:
extension URLSession {
func ints(from url: URL) -> AnyPublisher<Int, Error> {
return dataTaskPublisher(for: url)
... blah blah blah ...
.flatMap { [=14=].publisher.mapError { [=14=] as Error } }
.eraseToAnyPublisher()
}
}
这是常见且简单的解决方案,通常也是您想要的。如果您不喜欢拼写 eraseToAnyPublisher
,您可以编写自己的 Publisher
扩展名来使用更短的名称,如下所示:
extension Publisher {
var typeErased: AnyPublisher<Output, Failure> { eraseToAnyPublisher() }
}
选项 3:编写您自己的 Publisher
类型
您可以用自己的类型包装您的发布商。您的类型的 receive(subscriber:)
构建“真正的”发布者,然后将订阅者传递给它,如下所示:
extension URLSession {
func ints(from url: URL) -> IntListPublisher {
return .init(session: self, url: url)
}
}
struct IntListPublisher: Publisher {
typealias Output = Int
typealias Failure = Error
let session: URLSession
let url: URL
func receive<S: Subscriber>(subscriber: S) where
S.Failure == Self.Failure, S.Input == Self.Output
{
session.dataTaskPublisher(for: url)
.flatMap { [=16=].publisher.mapError { [=16=] as Error } }
... blah blah blah ...
.subscribe(subscriber)
}
}
我正在学习如何使用 Combine。我有使用 Rx(RxSwift 和 RxJava)的经验,我注意到它们非常相似。
但是,有一点非常不同(并且有点烦人),那就是 Publisher
协议不为其 Output
和 Failure
类型使用泛型;它改用关联类型。
这意味着我不能指定多态 Publisher
类型(例如 Publisher<Int, Error>
),而只是 return 任何符合 Publisher
的类型那些类型。我需要改用 AnyPublisher<Int, Error>
,而且我被迫到处都包含 eraseToAnyPublisher()
。
如果这是唯一的选择,那我就忍了。但是,我最近还在 Swift 中了解了不透明类型,我想知道我是否可以使用它们来解决这个问题。
有没有办法让我拥有一个函数 returns some Publisher
并为 Output
和 Failure
使用特定类型?
这似乎是不透明类型的完美案例,但我不知道是否有办法让我既使用不透明类型又指定关联类型。
我正在想象这样的事情:
func createPublisher() -> some Publisher where Output = Int, Failure = Error {
return Just(1)
}
使用不透明 return 时,类型由闭包 return 中的内容定义,因此您可以只使用
func createPublisher() -> some Publisher {
return Just(1)
}
let cancellable = createPublisher()
.print()
.sink(receiveCompletion: { _ in
print(">> done")
}) { value in
print(">> \(value)")
}
// ... all other code here
并且有效。使用 Xcode 11.4.
测试我没有遇到 some Publisher
(烦人的限制)。
一种选择是使用 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 = [=10=] // (a: 1, b: "two)
})
b().sink(receiveValue: {
let x = [=10=] // "two"
})
或者,"Apple way"(他们在标准库中使用的)似乎是类型别名(或包装器结构):
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 = [=11=] // (a: 1, b: "two)
})
b().sink(receiveValue: {
let x = [=11=] // "two"
})
这是 Combine 框架中 Publishers
命名空间的用途。
结构比类型别名更不透明。类型别名可能会导致错误消息,如 Cannot convert Utils.MyTypeAlias (aka 'TheLongUnderlyingTypeOf') to expected type ABC
,因此最接近正确不透明类型的方法可能是使用结构,这实际上就是 AnyPublisher
。
Swift,截至撰写本文时,还没有您想要的功能。 Joe Groff 特别描述了他的 “Improving the UI of generics” document:
标题为“Type-level 函数 returns 缺少抽象”的部分中缺少的内容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 { [=10=] % 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 { [=11=] % 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.
有朝一日,不透明的 return 类型语法 (some Publisher
) 可能会得到扩展以支持这种用法。
今天你有三个选择。为了理解它们,让我们考虑一个具体的例子。假设您要从 URL 中获取一个整数文本列表,每行一个,并将每个整数发布为单独的输出:
return dataTaskPublisher(for: url)
.mapError { [=12=] as Error }
.flatMap { data, response in
(response as? HTTPURLResponse)?.statusCode == 200
? Result.success(data).publisher
: Result.failure(URLError(.resourceUnavailable)).publisher
}
.compactMap { String(data: [=12=], encoding: .utf8) }
.map { data in
data
.split(separator: "\n")
.compactMap { Int([=12=]) }
}
.flatMap { [=12=].publisher.mapError { [=12=] as Error } }
选项 1:拼出 return 类型
您可以使用完整、复杂的 return 类型。看起来像这样:
extension URLSession {
func ints(from url: URL) -> Publishers.FlatMap<
Publishers.MapError<
Publishers.Sequence<[Int], Never>,
Error
>,
Publishers.CompactMap<
Publishers.FlatMap<
Result<Data, Error>.Publisher,
Publishers.MapError<
URLSession.DataTaskPublisher,
Error
>
>,
[Int]
>
> {
return dataTaskPublisher(for: url)
... blah blah blah ...
.flatMap { [=13=].publisher.mapError { [=13=] as Error } }
}
}
我自己没弄清楚 return 类型。我将 return 类型设置为 Int
然后编译器告诉我 Int
不是正确的 return 类型,错误消息包括正确的 return类型。这不太好,如果您更改实现,您将不得不找出新的 return 类型。
选项 2:使用 AnyPublisher
在发布者末尾添加.eraseToAnyPublisher()
:
extension URLSession {
func ints(from url: URL) -> AnyPublisher<Int, Error> {
return dataTaskPublisher(for: url)
... blah blah blah ...
.flatMap { [=14=].publisher.mapError { [=14=] as Error } }
.eraseToAnyPublisher()
}
}
这是常见且简单的解决方案,通常也是您想要的。如果您不喜欢拼写 eraseToAnyPublisher
,您可以编写自己的 Publisher
扩展名来使用更短的名称,如下所示:
extension Publisher {
var typeErased: AnyPublisher<Output, Failure> { eraseToAnyPublisher() }
}
选项 3:编写您自己的 Publisher
类型
您可以用自己的类型包装您的发布商。您的类型的 receive(subscriber:)
构建“真正的”发布者,然后将订阅者传递给它,如下所示:
extension URLSession {
func ints(from url: URL) -> IntListPublisher {
return .init(session: self, url: url)
}
}
struct IntListPublisher: Publisher {
typealias Output = Int
typealias Failure = Error
let session: URLSession
let url: URL
func receive<S: Subscriber>(subscriber: S) where
S.Failure == Self.Failure, S.Input == Self.Output
{
session.dataTaskPublisher(for: url)
.flatMap { [=16=].publisher.mapError { [=16=] as Error } }
... blah blah blah ...
.subscribe(subscriber)
}
}