Swift 作为现代、高效、安全的编程语言,其背面有许多高档特性为之支撑。

『 Swift 最佳实践 』系列对常用的语言特性逐个进行介绍,助力写出更简练、更高雅的 Swift 代码,快速完成从 OC 到 Swift 的改变。

该系列内容首要包括:

  • Optional
  • Enum
  • Closure
  • Protocol
  • Generic
  • Property Wrapper
  • Structured Concurrent
  • Result builder
  • Error Handle
  • Advanced Collections (Asyncsequeue/OptionSet/Lazy)
  • Expressible by Literal
  • Pattern Matching
  • Metatypes(.self/.Type/.Protocol)

ps. 本系列不是入门级语法教程,需求有必定的 Swift 根底

本文是系列文章的第四篇,介绍 Protocol。

Overview


Protocol 基本语法在此就不赘述,不熟悉的话主张看看官方文档:Documentation Protocols

Swift 号称 POP,为 Protocol 供给了强壮的才能,如 Extension、Default Implementation 等,深受开发者喜爱。

将 Protocol 用作类型时,需求装箱(Boxing)且办法是动态派发,有必定的功能损耗。

借助 Associated Type 使得 Protocol 更加灵敏,但又不失类型安全,值得具有。

对,本文首要便是评论这些话题 。

Extension & Default Implementation


Extension

Swift Protocol 能够像 Class/Structure/Enum 那样供给 Extension。

在 Extension 中能够完成办法、计算特点等:

  • 既能够在 Extension 中完成 Protocol 要求的办法

  • 还能够完成未出现在 Protocol 界说中的办法

Default Implementation

在扩展中完成协议要求的办法 (requirement of protocol),即为其供给默许完成。然后完成该协议的 Class/Structure/Enum 就能够直接运用默许完成:

protocol ProtocolDemo {
  func foo()
}
extension ProtocolDemo {
  func foo() {
    print("foo: in Extension")
  }
}
// 正是因为 ProtocolDemo 供给了 foo 的默许完成
// 故,ImplementDemo 能够不完成 foo,编译没问题
//
struct ImplementDemo: ProtocolDemo {}

Quiz

来个小检验 ,如下这段代码的结果是❓

  • Compiler Error?

  • Crash?

  • ?

Swift 最佳实践之 Protocol

正确答案 :

bar: in Extension

foo: in ImplementDemo

bar: in ImplementDemo

foo: in ImplementDemo

这个呢?

Compiler Error❓ Crash❓

Swift 最佳实践之 Protocol

正确答案:

bar: in Extension

foo: in ImplementDemo

bar: in Extension

foo: in ImplementDemo

Why❓

关于 foo 的行为应该好理解,便是具体完成 (ImplementDemo.foo) 覆盖默许完成。

关于 bar 的行为就不那么好理解了 ,但好像能够总结出一些规律。

关于不是协议要求的办法 (non-requirement of protocol),即只在 protocol extension 中界说的办法:

  • 关于用协议类型界说的实例 (如,protocolDemo),调用的必定是 protocol extension 中的完成

  • 关于用具体类型界说的实例 (如,implementDemo),情况较复杂:

    • 若具体类型也完成了该办法 (如,ImplementDemo.bar),则调用的是该完成

    • 否则调用 protocol extension 中的完成

    ⚡️⚡️⚡️ 防止重写 Protocol extension 中界说的 non-requirement 办法!

‍ ❓❗️

protocolDemoimplementDemo 有何不同❓

不都是 ImplementDemo 的实例吗❓

这就需求好好说说「协议作为类型」用了。

Existential Type


如上节所述,Protocol 能够作为类型用,用于界说变量:

var protocolDemo: ProtocolDemo

继续做题吧

分别用 Class、Struct 完成了 ProtocolDemo 协议:

Swift 最佳实践之 Protocol

正确答案:

protocolClassDemo = 40

classDemo = 8

protocolStructDemo = 40

structDemo = 48

这个呢❓‍ ,给 ProtocolDemo 加了只能用于 Class 的束缚 (AnyObject):

Swift 最佳实践之 Protocol

正确答案:

protocolClassDemo = 16

classDemo = 8

有没有感受到「 Protocol as Type 」的复杂性!

「 Protocol as Type 」有个专有名称:「 Existential Type

Protocols don’t actually implement any functionality themselves. Nonetheless, you can use protocols as a fully fledged types in your code. Using a protocol as a type is sometimes called anexistential type, which comes from the phrase “there exists a typeTsuch thatTconforms to the protocol”.

— Swift Docs Protocols

