Swift — 协议(Protocol)
Swift — 协议(Protocol)
[TOC]
协议定义了一个蓝图,规定了用来实现某一特定任务或者功能的方法、属性,以及其他需要的东西。类、结构体和枚举都可以遵循协议,并为协议定义的这些要求提供具体实现。某个类型能够满足某个协议的要求,就可以说该类型遵循这个协议。
除了遵循协议的类型必须实现的要求外,还可以对协议进行扩展,通过扩展来实现一部分要求或者实现一些附加功能,这些遵循协议的类型就能够使用这些功能。
1. 协议的基本用法
1.1 协议语法
协议的定义方式与类、结构体和枚举的定义非常相似
- 基本语法
protocol SomeProtocol {
// 这里是协议的定义部分
}
- 如果让自定义的类型遵循某个协议,在定义类型时,需要在类型名称后面加上协议名称,中间以冒号(
:
)隔开,如果需要遵循多个协议时,个协议之间用逗号(,
)分割:
struct SomeStructure: FirstProtocol, AnotherProtocol {
// 这里是结构体的定义部分
}
- 如果自定义类型拥有一个父类,应该将父类名放在遵循协议名之前,以逗号分隔:
class SomeClass: SomeSuperClass, FirstProtocol, AnotherProtocol {
// 这里是类的定义部分
}
1.2 属性要求
我们可以在协议中添加属性,但需要注意以下几点:
- 属性可以是实例属性和类型属性
- 属性需要使用
var
修饰,不能属于let
- 类型属性只能使用
static
修饰,不能使用class
- 我们需要声明属性必须是可读的或者可读可写的
protocol SomeProtocol {
var propertyOne: Int { get set }
var propertyTwo: Int { get }
static var propertyThree: Int { get set }
}
1.3 方法要求
我们可以在协议中添加方法,但需要注意以下几点:
- 可以是实例方法或类方法
- 像普通方法一样放在协议定义中,但不需要大括号和方法体
- 协议中不支持为协议中的方法提供默认参数
- 协议中的类方法也只能使用
static
关键字作为前缀,不能使用class
- 可以使用
mutating
提供异变方法,以使用该方法时修改实体的属性等。 - 可以定义构造方法,但是使用的时候需要使用
required
关键字
protocol SomeProtocol {
func someMethod1()
func someMethod2() ->Int
}
构造方法
protocol SomeProtocol {
init(param: Int)
}
class SomeClass: SomeProtocol {
required init(param: Int) { }
}
异变方法
protocol Togglable {
mutating func toggle()
}
enum OnOffSwitch: Togglable {
case off, on
mutating func toggle() {
switch self {
case .off:
self = .on
case .on:
self = .off
}
}
}
1.4 协议作为类型
尽管协议本身并未实现任何功能,但是协议可以被当做一个功能完备的类型来使用。协议作为类型使用,有时被称作「存在类型」,这个名词来着存在着一个类型T,该类型遵循协议T。
协议可以像其他普通类型一样使用,使用场景如下:
- 作为函数、方法或构造器中的参数类型或返回值类型
- 作为常量、变量或属性的类型
- 作为数组、字典或其他容器中的元素类型
protocol SomeProtocol { }
class SomeClass {
required init(param: SomeProtocol) {}
}
1.5 其他
- 协议还可以被继承
- 可以在扩展里面遵循协议
- 在扩展里面声明采纳协议
- 使用合成来采纳协议
- 可以定义由类专属协议,只需要继承自
AnyObject
- 协议可以合成
- 协议也可以扩展
更多的关于协议的用法请参考:
以及它的译文:
2. 协议中方法的调用
举个例子,在数学中我们会求某个图形的面积,但是不同形状求面积的公式是不一样的,如果用代码来实现可以怎么来实现呢?
首先我们可以通过继承父类的方法来实现,但是在这里我们就可以使用协议来实现:
protocol Shape {
var area: Double {get}
}
class Circle: Shape{
var radius: Double
init(_ radius: Double) {
self.radius = radius
}
var area: Double{
get{
return radius * radius * 3.14
}
}
}
class Rectangle: Shape{
var width, height: Double
init(_ width: Double, _ height: Double) {
self.width = width
self.height = height
}
var area: Double{
get{
return width * height
}
}
}
var circle: Shape = Circle.init(10.0)
var rectangle: Shape = Rectangle.init(10.0, 20.0)
print(circle.area)
print(rectangle.area)
<!--打印结果-->
314.0
200.0
此时的打印结果是符合我们的预期的。
我们知道协议可以扩展,此时我们把协议的代码修改成如下:
protocol Shape {
// var area: Double {get}
}
extension Shape{
var area: Double {
get{return 0.0}
}
}
<!--打印结果-->
0.0
0.0
此时并没有如我们预期的打印,如果我们声明变量的时候写成如下呢:
var circle: Circle = Circle.init(10.0)
var rectangle: Rectangle = Rectangle.init(10.0, 20.0)
<!--打印结果-->
314.0
200.0
此时的打印就符合我们的预期了。
其实我们也能够清楚的了解到为什么会打印0.0
,在Swift 方法调度这篇文章中我们介绍了extension
中声明的方法是静态调用的,也就是说在编译后当前代码的地址已经确定,我们无法修改,当声明为Shap
类型后,默认调用的就是Shape extension
中的属性的get
方法。下面我们在通过sil
代码来验证一下,关于生成sil
代码的方法,请参考我以前的文章。
为了方便查看,我们精简并修改代码为如下:
protocol Shape {
// var area: Double {get}
}
extension Shape{
var area: Double {
get{return 0.0}
}
}
class Circle: Shape{
var radius: Double
init(_ radius: Double) {
self.radius = radius
}
var area: Double{
get{
return radius * radius * 3.14
}
}
}
var circle: Shape = Circle.init(10.0)
var a = circle.area
生成的sil代码:
image
通过sil代码我们可以清晰的看到,这里直接调用的Shape.area.getter
方法。
下面我们换一些简单的代码再次看一下:
protocol PersonProtocol {
func eat()
}
extension PersonProtocol{
func eat(){ print("PersonProtocol eat") }
}
class Person: PersonProtocol{
func eat(){ print("Person eat") }
}
let p: PersonProtocol = Person()
p.eat()
let p1: Person = Person()
p1.eat()
<!--打印结果-->
Person eat
Person eat
可以看到上面这段代码的打印结果都是Person eat
,那么为什么会打印相同的结果呢?首先通过代码我们可以知道,在PersonProtocol
中声明了eat
方法。对于声明的协议方法,如果类中也实现了,就不会调用协议扩展中的方法。上面的属性的例子中并没有在协议中声明属性,只是在协议扩展中添加了一个属性。下面我们看看上面这段代码的sil代码:
首先我们可以看到,对于两个eat
方法的确实存在不同,首先声明为协议类型的变量调用eat
方法是通过witness_method
调用,另一个则是通过class_method
调用。
-
witness_method
是通过PWT
(协议目击表)获取对应的函数地址 -
class_method
是通过类的函数表来查找函数进行调用
在刚刚sil代码中我们可以找到sil_witness_table
,在里面有PersonProtocol.eat
方法,找到PersonProtocol.eat
方法可以发现里面是调用class_method
寻找的类中VTable
的Person.eat
方法。
如果我们不在协议中声明eat
方法:
protocol PersonProtocol {
// func eat()
}
extension PersonProtocol{
func eat(){ print("PersonProtocol eat") }
}
class Person: PersonProtocol{
func eat(){ print("Person eat") }
}
let p: PersonProtocol = Person()
p.eat()
let p1: Person = Person()
p1.eat()
<!--打印结果-->
PersonProtocol eat
Person eat
查看sil代码:
image此时我们可以看到,对于不在协议中声明方法的时候,依然是直接调用(静态调用)。
所以对于协议中方法的调度:
- 对于不在协议中声明的方法
- 在协议扩展中有实现就是直接调用
- 在遵循协议的实体中按照其调度方式决定
- 两处都实现了,声明的实例是协议类型则直接调用协议扩展中的方法,反之调用遵循协议实体中的方法
- 对于声明在协议中的方法
- 如果遵循该协议的实体实现了该方法,则通过
PWT
协议目击表查找到实现的方法进行调用(与声明变量的类型无关) - 如果遵循协议的实体没实现,协议扩展实现了,则会调用协议扩展中的方法
- 如果遵循该协议的实体实现了该方法,则通过
3. 协议原理探索
在上面探索协议中的方法调用的时候,我们提到过PWT
也就是Protocol witness table
,协议目击表,那么它存储在什么地方呢?我们在Swift 方法调度这篇文章中讲过,V-Table
是存储在metadata
中的,那么我们就探索一下PWT
的存储位置。
3.1 内存占用
首先我们先来看看如下代码的的打印结果:
protocol Shape {
var area: Double { get }
}
class Circle: Shape {
var radius: Double
init(_ radius: Double) {
self.radius = radius
}
var area: Double{
get{ return radius * radius * 3.14 }
}
}
var circle: Shape = Circle(10.0)
print(MemoryLayout.size(ofValue: circle))
print(MemoryLayout.stride(ofValue: circle))
var circle1: Circle = Circle(10.0)
print(MemoryLayout.size(ofValue: circle1))
print(MemoryLayout.stride(ofValue: circle1))
<!--打印结果-->
40
40
8
8
3.2 lldb探索内存结构
看到这个打印结果我能第一时间想到的就是生命为协议类型会存储更多的信息。生命为类的时候,存储的是类的实例对象的指针8字节。下面我们通过lldb
调试来探索一下这个40字节都存储了什么信息。
3.3 sil 探索内存结构
通过lldb
我们可以看到其内部应该存储着一些信息,那么具体存了什么呢?我们在看看sil
代码:
在sil代码中我们可以看到,在初始化circle
这个变量的时候使用到了init_existential_addr
,查看SIL文档:
译文:用一个准备好包含类型为$T的存在容器部分初始化%0引用的内存。该指令的结果是一个地址,该地址引用了所包含值的存储空间,该存储空间仍然没有初始化。包含的值必须存储为-d或copy_addr-ed,以便完全初始化存在值。如果存在容器的值未初始化时需要销毁,则必须使用deinit_existential_addr来完成此操作。可以像往常一样使用destroy_addr销毁完全初始化的存在性容器。销毁一个部分初始化存在容器的addr是未定义的行为。
文档中的意思是,使用了包含$T
的existential container
来初始化%0
引用的内存。在这里就是使用包含Circle
的existential container
来初始化circle
引用的内存,简单来说就是将circle
包装到了一个existential container
初始化的内存。
existential container
是编译器生成的一种特殊的数据类型,也用于管理遵守了相同协议的协议类型。因为这些塑化剂类型的内存空间尺寸不同,使用existential container
进行管理可以实现存储一致性。
3.4 IR代码探索内存结构
那么这个existential container
都包装了什么呢?目前通过sil代码是看不出来什么了,那么我们就看看IR
代码:
; 一个结构体,占用24字节内存的数组,wift.type指针, i8*指针
%T4main5ShapeP = type { [24 x i8], %swift.type*, i8** }
define i32 @main(i32 %0, i8** %1) #0 {
entry:
%2 = bitcast i8** %1 to i8*
; main.Circle 的 metadata
%3 = call swiftcc %swift.metadata_response @"type metadata accessor for main.Circle"(i64 0) #7
%4 = extractvalue %swift.metadata_response %3, 0
;init放
%5 = call swiftcc %T4main6CircleC* @"main.Circle.__allocating_init(Swift.Double) -> main.Circle"(double 1.000000e+01, %swift.type* swiftself %4)
; 存%4 也就是metadata,存到T4main5ShapeP结构体中,这里存的位置是第二个位置
store %swift.type* %4, %swift.type** getelementptr inbounds (%T4main5ShapeP, %T4main5ShapeP* @"main.circle : main.Shape", i32 0, i32 1), align 8
; 存pwt 也就是协议目击表,存到第三个位置
store i8** getelementptr inbounds ([2 x i8*], [2 x i8*]* @"protocol witness table for main.Circle : main.Shape in main", i32 0, i32 0), i8*** getelementptr inbounds (%T4main5ShapeP, %T4main5ShapeP* @"main.circle : main.Shape", i32 0, i32 2), align 8
; 存放%5到二级指针,%5是init出来的对象,所以这里也就是个HeapObject结构,也就是T4main6CircleC结构体的第一个8字节内存空间处
store %T4main6CircleC* %5, %T4main6CircleC** bitcast (%T4main5ShapeP* @"main.circle : main.Shape" to %T4main6CircleC**), align 8
}
从IR代码中我们可以知道,这里面的存储是一个结构体,结构体中主要分为三个方面:
- 一个连续的24字节空间
- 一个存放metadata的指针
- 存放
pwt
指针
3.5 仿写
下面我们就来仿写一下这个结构:
struct HeapObject {
var type: UnsafeRawPointer
var refCount1: UInt32
var refCount2: UInt32
}
struct protocolData {
//24 * i8 :因为是8字节读取,所以写成3个指针
var value1: UnsafeRawPointer
var value2: UnsafeRawPointer
var value3: UnsafeRawPointer
//type 存放metadata,目的是为了找到Value Witness Table 值目录表
var type: UnsafeRawPointer
// i8* 存放pwt指针
var pwt: UnsafeRawPointer
}
3.5.1 类遵循协议重绑定
进行内存的重新绑定:
protocol Shape {
var area: Double { get }
}
class Circle: Shape {
var radius: Double
init(_ radius: Double) {
self.radius = radius
}
var area: Double{
get{ return radius * radius * 3.14 }
}
}
var circle: Shape = Circle(10.0)
// 将circle强转为protocolData结构体
withUnsafePointer(to: &circle) { ptr in
ptr.withMemoryRebound(to: protocolData.self, capacity: 1) { pointer in
print(pointer.pointee)
}
}
<!--打印结果-->
protocolData(value1: 0x00000001006082b0, value2: 0x0000000000000000, value3: 0x0000000000000000, type: 0x0000000100008180, pwt: 0x0000000100004028)
image
通过lldb
查看:
- 我们也可以看到对应
HeapObject
结构- 该结构存储的是
Circle
的实例变量 - 并且在这里面的
metadata
与protocolData
里面的存储的metadata
的地址是一致的;
- 该结构存储的是
- 通过
cat address
命令查看pwt
对应的指针,可以看到这段内存对应的就是SwiftProtocol.Circle
的protocol witness table
。
至此我们就清楚的找到你了PWT
的存储位置,PWT
存在协议类型实例的内存结构中。
3.5.2 结构体遵循协议重绑定
在上面这个例子中我们使用的是类,我们知道类是引用类型,如果换成结构体呢?
protocol Shape {
var area: Double {get}
}
struct Rectangle: Shape{
var width, height: Double
init(_ width: Double, _ height: Double) {
self.width = width
self.height = height
}
var area: Double{
get{
return width * height
}
}
}
var rectangle: Shape = Rectangle(10.0, 20.0)
struct HeapObject {
var type: UnsafeRawPointer
var refCount1: UInt32
var refCount2: UInt32
}
struct protocolData {
//24 * i8 :因为是8字节读取,所以写成3个指针
var value1: UnsafeRawPointer
var value2: UnsafeRawPointer
var value3: UnsafeRawPointer
//type 存放metadata,目的是为了找到Value Witness Table 值目录表
var type: UnsafeRawPointer
// i8* 存放pwt指针
var pwt: UnsafeRawPointer
}
// 将circle强转为protocolData结构体
withUnsafePointer(to: &rectangle) { ptr in
ptr.withMemoryRebound(to: protocolData.self, capacity: 1) { pointer in
print(pointer.pointee)
}
}
<!--打印结果-->
protocolData(value1: 0x4024000000000000, value2: 0x4034000000000000, value3: 0x0000000000000000, type: 0x0000000100004098, pwt: 0x0000000100004028)
image
此时我们可以看到,此时并没有存储一个HeapObject结构的指针,而是直接存储Double
类型的值,metadata
和pwt
没有变。
在看下IR
代码:
define i32 @main(i32 %0, i8** %1) #0 {
entry:
%2 = bitcast i8** %1 to i8*
%3 = call swiftcc { double, double } @"main.Rectangle.init(Swift.Double, Swift.Double) -> main.Rectangle"(double 1.000000e+01, double 2.000000e+01)
; 10
%4 = extractvalue { double, double } %3, 0
; 20
%5 = extractvalue { double, double } %3, 1
;metadata
store %swift.type* bitcast (i64* getelementptr inbounds (<{ i8**, i64, <{ i32, i32, i32, i32, i32, i32, i32 }>*, i32, i32 }>, <{ i8**, i64, <{ i32, i32, i32, i32, i32, i32, i32 }>*, i32, i32 }>* @"full type metadata for main.Rectangle", i32 0, i32 1) to %swift.type*), %swift.type** getelementptr inbounds (%T4main5ShapeP, %T4main5ShapeP* @"main.rectangle : main.Shape", i32 0, i32 1), align 8
;pwt
store i8** getelementptr inbounds ([2 x i8*], [2 x i8*]* @"protocol witness table for main.Rectangle : main.Shape in main", i32 0, i32 0), i8*** getelementptr inbounds (%T4main5ShapeP, %T4main5ShapeP* @"main.rectangle : main.Shape", i32 0, i32 2), align 8
;存%4 也就是10
store double %4, double* getelementptr inbounds (%T4main9RectangleV, %T4main9RectangleV* bitcast (%T4main5ShapeP* @"main.rectangle : main.Shape" to %T4main9RectangleV*), i32 0, i32 0, i32 0), align 8
; 存%5 也就是20
store double %5, double* getelementptr inbounds (%T4main9RectangleV, %T4main9RectangleV* bitcast (%T4main5ShapeP* @"main.rectangle : main.Shape" to %T4main9RectangleV*), i32 0, i32 1, i32 0), align 8
}
通过IR
代码我们可以看到:
- 对于
metadata
和pwt
的存储依旧 - 然后存储了两个
Double
值,并没有存储HeapObject
类型的指针
那么如果有3个属性呢?
struct Rectangle: Shape{
var width, width1, height: Double
init(_ width: Double, _ width1: Double, _ height: Double) {
self.width = width
self.width1 = width1
self.height = height
}
var area: Double{
get{
return width * height
}
}
}
<!--内存绑定后的打印结果-->
protocolData(value1: 0x4024000000000000, value2: 0x4034000000000000, value3: 0x403e000000000000, type: 0x0000000100004098, pwt: 0x0000000100004028)
这个三个Value
的值分别是10,20,30
那如果是4个呢?
struct Rectangle: Shape{
var width, width1, height, height1: Double
init(_ width: Double, _ width1: Double, _ height: Double, _ height1: Double) {
self.width = width
self.width1 = width1
self.height = height
self.height1 = height1
}
var area: Double{
get{
return width * height
}
}
}
var rectangle: Shape = Rectangle(10.0, 20.0, 30.0, 40.0)
<!--内存绑定后的打印结果-->
protocolData(value1: 0x0000000100715870, value2: 0x0000000000000000, value3: 0x0000000000000000, type: 0x00000001000040c0, pwt: 0x0000000100004050)
此时并没有直接看到Double
值了,查看value1
的内存:
此时我们可以看到,这个内存中存储了10,20,30,40这四个值。
所以如果我们需要存储的数据超过了24 x i8*,也就是24字节时,就会开辟内存空间进行存储。这里只存储指向新开辟内存空间的指针。
这里的顺序是,如果不够存储就直接开辟内存空间,存储值,记录指针。而不是先存储不够了在开辟内存空间。
我们都知道,结构体是值类型,如果超过这24字节的存储空间就会开辟内存用来存储结构体中的值,如果此时发生拷贝会是神马结构呢?下面我们就来验证一下:
结构体拷贝:
protocol Shape {
var area: Double {get}
}
struct Rectangle: Shape{
var width, width1, height, height1: Double
init(_ width: Double, _ width1: Double, _ height: Double, _ height1: Double) {
self.width = width
self.width1 = width1
self.height = height
self.height1 = height1
}
var area: Double{
get{
return width * height
}
}
}
var rectangle: Shape = Rectangle(10.0, 20.0, 30.0, 40.0)
var rectangle1 = rectangle
struct HeapObject {
var type: UnsafeRawPointer
var refCount1: UInt32
var refCount2: UInt32
}
struct protocolData {
//24 * i8 :因为是8字节读取,所以写成3个指针
var value1: UnsafeRawPointer
var value2: UnsafeRawPointer
var value3: UnsafeRawPointer
//type 存放metadata,目的是为了找到Value Witness Table 值目录表
var type: UnsafeRawPointer
// i8* 存放pwt指针
var pwt: UnsafeRawPointer
}
// 内存重绑定
withUnsafePointer(to: &rectangle) { ptr in
ptr.withMemoryRebound(to: protocolData.self, capacity: 1) { pointer in
print(pointer.pointee)
}
}
withUnsafePointer(to: &rectangle1) { ptr in
ptr.withMemoryRebound(to: protocolData.self, capacity: 1) { pointer in
print(pointer.pointee)
}
}
<!--打印结果-->
protocolData(value1: 0x000000010683bac0, value2: 0x0000000000000000, value3: 0x0000000000000000, type: 0x00000001000040c0, pwt: 0x0000000100004050)
protocolData(value1: 0x000000010683bac0, value2: 0x0000000000000000, value3: 0x0000000000000000, type: 0x00000001000040c0, pwt: 0x0000000100004050)
此时我们看到打印结果是一样的。
那么修改呢?
添加如下代码:
protocol Shape {
// 为了方便修改,在这声明一下
var width: Double {get set}
var area: Double {get}
}
rectangle1.width = 50
image
通过lldb重新打印,我们可以看到在修改值后,内存地址已经修改了,此时就是写时复制。当复制时并没有值的修改,所以两个变量指向同一个堆区内存。当修改变量的时候,会原本的堆区内存的值拷贝到一个新的内存区域,并进行值的修改。
如果我们将struct
修改成class
,这里并不会触发写时复制,因为在Swift中类是引用类型,修改类的值就是修改其引用地址中的值。这里就不验证了,感兴趣的可以自己去试试。
如果我们将Double
换成String
原理也是一致的,这里也就不一一验证了。
3.5.3 小结
至此我们也就清楚了,为什么协议中通过witness_method
调用,最终能找到V-Table
中的方法,原因就是存储了metadata
和pwt
。这也是我们都声明为协议类型,最终能打印出不同形状的面积根本原因。
4. 总结
至此我们对Swift中协议的分析就结束了,现总结如下:
- Swift中类、结构体、枚举都可以遵守协议
- 遵守多个协议使用逗号(
,
)分隔 - 有父类的,父类写在前面,协议在后面用逗号(
,
)分隔 - 协议中可以添加属性
- 属性可以是实例属性和类型属性
- 属性需要使用
var
修饰,不能属于let
- 类型属性只能使用
static
修饰,不能使用class
- 我们需要声明属性必须是可读的或者可读可写的
- 协议中可以添加方法
- 可以是实例方法或类方法
- 像普通方法一样放在协议定义中,但不需要大括号和方法体
- 协议中不支持为协议中的方法提供默认参数
- 协议中的类方法也只能使用
static
关键字作为前缀,不能使用class
- 可以使用
mutating
提供异变方法,以使用该方法时修改实体的属性等。 - 可以定义构造方法,但是使用的时候需要使用
required
关键字
- 如果定义由类专属协议,则需要继承自
AnyObject
- 协议可以作为类型
- 作为函数、方法或构造器中的参数类型或返回值类型
- 作为常量、变量或属性的类型
- 作为数组、字典或其他容器中的元素类型
- 协议的底层存储结构是:24字节的
ValueBuffer
+ metadata(8字节,也就是vwt) + pwt(8字节)- 前24字节,官方说法是
ValueBuffer
,主要用于存储遵循了协议的实体的属性值 - 如果超过
ValueBuffer
最大容量就会开辟内存进行存储,此24字节拿出8字节存储指向该内存区域的指针 - 目前对于类,发现其存储的都是指针
- 存储
metadata
是为了查找遵守协议的实体中实现协议的方法 -
pwt
就是protocol witness table
协议目击表,存储协议中的方法
- 前24字节,官方说法是