Swift 函數(shù)派發(fā)機(jī)制

函數(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 dispatchStruct 默認(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 TypesGeneric code,它們?cè)谶@三方面的表現(xiàn)如何,Protocol TypeGeneric 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)?PointLine 的尺寸不同,數(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 TableProtocol 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ò)程:

  1. 讀取對(duì)象 0xC00 的函數(shù)表
  2. 讀取函數(shù)指針的索引, method2 的地址為0x322
  3. 跳轉(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 dispatchMessage 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é)果。Swiftextension 文檔 中說(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)題的原因是,NSObjectextension 是使用的 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è) GreetableWitness 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)用 LoudPersonsayHi 方法。
  • 對(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)境中的接受 CollectionTypemap 方法,就可以被移動(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)了 method1method2。接下來(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ù)制代碼

a2b2 是同一個(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)用

參考:

PROTOCOL EXTENSION

【基本功】深入剖析Swift性能優(yōu)化

從SIL看Swift函數(shù)派發(fā)機(jī)制

作者:奉孝
鏈接:https://juejin.cn/post/7033682844581019656

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容