函數(shù)派發(fā)方式
能夠在編譯期確定執(zhí)行方法的方式叫做靜態(tài)分派 Static dispatch,無(wú)法在編譯期確定,只能在運(yùn)行時(shí)去確定執(zhí)行方法的分派方式叫做動(dòng)態(tài)分派 Dynamic dispatch。
靜態(tài)分派(Static dispatch)
Static dispatch 更快,CPU 直接拿到函數(shù)地址并進(jìn)行調(diào)用,而且靜態(tài)分派可以進(jìn)行內(nèi)聯(lián)等進(jìn)一步的優(yōu)化,使得執(zhí)行更快速,性能更高。
使用 Static dispatch 代替 Dynamic dispatch 提升性能
我們知道Static dispatch快于Dynamic dispatch,如何在開(kāi)發(fā)中去盡可能使用Static dispatch。
-
inheritance constraints繼承約束我們可以使用
final關(guān)鍵字去修飾Class,以此生成的Final class,使用Static dispatch。 -
access control訪問(wèn)控制private關(guān)鍵字修飾,使得方法或?qū)傩灾粚?duì)當(dāng)前類可見(jiàn)。編譯器會(huì)對(duì)方法進(jìn)行Static dispatch。
編譯器可以通過(guò) whole module optimization 檢查繼承關(guān)系,對(duì)某些沒(méi)有標(biāo)記 final 的類通過(guò)計(jì)算,如果能在編譯期確定執(zhí)行的方法,則使用 Static dispatch。 Struct 默認(rèn)使用 Static dispatch。
Swift 提供了更靈活的Struct,用以在內(nèi)存、引用計(jì)數(shù)、方法分派等角度去進(jìn)行性能的優(yōu)化,在正確的時(shí)機(jī)選擇正確的數(shù)據(jù)結(jié)構(gòu),可以使我們的代碼性能更快更安全。
如果你正在面試,或者正準(zhǔn)備跳槽,不妨看看我精心總結(jié)的面試資料:https://gitee.com/Mcci7/i-oser 來(lái)獲取一份詳細(xì)的大廠面試資料 為你的跳槽加薪多一份保障
你可能會(huì)問(wèn) Struct 如何實(shí)現(xiàn)多態(tài)呢?
答案是 protocol oriented programming。
以上分析了影響性能的幾個(gè)標(biāo)準(zhǔn),那么不同的算法機(jī)制Class,Protocol Types 和 Generic code,它們?cè)谶@三方面的表現(xiàn)如何,Protocol Type 和 Generic code 分別是怎么實(shí)現(xiàn)的呢?我們帶著這個(gè)問(wèn)題看下去。
Protocol Type
這里我們會(huì)討論P(yáng)rotocol Type如何存儲(chǔ)和拷貝變量,以及方法分派是如何實(shí)現(xiàn)的。不通過(guò)繼承或者引用語(yǔ)義的多態(tài):
protocol Drawable {
func draw()
}
struct Point :Drawable {
var x, y:Double
func draw() { … }
}
struct Line :Drawable {
var x1, y1, x2, y2:Double
func draw() {
…
}
}
var drawables:[Drawable] //遵守了Drawable協(xié)議的類型集合,可能是point或者line
for d in drawables {
d.draw()
}
復(fù)制代碼
因?yàn)?Point 和 Line 的尺寸不同,數(shù)組存儲(chǔ)數(shù)據(jù)實(shí)現(xiàn)一致性存儲(chǔ),使用了Existential Container。查找正確的執(zhí)行方法則使用了 Protoloc Witness Table 。
以上通過(guò) Protocol Type 實(shí)現(xiàn)多態(tài),幾個(gè)類之間沒(méi)有繼承關(guān)系,故不能按照慣例借助 V-Table 實(shí)現(xiàn)動(dòng)態(tài)分派。但是對(duì)于 swift 來(lái)說(shuō),class 類和 struct 結(jié)構(gòu)體的實(shí)現(xiàn)是不同的,而屬于結(jié)構(gòu)體的協(xié)議Protocol,可以擁有屬性和實(shí)現(xiàn)方法,管理Protocol Type方法分派的表就叫做Protocol Witness Table。
Protocol Witness Table
和 V-table 一樣,Protocol Witness Table(簡(jiǎn)稱 PWT )內(nèi)存儲(chǔ)的是方法數(shù)組,里面包含了方法實(shí)現(xiàn)的指針地址,一般我們調(diào)用方法時(shí),是通過(guò)獲取對(duì)象的內(nèi)存地址和方法的位移offset去查找的.
Protocol Witness Table 是用于管理 Protocol Type 的方法調(diào)用的,在我們接觸 swift 性能優(yōu)化時(shí),聽(tīng)到另一個(gè)概念叫做 Value Witness Table (簡(jiǎn)稱 VWT),這個(gè)又是做什么的呢?
Value Witness Table
[圖片上傳失敗...(image-1b0d39-1638867090452)]
用于管理任意值的初始化、拷貝、銷毀。即對(duì) Protocol Type 的生命周期進(jìn)行專項(xiàng)管理
對(duì)于每一個(gè)類型(Int或者自定義),都在metadata中存儲(chǔ)了一個(gè)VWT(用來(lái)管理當(dāng)前類型的值)
Value Witness Table 和 Protocol Witness Table 通過(guò)分工,去管理 Protocol Type 實(shí)例的內(nèi)存管理(初始化,拷貝,銷毀)和方法調(diào)用。
動(dòng)態(tài)分派
但是對(duì)于多態(tài)的情況,我們不能在編譯期確定最終的類型,這里就用到了 Dynamic dispatch 動(dòng)態(tài)分派。動(dòng)態(tài)分派的實(shí)現(xiàn)是,每種類型都會(huì)創(chuàng)建一張表,表內(nèi)是一個(gè)包含了方法指針的數(shù)組。動(dòng)態(tài)分派更靈活,但是因?yàn)橛胁楸砗吞D(zhuǎn)的操作,并且因?yàn)楹芏嗵攸c(diǎn)對(duì)于編譯器來(lái)說(shuō)并不明確,所以相當(dāng)于 block 了編譯器的一些后期優(yōu)化。所以速度慢于 Static dispatch。
函數(shù)表派發(fā)(Table dispatch)
編譯型語(yǔ)言中最常見(jiàn)的派發(fā)方式,既保證了動(dòng)態(tài)性也兼顧了執(zhí)行效率。
函數(shù)所在的類會(huì)維護(hù)一個(gè)“函數(shù)表”(虛函數(shù)表),存取了每個(gè)函數(shù)實(shí)現(xiàn)的指針。
每個(gè)類的 vtable 在編譯時(shí)就會(huì)被構(gòu)建,所以與靜態(tài)派發(fā)相比多出了兩個(gè)讀取的工作:
- 讀取該類的 vtable
- 讀取函數(shù)的指針
優(yōu)點(diǎn):
- 查表是一種簡(jiǎn)單,易實(shí)現(xiàn),而且性能可預(yù)知的方式。
- 理論上說(shuō),函數(shù)表派發(fā)也是一種高效的方式。
缺點(diǎn):
- 與靜態(tài)派發(fā)相比,從字節(jié)碼角度來(lái)看,多了兩次讀和一次跳轉(zhuǎn)。
- 與靜態(tài)派發(fā)相比,編譯器對(duì)某些含有副作用的函數(shù)無(wú)法優(yōu)化。
- Swift 類擴(kuò)展里面的函數(shù)無(wú)法動(dòng)態(tài)加入該類的函數(shù)表中,只能使用靜態(tài)派發(fā)的方式。
舉個(gè)例子(只是一個(gè)示例):
class A {
func method1() {}
}
class B: A {
func method2() {}
}
class C: B {
override func method2() {}
func method3() {}
}
復(fù)制代碼
復(fù)制代碼
| offset | 0xA00 | A | 0xB00 | B | 0xC00 | C |
|---|---|---|---|---|---|---|
| 0 | 0x121 | A.method1 | 0x121 | A.method1 | 0x121 | A.method1 |
| 1 | 0x222 | B.method2 | 0x322 | C.method2 | ||
| 2 | 0x323 | C.method3 |
let obj = C()
obj.method2()
復(fù)制代碼
當(dāng)method2被調(diào)用時(shí),會(huì)經(jīng)歷下面的幾個(gè)過(guò)程:
- 讀取對(duì)象
0xC00的函數(shù)表 - 讀取函數(shù)指針的索引,
method2的地址為0x322 - 跳轉(zhuǎn)執(zhí)行
0x322
消息派發(fā)(Message dispatch)
熟悉 OC 的人都知道,OC 采用了運(yùn)行時(shí)機(jī)制使用 obj_msgSend 發(fā)送消息,runtime 非常的靈活,我們不僅可以對(duì)方法調(diào)用采用 swizzling,對(duì)于對(duì)象也可以通過(guò) isa-swizzling 來(lái)擴(kuò)展功能,應(yīng)用場(chǎng)景有我們常用的 hook 和大家熟知的 KVO。
大家在使用 Swift 進(jìn)行開(kāi)發(fā)時(shí)都會(huì)問(wèn),Swift 是否可以使用OC的運(yùn)行時(shí)和消息轉(zhuǎn)發(fā)機(jī)制呢?答案是可以。
Swift 可以通過(guò)關(guān)鍵字 dynamic 對(duì)方法進(jìn)行標(biāo)記,這樣就會(huì)告訴編譯器,此方法使用的是 OC 的運(yùn)行時(shí)機(jī)制。
id returnValue = [obj messageName:param];
// 底層代碼
id returnValue = objc_msgSend(obj, @selector(messageName:), param);
復(fù)制代碼
復(fù)制代碼
優(yōu)點(diǎn):
- 動(dòng)態(tài)性高
- Method Swizzling
- isa Swizzling
- ...
缺點(diǎn):
- 執(zhí)行效率是三種派發(fā)方式中最低的
所幸的是 objc_msgSend 會(huì)將匹配的結(jié)果緩存到一個(gè)映射表中,每個(gè)類都有這樣一塊緩存。若是之后發(fā)送相同的消息,執(zhí)行速率會(huì)很快。
總結(jié)來(lái)說(shuō),Swift 通過(guò) dynamic 關(guān)鍵字的擴(kuò)展后,一共包含三種方法分派方式:Static dispatch,Table dispatch 和 Message dispatch。下表為不同的數(shù)據(jù)結(jié)構(gòu)在不同情況下采取的分派方式:
| 類型 | 靜態(tài)派發(fā) | 函數(shù)表派發(fā) | 消息派發(fā) |
|---|---|---|---|
| 值類型 | 所有方法 | / | / |
| 協(xié)議 | extension | 主體創(chuàng)建 | / |
| 類 | extension/final/static | 主體創(chuàng)建 | @objc + dynamic |
| NSObject子類 | extension/final/static | 主體創(chuàng)建 | @objc + dynamic |
如果在開(kāi)發(fā)過(guò)程中,錯(cuò)誤的混合了這幾種分派方式,就可能出現(xiàn) Bug,以下我們對(duì)這些 Bug 進(jìn)行分析:
此情況是在子類的 extension 中重載父類方法時(shí),出現(xiàn)和預(yù)期不同的行為。
class Base:NSObject {
var directProperty:String { return "This is Base" }
var indirectProperty:String { return directProperty }
}
class Sub:Base { }
extension Sub {
override var directProperty:String { return "This is Sub" }
}
復(fù)制代碼
執(zhí)行以下代碼,直接調(diào)用沒(méi)有問(wèn)題:
Base().directProperty // “This is Base”
Sub().directProperty // “This is Sub”
復(fù)制代碼
間接調(diào)用結(jié)果和預(yù)期不同:
Base().indirectProperty // “This is Base”
Sub().indirectProperty // expected "this is Sub",but is “This is Base” <- Unexpected!
復(fù)制代碼
在 Base.directProperty 前添加 dynamic 關(guān)鍵字就可以獲得 "this is Sub" 的結(jié)果。Swift 在 extension 文檔 中說(shuō)明,不能在 extension 中重載已經(jīng)存在的方法。
“Extensions can add new functionality to a type, but they cannot override existing functionality.”
會(huì)出現(xiàn)報(bào)錯(cuò):Cannot override a non-dynamic class declaration from an extension。
[圖片上傳失敗...(image-c05dcd-1638867090451)]
出現(xiàn)這個(gè)問(wèn)題的原因是,NSObject 的 extension 是使用的 Message dispatch,而 Initial Declaration 使用的是 Table dispath(查看上圖)。extension 重載的方法添加在了 Message dispatch 內(nèi),沒(méi)有修改虛函數(shù)表,虛函數(shù)表內(nèi)還是父類的方法,故會(huì)執(zhí)行父類方法。想在 extension 重載方法,需要標(biāo)明dynamic來(lái)使用 Message dispatch。
協(xié)議的擴(kuò)展內(nèi)實(shí)現(xiàn)的方法,無(wú)法被遵守類的子類重載:
protocol Greetable {
func sayHi()
}
extension Greetable {
func sayHi() {
print("Hello")
}
}
func greetings(greeter:Greetable) {
greeter.sayHi()
}
復(fù)制代碼
現(xiàn)在定義一個(gè)遵守了協(xié)議的類 Person。遵守協(xié)議類的子類 LoudPerson:
class Person:Greetable {
}
class LoudPerson:Person {
func sayHi() {
print("sub")
}
}
復(fù)制代碼
執(zhí)行下面代碼結(jié)果為:
var sub:LoudPerson = LoudPerson()
sub.sayHi() //sub
復(fù)制代碼
不符合預(yù)期的代碼:
var sub:Person = LoudPerson()
sub.sayHi() //HellO <-使用了protocol的默認(rèn)實(shí)現(xiàn)
復(fù)制代碼
注意,在子類。LoudPerson中沒(méi)有出現(xiàn)override關(guān)鍵字。可以理解為LoudPerson并沒(méi)有成功注冊(cè)Greetable在Witness table的方法。所以對(duì)于聲明為Person實(shí)際為LoudPerson的實(shí)例,會(huì)在編譯器通過(guò)Person去查找,Person沒(méi)有實(shí)現(xiàn)協(xié)議方法,則不產(chǎn)生Witness table,sayHi方法是直接調(diào)用的。解決辦法是在base類內(nèi)實(shí)現(xiàn)協(xié)議方法,無(wú)需實(shí)現(xiàn)也要提供默認(rèn)方法。或者將基類標(biāo)記為final來(lái)避免繼承
這里的確沒(méi)有編譯驗(yàn)證,感謝 MemoryReload 的實(shí)踐驗(yàn)證,自己編譯了源碼之后糾正一下,這里的直接調(diào)用的確不是上述的原因:
- 在子類
LoudPerson中實(shí)現(xiàn)了sayHi方法,所以會(huì)直接調(diào)用LoudPerson的sayHi方法。 - 對(duì)于聲明為
Person實(shí)際為LoudPerson的實(shí)例,會(huì)在編譯器通過(guò)Person去查找,聲明在協(xié)議中的函數(shù)是使用函數(shù)表 vtable 派發(fā)的,從下面編譯的 SIL 代碼中也可以看到,Person的函數(shù)表 vtable 中沒(méi)有sayHi方法,實(shí)際上,Person類的sayHi方法只是在接口擴(kuò)展中進(jìn)行了定義,沒(méi)有最終的類型中實(shí)現(xiàn)。在使用時(shí),因?yàn)?Person類只是一個(gè)符合Greetable接口的實(shí)例,編譯器對(duì)sayHi唯一能確定的只是在接口擴(kuò)展中有一個(gè)默認(rèn)實(shí)現(xiàn),因此在調(diào)用時(shí),無(wú)法確定安全,也就不會(huì)去進(jìn)行動(dòng)態(tài)派發(fā),而是轉(zhuǎn)而編譯期間就確定的默認(rèn)實(shí)現(xiàn)。
import Foundation
protocol Greetable {
func sayHi()
}
extension Greetable {
func sayHi()
}
func greetings(greeter: Greetable)
class Person : Greetable {
@objc deinit
init()
}
@_inheritsConvenienceInitializers class LoudPerson : Person {
func sayHi()
override init()
@objc deinit
}
// main
sil @main : $@convention(c) (Int32, UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>) -> Int32 {
...
} // end sil function 'main'
// Greetable.sayHi()
sil hidden @$s16FunctionDispatch9GreetablePAAE5sayHiyyF : $@convention(method) <Self where Self : Greetable> (@in_guaranteed Self) -> () {
// %0 "self" // user: %1
bb0(%0 : $*Self):
debug_value_addr %0 : $*Self, let, name "self", argno 1 // id: %1
%2 = integer_literal $Builtin.Word, 1 // user: %4
// function_ref _allocateUninitializedArray<A>(_:)
%3 = function_ref @$ss27_allocateUninitializedArrayySayxG_BptBwlF : $@convention(thin) <τ_0_0> (Builtin.Word) -> (@owned Array<τ_0_0>, Builtin.RawPointer) // user: %4
%4 = apply %3<Any>(%2) : $@convention(thin) <τ_0_0> (Builtin.Word) -> (@owned Array<τ_0_0>, Builtin.RawPointer) // users: %6, %5
%5 = tuple_extract %4 : $(Array<Any>, Builtin.RawPointer), 0 // user: %17
%6 = tuple_extract %4 : $(Array<Any>, Builtin.RawPointer), 1 // user: %7
%7 = pointer_to_address %6 : $Builtin.RawPointer to [strict] $*Any // user: %14
%8 = string_literal utf8 "Hello" // user: %13
%9 = integer_literal $Builtin.Word, 5 // user: %13
%10 = integer_literal $Builtin.Int1, -1 // user: %13
%11 = metatype $@thin String.Type // user: %13
...
} // end sil function '$s16FunctionDispatch9GreetablePAAE5sayHiyyF'
// protocol witness for Greetable.sayHi() in conformance Person
sil private [transparent] [thunk] @$s16FunctionDispatch6PersonCAA9GreetableA2aDP5sayHiyyFTW : $@convention(witness_method: Greetable) <τ_0_0 where τ_0_0 : Person> (@in_guaranteed τ_0_0) -> () {
// %0 // user: %2
bb0(%0 : $*τ_0_0):
// function_ref Greetable.sayHi()
%1 = function_ref @$s16FunctionDispatch9GreetablePAAE5sayHiyyF : $@convention(method) <τ_0_0 where τ_0_0 : Greetable> (@in_guaranteed τ_0_0) -> () // user: %2
%2 = apply %1<τ_0_0>(%0) : $@convention(method) <τ_0_0 where τ_0_0 : Greetable> (@in_guaranteed τ_0_0) -> ()
%3 = tuple () // user: %4
return %3 : $() // id: %4
} // end sil function '$s16FunctionDispatch6PersonCAA9GreetableA2aDP5sayHiyyFTW'
// LoudPerson.sayHi()
sil hidden @$s16FunctionDispatch10LoudPersonC5sayHiyyF : $@convention(method) (@guaranteed LoudPerson) -> () {
sil_vtable Person {
#Person.init!allocator: (Person.Type) -> () -> Person : @$s16FunctionDispatch6PersonCACycfC // Person.__allocating_init()
#Person.deinit!deallocator: @$s16FunctionDispatch6PersonCfD // Person.__deallocating_deinit
}
sil_vtable LoudPerson {
#Person.init!allocator: (Person.Type) -> () -> Person : @$s16FunctionDispatch10LoudPersonCACycfC [override] // LoudPerson.__allocating_init()
#LoudPerson.sayHi: (LoudPerson) -> () -> () : @$s16FunctionDispatch10LoudPersonC5sayHiyyF // LoudPerson.sayHi()
#LoudPerson.deinit!deallocator: @$s16FunctionDispatch10LoudPersonCfD // LoudPerson.__deallocating_deinit
}
sil_witness_table hidden Person: Greetable module FunctionDispatch {
method #Greetable.sayHi: <Self where Self : Greetable> (Self) -> () -> () : @$s16FunctionDispatch6PersonCAA9GreetableA2aDP5sayHiyyFTW // protocol witness for Greetable.sayHi() in conformance Person
}
復(fù)制代碼
進(jìn)一步通過(guò)示例去理解:
// Defined protocol。
protocol A {
func a() -> Int
}
extension A {
func a() -> Int {
return 0
}
}
// A class doesn't have implement of the function。
class B:A {}
class C:B {
func a() -> Int {
return 1
}
}
// A class has implement of the function。
class D:A {
func a() -> Int {
return 1
}
}
class E:D {
override func a() -> Int {
return 2
}
}
// Failure cases。
B().a() // 0
C().a() // 1
(C() as A).a() // 0 # We thought return 1。
// Success cases。
D().a() // 1
(D() as A).a() // 1
E().a() // 2
(E() as A).a() // 2
復(fù)制代碼
如果對(duì)上述代碼的執(zhí)行結(jié)果理解的不到位的話,還可以借助喵神 PROTOCOL EXTENSION 里面的例子理解一下:
現(xiàn)在我們可以對(duì)一個(gè)已有的 protocol 進(jìn)行擴(kuò)展,而擴(kuò)展中實(shí)現(xiàn)的方法將作為實(shí)現(xiàn)擴(kuò)展的類型的默認(rèn)實(shí)現(xiàn)。也就是說(shuō),假設(shè)我們有下面的 protocol 聲明,以及一個(gè)對(duì)該接口的擴(kuò)展:
protocol MyProtocol {
func method()
}
extension MyProtocol {
func method() {
print("Called")
}
}
復(fù)制代碼
在具體的實(shí)現(xiàn)這個(gè)接口的類型中,即使我們什么都不寫(xiě),也可以編譯通過(guò)。進(jìn)行調(diào)用的話,會(huì)直接使用 extension 中的實(shí)現(xiàn):
struct MyStruct: MyProtocol {
}
MyStruct().method()
// 輸出:
// Called in extension
復(fù)制代碼
當(dāng)然,如果我們需要在類型中進(jìn)行其他實(shí)現(xiàn)的話,可以像以前那樣在具體類型中添加這個(gè)方法:
struct MyStruct: MyProtocol {
func method() {
print("Called in struct")
}
}
MyStruct().method()
// 輸出:
// Called in struct
復(fù)制代碼
也就是說(shuō),protocol extension 為 protocol 中定義的方法提供了一個(gè)默認(rèn)的實(shí)現(xiàn)。有了這個(gè)特性以后,之前被放在全局環(huán)境中的接受 CollectionType 的 map 方法,就可以被移動(dòng)到 CollectionType 的接口擴(kuò)展中去了:
extension CollectionType {
public func map<T>(@noescape transform: (Self.Generator.Element) -> T) -> [T]
//...
}
復(fù)制代碼
在日常開(kāi)發(fā)中,另一個(gè)可以用到 protocol extension 的地方是 optional 的接口方法。通過(guò)提供 protocol 的 extension,我們?yōu)?protocol 提供了默認(rèn)實(shí)現(xiàn),這相當(dāng)于變相將 protocol 中的方法設(shè)定為了 optional。關(guān)于這個(gè),我們?cè)?a target="_blank">可選接口和接口擴(kuò)展一節(jié)中已經(jīng)講述過(guò),就不再重復(fù)了。
對(duì)于 protocol extension 來(lái)說(shuō),有一種會(huì)非常讓人迷惑的情況,就是在接口的擴(kuò)展中實(shí)現(xiàn)了接口里沒(méi)有定義的方法時(shí)的情況。舉個(gè)例子,比如我們定義了這樣的一個(gè)接口和它的一個(gè)擴(kuò)展:
protocol A1 {
func method1() -> String
}
struct B1: A1 {
func method1() -> String {
return "hello"
}
}
復(fù)制代碼
在使用的時(shí)候,無(wú)論我們將實(shí)例的類型為 A1 還是 B1,因?yàn)閷?shí)現(xiàn)只有一個(gè),所以沒(méi)有任何疑問(wèn),調(diào)用方法時(shí)的輸出都是 “hello”:
let b1 = B1() // b1 is B1
b1.method1()
// hello
let a1: A1 = B1()
// a1 is A1
a1.method1()
// hello
復(fù)制代碼
但是如果在接口里只定義了一個(gè)方法,而在接口擴(kuò)展中實(shí)現(xiàn)了額外的方法的話,事情就變得有趣起來(lái)了。考慮下面這組接口和它的擴(kuò)展:
protocol A2 {
func method1() -> String
}
extension A2 {
func method1() -> String {
return "hi"
}
func method2() -> String {
return "hi"
}
}
復(fù)制代碼
擴(kuò)展中除了實(shí)現(xiàn)接口定義的 method1 之外,還定義了一個(gè)接口中不存在的方法 method2。我們嘗試來(lái)實(shí)現(xiàn)這個(gè)接口:
struct B2: A2 {
func method1() -> String {
return "hello"
}
func method2() -> String {
return "hello"
}
}
復(fù)制代碼
B2 中實(shí)現(xiàn)了 method1 和 method2。接下來(lái),我們嘗試初始化一個(gè) B2 對(duì)象,然后對(duì)這兩個(gè)方法進(jìn)行調(diào)用:
let b2 = B2()
b2.method1() // hello
b2.method2() // hello
復(fù)制代碼
結(jié)果在我們的意料之中,雖然在 protocol extension 中已經(jīng)實(shí)現(xiàn)了這兩個(gè)方法,但是它們只是默認(rèn)的實(shí)現(xiàn),我們?cè)诰唧w實(shí)現(xiàn)接口的類型中可以對(duì)默認(rèn)實(shí)現(xiàn)進(jìn)行覆蓋,這非常合理。但是如果我們稍作改變,在上面的代碼后面繼續(xù)添加:
let a2 = b2 as A2
a2.method1() // hello
a2.method2() // hi
復(fù)制代碼
a2 和 b2 是同一個(gè)對(duì)象,只不過(guò)我們通過(guò) as 告訴編譯器我們?cè)谶@里需要的類型是 A2。但是這時(shí)候在這個(gè)同樣的對(duì)象上調(diào)用同樣的方法調(diào)用卻得到了不同的結(jié)果,發(fā)生了什么?
我們可以看到,對(duì) a2 調(diào)用 method2 實(shí)際上是接口擴(kuò)展中的方法被調(diào)用了,而不是 a2 實(shí)例中的方法被調(diào)用。我們不妨這樣來(lái)理解:對(duì)于 method1,因?yàn)樗?protocol 中被定義了,因此對(duì)于一個(gè)被聲明為遵守接口的類型的實(shí)例 (也就是對(duì)于 a2) 來(lái)說(shuō),可以確定實(shí)例必然實(shí)現(xiàn)了 method1,我們可以放心大膽地用動(dòng)態(tài)派發(fā)的方式使用最終的實(shí)現(xiàn) (不論它是在類型中的具體實(shí)現(xiàn),還是在接口擴(kuò)展中的默認(rèn)實(shí)現(xiàn));但是對(duì)于 method2 來(lái)說(shuō),我們只是在接口擴(kuò)展中進(jìn)行了定義,沒(méi)有任何規(guī)定說(shuō)它必須在最終的類型中被實(shí)現(xiàn)。在使用時(shí),因?yàn)?a2 只是一個(gè)符合 A2 接口的實(shí)例,編譯器對(duì) method2 唯一能確定的只是在接口擴(kuò)展中有一個(gè)默認(rèn)實(shí)現(xiàn),因此在調(diào)用時(shí),無(wú)法確定安全,也就不會(huì)去進(jìn)行動(dòng)態(tài)派發(fā),而是轉(zhuǎn)而編譯期間就確定的默認(rèn)實(shí)現(xiàn)。
也許在這個(gè)例子中你會(huì)覺(jué)得無(wú)所謂,因?yàn)閷?shí)際中估計(jì)并不會(huì)有人將一個(gè)已知類型實(shí)例轉(zhuǎn)回接口類型。但是要考慮到如果你的一些泛型 API 中有類似的直接拿到一個(gè)接口類型的結(jié)果的時(shí)候,調(diào)用它的擴(kuò)展方法時(shí)就需要特別小心了:一般來(lái)說(shuō),如果有這樣的需求的話,我們可以考慮將這個(gè)接口類型再轉(zhuǎn)回實(shí)際的類型,然后進(jìn)行調(diào)用。
整理一下相關(guān)的規(guī)則的話:
-
如果類型推斷得到的是實(shí)際的類型
- 那么類型中的實(shí)現(xiàn)將被調(diào)用;如果類型中沒(méi)有實(shí)現(xiàn)的話,那么接口擴(kuò)展中的默認(rèn)實(shí)現(xiàn)將被使用
-
如果類型推斷得到的是接口,而不是實(shí)際類型
并且方法在接口中進(jìn)行了定義,那么類型中的實(shí)現(xiàn)將被調(diào)用;如果類型中沒(méi)有實(shí)現(xiàn),那么接口擴(kuò)展中的默認(rèn)實(shí)現(xiàn)被使用
否則 (也就是方法沒(méi)有在接口中定義),擴(kuò)展中的默認(rèn)實(shí)現(xiàn)將被調(diào)用
參考: