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

Is there a way to avoid using AnyPublisher/eraseToAnyPublisher all over the place?

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

但是,有一点非常不同(并且有点烦人),那就是 Publisher 协议不为其 OutputFailure 类型使用泛型;它改用关联类型。

这意味着我不能指定多态 Publisher 类型(例如 Publisher<Int, Error>),而只是 return 任何符合 Publisher 的类型那些类型。我需要改用 AnyPublisher<Int, Error>,而且我被迫到处都包含 eraseToAnyPublisher()

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

有没有办法让我拥有一个函数 returns some Publisher 并为 OutputFailure 使用特定类型?

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

我正在想象这样的事情:

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)
    }
}