本系列文章将从实践技巧、完结原理以及追踪言语更新等方面对 Swift Protocol 翻开深入谈论。首要内容有:

  • Swift Protocol 反面的故事(实践)

  • Swift Protocol 反面的故事(理论)

  • Swift Protocol 反面的故事(Swift 5.6/5.7)

本文是系列文章第一篇,首要介绍实践技巧,以一个 Protocol 相关的编译差错为引,通过实例对 Type Erasure、Opaque Types 、Generics 以及 Phantom Types 做了较具体的谈论。它们关于写出更优、更雅的 Swift 代码有必定的协助。

本文一同宣布于我的个人博客


Swift 推崇面向协议编程 (POP, Protocol Oriented Programming),因而 Protocol 在 Swift 中就显得尤为重要。

但本文要谈论的既不是 Protocol 的运用,也不是 POP。

我们的谈论从一个编译差错初步:

Protocol ‘Equatable’ can only be used as a generic constraint because it has Self or associated type requirements.

2022 年 06 月 30 日更新:从 Swift 5.7 起不再会有这个差错了,详情请见 Swift Protocol 反面的故事(Swift 5.6/5.7)

关于 Swift 开发者来说上面这个编译差错应该不陌生。

其字面意思不难理解:含有 Self 或相关类型的协议只能用作泛型捆绑,不能单独作为类型运用。

Why?

由于 Swift 是类型安全的言语 (type-safe language)。

why?

上面这个解说是句『 正确的废话 』,没有说到点子上。

下面我们以一个 Demo 为基础翻开今天的谈论 (GitHub – zxfcumtcs/MarkdownDemo: Swift Protocol Demo):

Swift Protocol 背面的故事(实践)
如上图,MarkdownEditor 是一个 Markdown 格式的编辑器。

为了处理不同的 Markdown 格式,我们定义了协议 MarkdownBuilder, 其作为揭穿接口曝露给事务方:

public protocol MarkdownBuilder: Equatable, Identifiable {
  var style: String { get }
  func build(from text: String) -> String
}

由于有判等需求,MarkdownBuilder 继承了 Equatable 协议。

假定我们直接将 MarkdownBuilder 作为类型运用,如:var builder: MarkdownBuilder ,就会报上面的差错。

由于,Equatable 有 Self requirements:要求 == 操作符的两个参数 lhsrhs 的类型有必要相同 (注意是准确的类型,而不是说只需遵循 Equatable 即可)。

public protocol Equatable {
  static func == (lhs: Self, rhs: Self) -> Bool
}

假定,答应有 Self requirements / Associated Type 的 Protocol 作为类型运用,就会出现以下状况,而编译器却无能为力:

let lhs: Equatable = 1           // Int
let rsh: Equatable = "1"         // String
lhs == rsh                       // ?!, 不同类型的值可以判等

关于 Associated Type 也是相同的道理:

// 用于校验电话号码是否合法
// 由于电话号码可以有多种表达格式
// 抽取了协议,并完结了Int、String两种格式
//
protocol PhoneNumberVerifier {
  associatedtype Phone
  func verify(_ model: Phone) -> Bool
}
struct IntPhoneNumberVerifier: PhoneNumberVerifier {
  func verify(_ model: Int) -> Bool {
    // do some verify
  }
}
struct StrPhoneNumberVerifier: PhoneNumberVerifier {
  func verify(_ model: String) -> Bool {
    // do some verify
  }
}
let verifiers: [PhoneNumberVerifier] = [...]
verifiers.forEach { verifier in
  verifier.verify(???) // 这儿的参数怎样传?Int? String? 编译器无法保证类型安全
}

说这么多,归根到底是由于 Protocol 是运行时特性,而其附带的 Self requirements / Associated Type 却需求在编译时保证。其效果必定凉凉~

Generics 是编译期特性,在编译时就能明晰泛型的具体类型,故有 Self requirements/Associated Type 的 Protocol 只能作为其捆绑运用。

Type Erasure


回到上节说到的 Markdown 编辑器:MarkdownEditor,我们完结了4种格式的 MarkdownBuilder:

extension MarkdownBuilder {
  public var id: String { style }
}
// 斜体
//
fileprivate struct ItalicsBuilder: MarkdownBuilder {
  public var style: String { "*Italics*" }
  public func build(from text: String) -> String { "*(text)*" }
}
// 粗体
//
fileprivate struct BoldBuilder: MarkdownBuilder {
  public var style: String { "**Bold**" }
  public func build(from text: String) -> String { "**(text)**" }
}
// 删除线
//
fileprivate struct StrikethroughBuilder: MarkdownBuilder {
  public var style: String { "~Strikethrough~" }
  public func build(from text: String) -> String { "~(text)~" }
}
// 超链接
//
fileprivate struct LinkBuilder: MarkdownBuilder {
  public var style: String { "[Link](link)" }
  public func build(from text: String) -> String { "[(text)](https://github.com)"}
}