在 Swift Protocol 背面的故事(理论) 中,我们具体介绍了「 Existential Type 」背面的完成机制:

Swift 最佳实践之 Protocol

简略总结一下:

  • Existential Type 与对应的 Normal Type (如:protocolClassDemoclassDemo) 根本不是一回事

  • 关于 Existential Type,Swift 对其做了一层封装 (Existential Container),作为 Protocol 的「模型」

  • non-class constraint protocol 与 class constraint protocol (: AnyObject) 的 Existential Container 不相同:

    // for non-class constraint protocol
    //
    struct OpaqueExistentialContainer {
      void *fixedSizeBuffer[3];
      Metadata *type;
      WitnessTable *witnessTables[NUM_WITNESS_TABLES];
    }
    
    // for class constraint protocol
    //
    struct ClassExistentialContainer {
      HeapObject *value;
      WitnessTable *witnessTables[NUM_WITNESS_TABLES];
    }
    

正是因为,从 Normal Type 到 Existential Type 需求做一次封装转化 (装箱),在功能上有必定的损耗。

// normal type -> protocol type
let protocolClassDemo: ProtocolDemo = ClassDemo()

一起,经过 Existential Type 发起的办法调用都是动态派发 (在 extension 中界说的 non-requirement 办法除外),也有必定的功能损耗。

总之,Existential Type 有功能损耗,需求尽量防止运用❗️⚡️:

  • 泛型代替 Existential Type:

    protocol Animal {
      func eat()
    }
    struct Farm {
      // 从运用视点看 genericsFeed 与 existentialFeed 作用是相同的
      // 功能上 genericsFeed 更优
      // 但 existentialFeed 更简练,写起来更便利
      func genericsFeed<T>(_ animal: T) where T: Animal {
        animal.eat()
      }
      func existentialFeed(_ animal: Animal) {
        animal.eat()
      }
    }
    
  • 正是因为 Existential Type 运用太便利了,经常有意无意的就用上了

    为此,Swift 5.6 引入了any关键字 (swift-evolution/0335-existential-any) 用于符号「Existential Type」:

    //                     
    let protocolClassDemo: any ProtocolDemo = ClassDemo()
    

    首要目的是显式提示⚡️开发人员正在运用『 Existential Type 』

    现在any还不是强制的,但从 Swift 6.0 开端将强制运用any,否则编译报错。因而,尽早用上any,避免后期晋级成本过高。

    关于对 any 的介绍能够参看之前的文章:Swift Protocol 背面的故事(Swift 5.6/5.7)

有个大胆的主意 :能否既有泛型的功能又有『 Existential Types 』的简练?

答案是必定的,那便是:「 Opaque Type 」、「 Opaque Parameter 」:

简略讲,便是当 Protocol 作为类型时,能够在其前面加上 some 关键字,如:

struct Farm {
  func genericsFeed<T>(_ animal: T) where T: Animal {
    animal.eat()
  }
  // some Animal 能够理解为一个匿名的具体类型
  // 并且该类型完成了 Animal 协议
  //                       
  func someFeed(_ animal: some Animal) {
    animal.eat()
  }
}
// 如上,`someFeed` 在功能上与 `genericsFeed` 无任何不同,但更简练、可读性更好
// Opaque Parameter 能够理解为泛型的简化版别

再来猜个题吧

Swift 最佳实践之 Protocol

正确答案:

protocolClassDemo = 8

classDemo = 8

关于 Opaque Types 的具体介绍请参看 Swift Protocol 背面的故事(实践)

关于 Opaque Parameter 的具体介绍请参看 Swift Protocol 背面的故事(Swift 5.6/5.7)

some 关键字需求在 (iOS 13 & Swift 5.1/Swift 5.7) 以上才能够用 ,any 只需 Swift 5.6 即可

Associated Type


从一个小使命开端 :界说一个 Collection Protocol (BetterCollection),要求:

  • 支持 「 增、删、查 」

  • Collection 中一切元素类型有必要共同

看似很简略:

protocol BetterCollection {
  mutating func append(_ element: )
  mutating func remove(_ element: )
  subscript(i: Int) ->  { get }
}

Collection 中元素的类型怎么写❓❗️

Any

无法满足「 Collection 中一切元素类型有必要共同 」的要求!

此时,就需求 Associated Type 登场了:

