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?
-
?
正确答案 :
bar: in Extension
foo: in ImplementDemo
bar: in ImplementDemo
foo: in ImplementDemo
这个呢?
Compiler Error❓ Crash❓
正确答案:
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 办法!
-
❓❗️
protocolDemo
与 implementDemo
有何不同❓
不都是 ImplementDemo
的实例吗❓
这就需求好好说说「协议作为类型」用了。
Existential Type
如上节所述,Protocol 能够作为类型用,用于界说变量:
var protocolDemo: ProtocolDemo
继续做题吧
分别用 Class、Struct 完成了 ProtocolDemo
协议:
正确答案:
protocolClassDemo = 40
classDemo = 8
protocolStructDemo = 40
structDemo = 48
这个呢❓ ,给 ProtocolDemo
加了只能用于 Class 的束缚 (AnyObject
):
正确答案:
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 」背面的完成机制:
简略总结一下:
-
Existential Type 与对应的 Normal Type (如:
protocolClassDemo
与classDemo
) 根本不是一回事 -
关于 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 能够理解为泛型的简化版别
再来猜个题吧
正确答案:
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 的束缚,当然通不过编译了:
这样就完美了:
别快乐的太早,还有新使命
给 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
-
Equatable
、Enum
只对 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
协议给
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