有没有一种方法可以避免在整个地方使用AnyPublisher / eraseToAnyPublisher?

时间:2020-05-02 01:18:09

标签: 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)
}

3 个答案:

答案 0 :(得分:12)

在撰写本文时,Swift没有您想要的功能。乔·格罗夫(Joe Groff)在他的“Improving the UI of generics” document的标题为“函数返回缺少类型级抽象”的节中专门描述了什么:

但是,通常要提取由选择的返回类型 实施。例如,一个函数可能会产生 集合,但不想透露具体的种类 的收藏。这可能是因为实施者想要 保留在将来的版本中更改集合类型的权利,或者 因为该实现使用组合的lazy转换,并且没有 想要在其类型中公开一个长的,易碎的,令人困惑的返回类型 接口。最初,人们可能会尝试在其中使用存在性 情况:

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

但是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 }
}

,但这也不起作用,因为如上所述,Output 通用参数由调用者选择-此函数签名为 声称能够返回呼叫者要求的任何种集合 而不是 实施。

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

所以您今天有两个选择:

  • 将您的退货类型更改为发布者的实际通用类型(例如Publishers.FlatMap<Publishers.CombineMany<Publishers.Just<etc etc>>>)。
  • 将您的发布商删除到AnyPublisher并退回。

通常,您选择第二个选项,因为它更容易读写。但是,有时您会看到使用第一个选项的方法。例如,Combine自己的combineLatest运算符具有一个变体,该变体采用闭包来转换组合的值,并返回Publishers.Map<Publishers.CombineLatest<Self, P>, T>而不是擦除为AnyPublisher<T, Failure>

如果您不喜欢在各地拼写eraseToAnyPublisher,可以给它起一个简短的名字:

extension Publisher {
    var erased: AnyPublisher<Output, Failure> { eraseToAnyPublisher() }
}

答案 1 :(得分:2)

对于不透明的返回,其类型由闭包的确切返回值定义,因此您可以只使用

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的测试。

demo

答案 2 :(得分:0)

我对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 = $0 // (a: 1, b: "two)
})

b().sink(receiveValue: {
    let x = $0 // "two"
})

或者,“ Apple方式”(它们在标准库中使用的方式)似乎是类型别名(或包装器结构):

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

这是Combine框架中Publishers命名空间的目的。

结构比类型别名更不透明。类型别名可能会导致出现类似Cannot convert Utils.MyTypeAlias (aka 'TheLongUnderlyingTypeOf') to expected type ABC之类的错误消息,因此,最接近正确的不透明类型的可能是使用结构,这实际上就是AnyPublisher的含义。