protocol BetterCollection {
  associatedtype Element    // 
  mutating func append(_ element: Element)
  mutating func remove(_ element: Element)
  subscript(i: Int) -> Element { get }
}
  • associatedtype 关键字用于界说 Associated Type

  • Associated Type 或许理解为是一个类型占位符,其具体值则在完成该协议时确认

    class MyElement {}
    struct MyArray: BetterCollection {
      typealias Element = MyElement  // ,可省掉
      var elements: [MyElement] = []
      mutating func append(_ element: MyElement) {}
      mutating func remove(_ element: MyElement) {}
      subscript(i: Int) -> MyElement { MyElement() }
    }
    

    正常情况下,在完成协议时经过 typealias associatedType = *** 确认associatedtype 的具体值

    但得益于 Swift 强壮的类型推演才能,一般情况下不必显式写 typealias associatedType = ***

不错,使命圆满完成✌️

but,从语义上说 mutating func remove(_ element: Element) 办法要求 Element 完成 Equatable 协议,即能够判等(==)

问题不大,能够经过给 Associated Type 增加束缚来完成

Adding Constraints to an Associated Type

protocol BetterCollection {
  associatedtype Element: Equatable  // ,要求 Element 完成 Equatable 协议
  mutating func append(_ element: Element)
  mutating func remove(_ element: Element)
  subscript(i: Int) -> Element { get }
}

此时,如果具体类型不满足 associatedtype 的束缚,当然通不过编译了:

Swift 最佳实践之 Protocol

这样就完美了:

Swift 最佳实践之 Protocol

别快乐的太早,还有新使命

BetterCollection 增加批量 append 元素的接口:

mutating func append(contentsOf elements: )  // 参数 contentsOf 的类型?

上述 append 参数 contentsOf 的类型应该满足:

  • 完成 BetterCollection 协议

  • 元素类型须共同 (contentsOf中元素类型与 self 中元素类型要相同)

有两种完成方式:

  • Generic + Associated Type Constraints

    protocol BetterCollection {
      mutating func append<T: BetterCollection>(contentsOf elements: T) where T.Element == Element
    }
    struct MyArray: BetterCollection {  
      mutating func append<T>(contentsOf elements: T) where T : BetterCollection, MyElement == T.Element {}
    }
    
  • Self requirements

    protocol BetterCollection {
      mutating func append(contentsOf elements: Self)
    }
    struct MyArray: BetterCollection {
      //                                          
      mutating func append(contentsOf elements: MyArray) {}
    }
    

从上述不同版别 MyArray.append 的界说能够看出它们间还是有必定区别的:

  • Generic 版别只要求参数 contentsOf 的类型完成 BetterCollection 协议且两者的 associatedtype 相同即可

  • Self requirements 版别要求参数 contentsOf 的类型与 Self 相同

    关于 Self requirements,最著名的使用恐怕便是:

    public protocol Equatable {
      // 判等的 2 个类型有必要相同
      static func == (lhs: Self, rhs: Self) -> Bool
    }
    

应该说,Generic 版别更灵敏,使用场景更广,但写起来有点费事,有没有更简练的版别?

答案是必定的

Primary Associated Type

为了解决上一小节说到的 Generic 版别复杂的问题,Swift 5.7 引入了「 Primary Associated Type 」的概念 (swift-evolution/0346-light-weight-same-type-syntax)

经过「 Primary Associated Type 」能够改写上面的 Generic 版别,代码更简练:

//                          
protocol BetterCollection<Element> {
  associatedtype Element: Equatable
  //                                                                
  mutating func append(contentsOf elements: some BetterCollection<Element>)
}
struct MyArray: BetterCollection {
  //                                                                  
  mutating func append(contentsOf elements: some BetterCollection<MyElement>) {}
}

关于「 Primary Associated Types 」的具体介绍能够参看 Swift Protocol 背面的故事(Swift 5.6/5.7)

至此,BetterCollection 好像很「完美」了:

protocol BetterCollection<Element> {
  associatedtype Element: Equatable
  mutating func append(_ element: Element)
  mutating func remove(_ element: Element)
  subscript(i: Int) -> Element { get }
  mutating func append(contentsOf elements: some BetterCollection<Element>)
}

but,MyArray 不怎么「完美」,其中的元素只能是 MyElement 类型!

Generic + Associated Type

能够将 MyArray 界说为 Generic,再联合 Protocol Associated Type,完美 ✌️:

// 将泛型类型 T 与 BetterCollection 的 associatedtype 相绑定
// 
struct MyArray<T: Equatable>: BetterCollection {
  var elements: [T] = []
  mutating func append(_ element: T) {}
  mutating func remove(_ element: T) {
    let index = elements.firstIndex { $0 == element }
    guard let index else {
      return
    }
    elements.remove(at: index)
  }
  subscript(i: Int) -> T { elements[i] }
  mutating func append(contentsOf elements: some BetterCollection<T>) {}
}

