工作生活

Understanding Opaque Return Type

2019-07-02  本文已影响0人  simpletonking

本文 翻译自 https://swiftrocks.com/understanding-opaque-return-types-in-swift.html

Understanding Opaque Return Types in Swift

为什么 SwiftUI 的返回类型 是 some View?

为什么 不能返回一个 普通的 协议?

Opaque Types 是什么?

Opaque Types 是一个 在 Swift 5.1 新添加的特性,它是很大一部分新的SwiftUI框架的功能。它解决了协议的使用问题和给Swift API的设计 提供了 新的使用 public APIs的 创建和使用可能,

在 Swift 5.1 之前构建APIs

为了去明白 Opaque Types 是什么 ,让我们先看看现在构建 public APs 的可行方案。

让我们 首先假设我们 有一个 payment(支付)的框架,它有一个 返回用户最喜欢的 信用卡的方法 ,返回值是 CreditCard(信用卡) struct。


public func favoriteCreditCard() -> CreditCard {

    return getLastUsedCreditCard()

}

这个方法对于内部的 API 来说非常棒,当对于一个 public 的 framework 来说就不是一个好主意了。用户可能没必要去获取到 CreditCard 这个类型。它可能包含的一些信息是我们不想让用户能够去交流的信息,像 hashing 方法如何去执行。

你也可以通过设置方法为public和private来解决。但是如果你想完全隐藏掉这个类型的存在呢?

现在,你可以通过协议来完成这个目标,通过将实现和类型细节抽象进入 protocol 中,用一个统一的协议名字来代替这个类型。

protocol PaymentType { /* ... */ }
struct CreditCard: PaymentType { /* ... */ }

public func favoriteCreditCard() -> PaymentType {
    return getLastUsedCreditCard() // () -> CreditCard
}

有了这个协议,我们甚至可以重写 favoriteCreditCard() 为泛型方法,它返回了不是 Credit cards type的 Payment 类型。

struct ApplePay: PaymentType { /* ... */ }

func favoritePaymentType() -> PaymentType {
    if likesApplePay {
        return ApplePay()
    } else {
        return getLastUsedCreditCard()
    }
}

不幸的是,这个协议的使用会产生一个大问题。因为 Swift 的协议会丢弃类型的基本标识,如果一个协议如果有 关联类型(包括 Self )的要求,比如 从 Equatable 继承的协议,那么它将不能像下面这么做。

protocol PaymentType: Equatable { /* ... */ }

public func favoriteCreditCard() -> PaymentType {
    return getLastUsedCreditCard() // () -> CreditCard
}
// Error: Protocol 'PaymentType' can only be used as a generic constraint because it has Self or associated type requirements

这意味着说 api 的使用者将不能比较两个 payment 类型,即使他们的底层类型是相同的

let creditCard = favoriteCreditCard()
let anotherCreditCard = mostRecentCreditCard()

creditCard == anotherCreditCard // `PaymentType` does not conform to Equatable.

在 Swift5.1 之前,这个问题的解决方法是通过特别的泛型方式来解决,将所有信息都放入一个类中或者使用类型擦除技术。以上这些方法的使用都会使 API 的使用更加复杂,并且会带来不同类型的问题到 app 中。例如,考量下下面这个方法:

func getHashedCard() -> HashedObject<CreditCard>

泛型的使用可以解决上面所提到的协议问题,但是它们同时也使API更难处理对待。可能使用HashedObject 在内部是非常重要的,但是使用者更可能不需要知道这些,返回一个PaymentType object 类型对象代替可能会更好,但是协议的局限性又阻止了这样的可能性。

Opaque Return Types

这个问题在 Swift5.1 的 Opaque Return Types 到来后又个明确的解决方法了。如果你有一个方法返回了一个由 protocol 掩盖的具体实体类型——就像上文所使用的实体 CreditCard type 由
不是很有用的 PaymentType 所掩盖的,你可以通过将返回类型改成 some {type name} 来使用
Opaque Return Types 。