struct MarkdownView: View 是整个 Demo 的主界面,需求在其间存储一切支撑的 Markdown Builder,以及当前选中的 Builder。

所以,我们不加思索地写下了以下代码:

struct MarkdownView: View {
  private let allBuilders: [MarkdownBuilder]
  private var selectedBuilders: [MarkdownBuilder]
}

效果可想而知!

怎样办?

Generics? 在这儿好像行不通!

将一切支撑的 Builder 逐个定义出来?

太蠢了!且不符合『 OCP 』准则。

此时,就需求用到本节的主角:Type Erasure (类型擦除)。

Type Erasure 是一项通用技术,并非 Swift 特有,中心思想是在编译期擦除 (转化) 原有类型,使其对事务方不可见。

有多种方法可以完结 Type Erasure,如:Boxing、Closures 等。

在 MarkdownEditor 中,我们通过 Boxing 完结 Type Erasure,简略讲就是对原有类型做一次封装 (Wrapper):

public struct AnyBuilder: MarkdownBuilder {
  public let style: String
  public var id: String { "AnyBuilder-(style)" }
  private let wrappedApply: (String) -> String
  public init<B: MarkdownBuilder>(_ builder: B) {
    style = builder.style
    wrappedApply = builder.build(from:)
  }
  public func build(from text: String) -> String {
    wrappedApply(text)
  }
  public static func == (lhs: AnyBuilder, rhs: AnyBuilder) -> Bool {
    lhs.id == rhs.id
  }
}

几个要害点:

  • AnyBuilder 完结了 MarkdownBuilder协议,(一般状况下 Wrapper 都需求完结待封装的协议);
  • init 是泛型方法,并将参数传递过来的 stylebuild(from:) 存储下来;
  • 在其自身的build(from:)方法中直接调用存储的 wrappedApply,其自身相当于一个转发署理。

一同,扩展 MarkdownBulider

public extension MarkdownBuilder {
  func asAnyBuilder() -> AnyBuilder {
    AnyBuilder(self)
  }
}

现在,我们就可以愉快地在 MarkdownView 中运用 AnyBuilder 了:

struct MarkdownView: View {
  private let allBuilders: [AnyBuilder]  
  private var selectedBuilders: [AnyBuilder]
}

由于有上面的 MarkdownBuilder 扩展,可以通过 2 种方法生成 AnyBuilder 实例:

  • BoldBuilder().asAnyBuilder()
  • AnyBuilder(BoldBuilder())

在 Swift 标准库中有许多通过 Boxing 完结的 Type Erasure ,如: AnySequenceAnyHashableAnyCancellable等等。

以 Any 为前缀的简直都是。

Opaque Types


假定,我们预备将 MarkdownEditor 做成一个独立的三方库,而且除了 MarkdownBuilder 协议,不计划曝露任何其他的完结细节以添加其灵活性。

即,ItalicsBuilderBoldBuilderStrikethroughBuilder 以及 LinkBuilder 都是库私有的。

怎么做?

又一次不加思索地写下了以下代码:

public func italicsBuilder() -> MarkdownBuilder {
  ItalicsBuilder()
}
public func boldBuilder() -> MarkdownBuilder {
  BoldBuilder()
}
public func strikethroughBuilder() -> MarkdownBuilder {
  StrikethroughBuilder()
}
public func linkBuilder() -> MarkdownBuilder {
  LinkBuilder()
}

我们希望通过 public func 为事务方创立相应的 Builder 实例,一同以接口的方法回来。

理想饱满,现实骨感!

相同的差错在等着你!

怎样办?

轮到本节主角 Opaque Types 上台了!

简略讲,Opaque Types 就是让函数/方法的回来值是协议,而不是具体的类型。

A function or method with an opaque return type hides its return value’s type information. Instead of providing a concrete type as the function’s return type, the return value is described in terms of the protocols it supports.

几个要害点:

  • 要害字 some,需在回来协议类型前添加 some 要害词,如: public func regularBuilder() -> some MarkdownBuilder 而不是 public func regularBuilder() -> MarkdownBuilder

  • Opaque Types 与直接回来协议类型的最大区别是:

    • Opaque Types 只是对运用方(人)躲藏了具体类型细节,编译器是知道具体类型的;

    • 而直接回来协议类型,则是运行时行为,编译器是无法知道的;

    • 如下代码,编译器是明晰知道 italicsBuilder 方法的回来值类型是 ItalicsBuilder,但方法调用方却只知道回来值遵循了 MarkdownBuilder 协议。然后也就达到了躲藏完结细节的目的;

      public func italicsBuilder() -> some MarkdownBuilder {
        ItalicsBuilder()
      }
      
    • 正是由于编译器需求明晰确定 Opaque Types 反面的真实类型,故不能在 Opaque Types 方法中回来不同的类型值,如下面这样是不答应的 (Opaque Types 归于编译期特性):

      public func italicsBuilder() -> some MarkdownBuilder {
        if ... {
          return ItalicsBuilder()
        }
        else {
          return BoldBuilder()
        }
      }
      

