有没有办法避免到处使用 AnyPublisher/eraseToAnyPublisher?

jch*_*tel 14 swift combine opaque-types

我只是在学习如何使用Combine。我有使用 Rx(RxSwift 和 RxJava)的经验,我注意到它非常相似。

然而,完全不同(有点烦人)的一件事是Publisher协议不对其OutputFailure类型使用泛型;它使用关联类型代替。

这意味着我无法指定多态Publisher类型(例如Publisher<Int, Error>)并简单地返回符合Publisher这些类型的任何类型。我需要AnyPublisher<Int, Error>改用,我被迫在eraseToAnyPublisher()所有地方都包括在内。

如果这是唯一的选择,那么我会忍受它。但是,我最近还了解了 Swift 中的不透明类型,我想知道是否可以使用它们来解决这个问题。

有没有办法,让我有,比方说,一个函数,返回some Publisher和使用的具体类型OutputFailure

这似乎是不透明类型的完美案例,但我不知道是否有办法既使用不透明类型又指定关联类型。

我正在想象这样的事情:

func createPublisher() -> some Publisher where Output = Int, Failure = Error {
    return Just(1)
}
Run Code Online (Sandbox Code Playgroud)

rob*_*off 21

在撰写本文时,Swift 还没有您想要的功能。Joe Groff 在他的“改进泛型 UI”文档的标题为“函数返回缺少类型级抽象”的部分中具体描述了缺失的内容

但是,通常希望从调用者那里抽象出由实现选择的返回类型。例如,一个函数可能会生成一个集合,但不想透露它到底是什么类型的集合的细节。这可能是因为实现者希望保留在未来版本中更改集合类型的权利,或者因为实现使用组合lazy转换并且不想在其接口中公开长、脆弱、令人困惑的返回类型。一开始,人们可能会尝试在这种情况下使用存在词:

func evenValues<C: Collection>(in collection: C) -> Collection where C.Element == Int {
  return collection.lazy.filter { $0 % 2 == 0 }
}
Run Code Online (Sandbox Code Playgroud)

但是 Swift 今天会告诉你,这Collection只能用作通用约束,导致有人自然而然地尝试这样做:

func evenValues<C: Collection, Output: Collection>(in collection: C) -> Output
  where C.Element == Int, Output.Element == Int
{  
  return collection.lazy.filter { $0 % 2 == 0 }
}
Run Code Online (Sandbox Code Playgroud)

但这也不起作用,因为如上所述,Output 泛型参数是由调用者选择的——这个函数签名声称能够返回调用者要求的任何类型的集合,而不是由调用者使用的一种特定类型的集合实施。

有可能some Publisher有一天不透明的返回类型语法 ( ) 将被扩展以支持这种使用。

今天你有三个选择。为了理解它们,让我们考虑一个具体的例子。假设您想从 URL 中获取一个整数文本列表,每行一个,并将每个整数作为单独的输出发布:

return dataTaskPublisher(for: url)
    .mapError { $0 as Error }
    .flatMap { data, response in
        (response as? HTTPURLResponse)?.statusCode == 200
            ? Result.success(data).publisher
            : Result.failure(URLError(.resourceUnavailable)).publisher
    }
    .compactMap { String(data: $0, encoding: .utf8) }
    .map { data in
        data
            .split(separator: "\n")
            .compactMap { Int($0) }
    }
    .flatMap { $0.publisher.mapError { $0 as Error } }

Run Code Online (Sandbox Code Playgroud)

选项 1:说明返回类型

您可以使用完整的、复杂的返回类型。它看起来像这样:

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 { $0.publisher.mapError { $0 as Error } }
    }
}
Run Code Online (Sandbox Code Playgroud)

我自己没有弄清楚返回类型。我将返回类型设置为Int,然后编译器告诉我这Int不是正确的返回类型,并且错误消息包括正确的返回类型。这不太好,如果您更改实现,则必须找出新的返回类型。

选项 2:使用 AnyPublisher

添加.eraseToAnyPublisher()到发布者的末尾:

extension URLSession {
    func ints(from url: URL) -> AnyPublisher<Int, Error> {
        return dataTaskPublisher(for: url)
            ... blah blah blah ...
            .flatMap { $0.publisher.mapError { $0 as Error } }
            .eraseToAnyPublisher()
    }
}
Run Code Online (Sandbox Code Playgroud)

这是常见且简单的解决方案,通常也是您想要的。如果您不喜欢拼写eraseToAnyPublisher,您可以编写自己的Publisher扩展名来使用较短的名称,如下所示:

extension Publisher {
    var typeErased: AnyPublisher<Output, Failure> { eraseToAnyPublisher() }
}
Run Code Online (Sandbox Code Playgroud)

选项 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 { $0.publisher.mapError { $0 as Error } }
            ... blah blah blah ...
            .subscribe(subscriber)
    }
}
Run Code Online (Sandbox Code Playgroud)