public func favoriteCreditCard() -> some PaymentType {
    return getLastUsedCreditCard() // () -> CreditCard
}

当这么做了后,返回的实际类型将会是确切的实际类型 CreditCard ,但是编译器将会假装用协议来代替它。这意味着当使用者看到这个通常的协议,它将有所有这个实体类型的能力:

let creditCard = favoriteCreditCard() // some 'PaymentType'
let debitCard = mostRecentCreditCard() // some 'PaymentType'

creditCard == debitCard // Now works, because two concrete CreditCards can be compared.

这个可以成功运行的理由是因为用了很具有想象力的编译器魔法——返回值一直是 CreditCard,它仅仅在你的编码层面被隐藏了。下面这个是 favoriteCreditCard() 被后的情况:

let favoriteCreditCardMangledName = "$s3MyApp9favoriteCreditCardQryF"
public func favoriteCreditCard() -> @_opaqueReturnTypeOf(favoriteCreditCardMangledName, 0) {
    return getLastUsedCreditCard() // () -> CreditCard
} like after compiling:

所有 favoriteCreditCard() 返回的 some PaymentType 的引用都被内部属性所代替-- 在这个内部属性执行过程中,将会带上标识并且用它来提供确切类型, 即 CreditCard ,它将会储存在 方法的 AST(抽象语法树)元数据中。

// The definition of favoriteCreditCard() contains:
(opaque_result_decl
  (opaque_type interface type='(some PaymentType).Type' naming_decl="favoritePaymentType()" underlying:
    substitution τ_0_0 -> CreditCard)))

因此,虽然在 IDE 中,你将会被阻止获取特殊的 CreditCard 的属性,但是在运行时,下面这个

public func favoriteCreditCard() -> some PaymentType {
    return getLastUsedCreditCard() // () -> CreditCard
}

与直接返回CreditCard相同。

public func favoriteCreditCard() -> CreditCard {
    return getLastUsedCreditCard() // () -> CreditCard
}

为什么它是有用的呢?

Opaque Return Types 是一个 给了api的使用者一个不需要暴露这些返回类型的实际类型,却又能获得它们的能力的类型。 某些时候,知道协议的基础类型是不必要的,但是你需要这些类型的能力去继续运行。PaymentType 这个例子对于 Opaque Return Types 来说可能太简单了,因此我们来看看在几个内部辅助类型上面是如何应用的,例如 lazy funcitons:

let lazyMap = [1,2,3].map { $0 * 2 }
let lazyFilter = lazyMap.filter { $0.isMultiple(of: 2) }
let lazyDrop = lazyFilter.drop { $0 != 2 }

lazyMap的类型是 LazyMapSequence<[Int], Int>,
lazyFilter的类型是LazyFilterSequence<LazyMapSequence<[Int], Int>>,
lazyDrop的类型是 LazyDropWhileSequence<LazyFilterSequence<LazyMapSequence<[Int], Int>>>

创建一个基于 Sequence protocol 的方法将会避免方法的使用者使用这个类型所具有的功能,但是返回一个如此巨大的特殊的泛型类型将是很疯狂的。
使用者很可能不在意是什么内部的辅助类型组成了这个对象。有了 opaque return types,你可以作为一个普通的 Sequence 类型安全的返回它,同时仍然保持原始类型的能力。

func getLazyDrop() -> some Sequence {
    let lazyMap = [1,2,3].lazy.map { $0 * 2 }
    let lazyFilter = lazyMap.filter { $0.isMultiple(of: 2) }
    let lazyDrop = lazyFilter.drop { $0 != 2 }
    return lazyDrop
}

这就是 为什么 SwiftUI 返回的是 some View—— 你需要实际的对象去比较,处理和在屏幕上定位它们,但是在大多数例子下,这个 View 到底是什么并不重要,我们只需要知道它是其中之一。最后,这就是一个工具,它能让你的编码更轻松。

转载请注明来源

上一篇下一篇

猜你喜欢

热点阅读