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代碼:
通過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代碼:
此時我們可以看到,對于不在協議中聲明方法的時候,依然是直接調用(靜態調用)。
所以對于協議中方法的調度:
- 對于不在協議中聲明的方法
- 在協議擴展中有實現就是直接調用
- 在遵循協議的實體中按照其調度方式決定
- 兩處都實現了,聲明的實例是協議類型則直接調用協議擴展中的方法,反之調用遵循協議實體中的方法
- 對于聲明在協議中的方法
- 如果遵循該協議的實體實現了該方法,則通過
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)
通過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)
此時我們可以看到,此時并沒有存儲一個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
通過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字節,官方說法是