将有 associatedtype 或 Self-requirements 的协议作为类型用时或许会遇到点费事⚡️

比方这样的编译过错:

「 Swift 5.7 」: Member ‘append’ cannot be used on value of type ‘any BetterCollection’; consider using a generic constraint instead

「 Swift ~5.6」: Protocol ‘BetterCollection’ can only be used as a generic constraint because it has Self or associated type requirements.

具体信息请参看:Swift Protocol 背面的故事(实践)、Swift Protocol 背面的故事(Swift 5.6/5.7)

Other


Automatically Synthesized Protocol Implementation

Swift 在满足必定条件时,会主动合成某些协议的完成,即不必手动写完成了 :

  • Equatable

    • Struct 的一切存储特点都(主动/手动)完成了 Equatable

    • 没有关联值的 Enum

    • Enum 关联值的类型(主动/手动)完成了 Equatable

  • Hashable

    • Struct 的一切存储特点都(主动/手动)完成了 Hashable

    • 没有关联值的 Enum

    • Enum 关联值的类型(主动/手动)完成了 Hashable

  • Codable

    • Class/Struct 的一切存储特点都(主动/手动)完成了 Codable

    • 没有关联值的 Enum

    • Enum 关联值的类型(主动/手动)完成了 Codable

EquatableEnum 只对 Struct、Enum 会主动合成

Codable 对 Class、Struct、Enum 都会主动合成

Class-Only-Protocols

有些 Protocol 要求其完成有必要是 Class

如,需求弱引证的 delegate:

weak var delegete: Delegate?

在界说协议时能够加上 AnyObject

protocol Delegate: AnyObject {}

Constraints

在 Protocol 中有各种不同的 Constraints,如:

  • associatedtype constraint,在界说 associatedtype 时指定束缚:

    associatedtype Element: Equatable
    
  • constraints between associatedtypes,当有多个 associatedtypes 时,能够在它们间指定束缚,如 Swift 标准库中 Sequence 的界说:

    public protocol Sequence<Element> {
      /// A type representing the sequence's elements.
      associatedtype Element where Self.Element == Self.Iterator.Element
      /// A type that provides the sequence's iteration interface and
      /// encapsulates its iteration state.
      associatedtype Iterator : IteratorProtocol
    }
    
  • method constraint,在声明办法时指定束缚:

    mutating func append<T: BetterCollection>(contentsOf elements: T) where T.Element == Element
    
  • Self requirements,要求相同的完成类型:

    static func == (lhs: Self, rhs: Self) -> Bool
    
  • extension constraint,有条件的对 Protocol extension:

    extension BetterCollection where Element: Codable {
      // ... 
    }
    
  • extension constraint on Self,在扩展协议时还能够对 Self 增加束缚

    extension BetterCollection where Self: UIView {}
    
  • primary associatedtype constraint,经过 primary associatedtype 能够简化 extension constraint 的写法:

    extension BetterCollection<Codable> {
      // ...
    }
    // Equivalent to:
    extension BetterCollection where Element: Codable {
      // ... 
    }
    
  • inherit constraint,有条件的继承:

    protocol CodableBetterCollection: BetterCollection where Element: Codable {}
    struct MyArray<T: Equatable & Codable>: CodableBetterCollection {
      // ...
    }
    
  • conditionally conforming to a protocol,有条件的完成某个协议

    如下代码编译报错,原因在于泛型类型 T 并没有完成 Codable 协议

    Swift 最佳实践之 Protocol

    MyArray 的扩展加个束缚即可:

    extension MyArray: Codable where T: Codable {}
    

小结

Swift Protocol 才能非常强壮,经过 extension 供给协议的默许完成,有助于开发功率的提高。

将 Protocol 用于类型时,因为需求装箱 (Existential Container) 以及办法需求动态派发,有必定的功能损耗。经过 Generic 或 Opaque Types(some) 能够防止功能问题。

Associated Type 提高了 Protocol 的灵敏性,但又不失类型安全。

Associated Type + Generic 更是如虎添翼

参考资料

Swift Protocol 背面的故事(实践)

Swift Protocol 背面的故事(理论)

Swift Protocol 背面的故事(Swift 5.6/5.7)

Documentation Protocols

Documentation Generics Associated-Types

swift-evolution/0346-light-weight-same-type-syntax

swift-evolution/0358-primary-associated-types-in-stdlib

Getting started with associated types in Swift Protocols