好了,现在我们知道只需在上述不加思索写出的代码中参加 some 要害字即可,不再赘述。

在 SwiftUI 中,许多运用到 Opaque Types。甚至可以说 Opaque Types 是为 SwiftUI 而生的。

Phantom Types


Phantom Types 自身与本文谈论的内容相关性不大,作为相似的概念,我们简略介绍一下。

Phantom Types 也非 Swift 特有的,归于一种通用编码技巧。

Phantom Types 没有严厉的定义,一般表述是:出现在泛型参数中,但没有被真实运用。

如下代码中的 Role (比如来自 How to use phantom types in Swift),它只出现在泛型参数中,在 Employee 完结中并未运用:

struct Employee<Role>: Equatable {
    var name: String
}

What?

Phantom Types 有何用?

用于对类型做进一步的强化。

Employee 可能有不同的人物,如:Sales、Programmer 等,我们将其定义为空 enum:

enum Sales { }
enum Programmer { }

由于 Employee 完结了 Equatable,可以在两个实例间进行判等操作。

但判等操作明显只需在同一种人物间进行才有意义:

let john = Employee<Sales>.init(name: "John")
let sea = Employee<Programmer>.init(name: "Sea")
john == sea

正是由于 Phantom Types 在起作用,上述代码中的判等操作编译无法通过:

Cannot convert value of type ‘Employee’ to expected argument type ‘Employee’

将 Phantom Types 定义成空 enum,使其无法被实例化,然后真实满足 Phantom Types 语义。

由于 Swift 没有 NameSpacing 这样的要害字,故通常用空 enum 来完结相似的效果,如 Apple Combine Framework 中的 Publishers:

public enum Publishers {}

然后在 extension 中添加具体 Publisher 类型的定义,如:

extension Publishers {
  struct First<Upstream>: Publisher where Upstream: Publisher {
    ...
  }
}

然后,可以通过 Publishers.First 的方法引证具体的 Publisher。

关于恰当运用命名空间的好处在:Five powerful, yet lesser-known ways to use Swift enums 中有一段精彩描述:

Using the above kind of namespacing can be a great way to add clear semantics to a group of types without having to manually attach a given prefix or suffix to each type’s name.

So while the above First type could instead have been named FirstPublisher and placed within the global scope, the current implementation makes it publicly available as Publishers.First — which both reads really nicely, and also gives us a hint that First is just one of many publishers available within the Publishers namespace.

It also lets us type Publishers. within Xcode to see a list of all available publisher variations as autocomplete suggestions.

小结


Swift 作为 POP (Protocol Oriented Programming) 的发起者,Protocol 的方位自然十分重要,Swift 赋于其强壮才干。

一同,Swift 又是类型安全的,因而关于带有 Self requirements / Associated Type 的 Protocol 在运用上又有必定的束缚。

结合实例,本文首要介绍了怎么通过 Type Erasure、Opaque Types 以及 Generics 等方法处理上述束缚。

在 Opaque Return Types and Type Erasure 这篇文章中作者分别从库的开发者 (Liam)、编译器 (Corrine)、运用方 (Abbie) 的视角剖析了他们是否了解 Protocols、Opaque Types、Generics 以及 Type Erasure 反面的私密:

Swift Protocol 背面的故事(实践)
如上图:

  • Protocols:

    • 协议自身具有躲藏完结细节以及运行时实例化的特性,故编译器、运用方无法知道其反面对应的真实类型;
    • 但,作为库的开发者 (代码是他写的),明晰知道 Protocol 反面可能对应的一切真实类型。
  • Opaque Types:

    • 同 Protocols,库的开发者肯定是知道的;
    • 由于 Opaque Types 束缚只能对应一种真实类型,并在编译期需明晰,故编译器是知道的;
    • 关于运用方来说,他们看到的仍是躲藏了细节的 Protocol。
  • Generics:

    • 泛型是将类型决定权让给运用方的,故库的开发者是不知道真实类型的,而运用方知道;
    • 泛型归于编译期行为,故编译器能明晰知道泛型关于的真实类型。
  • Type Erasure:

    • 类型擦除归于运用方行为,用于躲避编译差错等,故只需运用方知道。

参考资料

swift-evolution Opaque Result Types

OpaqueTypes

Different flavors of type erasure in Swift

Opaque Return Types and Type Erasure

Phantom types in Swift

How to use phantom types in Swift

swift/TypeMetadata.rst at main apple/swift GitHub

swift/TypeLayout.rst at main apple/swift GitHub

Swift TypeMetadata

Understanding Swift Performance WWDC2016

Swift.org – Whole-Module Optimization in Swift 3