Swift-多態(tài)實(shí)現(xiàn)原理冲呢、初始化器語法學(xué)習(xí)

特別備注

本系列文章總結(jié)自MJ老師在騰訊課堂Swift編程從入門到精通-MJ大神精選,相關(guān)圖片素材均取自課程中的課件。

多態(tài)是面向?qū)ο蟮娜筇匦灾?當(dāng)時(shí)設(shè)計(jì)OOP機(jī)制的時(shí)候些楣,能夠想到多態(tài)的人昂勉,真特么太牛叉了浪册。

那什么是多態(tài)呢,滿足下面這幾個(gè)條件就可以稱為多態(tài):

1岗照、繼承了某個(gè)類村象、實(shí)現(xiàn)了某個(gè)接口

2、重寫父類的方法攒至、實(shí)現(xiàn)接口中的方法

3厚者、父類引用指向子類對(duì)象

一. 多態(tài)實(shí)現(xiàn)原理

多態(tài)就是父類指針指向子類對(duì)象

關(guān)于多態(tài):在編譯的時(shí)候并不知道要調(diào)用的是父類還是子類的方法,運(yùn)行的時(shí)候才會(huì)根據(jù)實(shí)際類型調(diào)用子類的方法迫吐。

對(duì)于結(jié)構(gòu)體來說库菲,因?yàn)榻Y(jié)構(gòu)體沒有繼承,編譯的時(shí)候就能知道調(diào)用哪個(gè)方法志膀。
但是對(duì)于類熙宇,只有在運(yùn)行的時(shí)候才能知道實(shí)際調(diào)用哪個(gè)方法。

先看下面這段程序的運(yùn)行溉浙,咋們開始分析swift多態(tài)的實(shí)現(xiàn)原理烫止。

class Animal {
    func speak() {
        print("Animal speak")
    }
    func eat() {
        print("Animal eat")
    }
    func sleep() {
        print("Animal sleep")
    }
}

class Dog : Animal {
    override func speak() {
        print("Dog speak")
    }
    override func eat() {
        print("Dog eat")
    }
    func run() {
        print("Dog run")
    }
}

var anim = Animal() //父類指針
anim = Dog() //指向子類對(duì)象

//這時(shí)候,編譯器提示放航,anim是Animal類型的烈拒,但是實(shí)際是Dog類型的
anim.speak()
anim.eat()
anim.sleep()

Run->

Dog speak
Dog eat
Animal sleep

先從結(jié)構(gòu)體開始看方法的調(diào)用

struct Animal {
    func speak() {
        print("Animal speak")
    }
    func eat() {
        print("Animal eat")
    }
    func sleep() {
        print("Animal sleep")
    }
}

var anim = Animal()
anim.speak()
anim.eat()
anim.sleep()

首先看看調(diào)用結(jié)構(gòu)體方法的匯編:

image-20210411145410172

struct結(jié)構(gòu)體,不存在繼承广鳍,方法在編譯的時(shí)候就和類型綁定了荆几,知道函數(shù)編譯后的地址,直接使用call調(diào)用赊时。

我們把struct改成class,再來看看他的的匯編:

image-20210411150216870
image-20210411150540231

可以看到,方法地址不是固定的,而是通過一定計(jì)算得來的.因?yàn)?code>class支持繼承,所以它的方法地址一定不是寫死的,而是通過計(jì)算得來的.所以在項(xiàng)目中如果我們只是單純的調(diào)用方法,那我們用結(jié)構(gòu)體就可以了,從匯編代碼可以看出,結(jié)構(gòu)體的匯編代碼更少,流程更簡單,效率更快.

下面我們就來研究 Swift 中,多態(tài)的實(shí)現(xiàn)原理.使用一開始就多態(tài)示例運(yùn)行的代碼吨铸,來看匯編運(yùn)行步驟

image-20210411152313498

運(yùn)行到callq *0x50(%rcx) 可以看到rcx讀取的值為0x000000010000c6b8 type metadata for TestSwift.Dog

image-20210411152411066
image-20210411152938381

為Dog對(duì)象的類型信息metadata

如下圖,通過窺探匯編得到的:

img

上圖就是子類在調(diào)用方法的時(shí)候是如何調(diào)用的流程,需要注意的是類型信息存儲(chǔ)在全局區(qū)

多態(tài)原理

我們都知道Dog對(duì)象的前8個(gè)字節(jié)是指向類型相關(guān)信息(metadata)祖秒,如上圖诞吱,類型相關(guān)的那塊區(qū)域里面,前面一塊區(qū)域存放Dog的類型信息相關(guān)竭缝,后面一塊區(qū)域存放方法的地址房维,比如Dog重寫了speak和eat,所以這兩個(gè)方法在最前面抬纸,沒有重寫sleep所以再后面是Animal.sleep咙俩,最后是Dog.run。

當(dāng)調(diào)用anim.speak(),首先先取出Dog對(duì)象的前8個(gè)字節(jié)阿趁,根據(jù)這8個(gè)字節(jié)指針找到指針指向的內(nèi)存膜蛔,找到內(nèi)存地址之后加上一個(gè)固定偏移量,就會(huì)獲得Dog.speak方法的內(nèi)存地址脖阵,找到方法之后調(diào)用皂股。

同理,Dog eat也是先取出前8字節(jié)命黔,找到這8字節(jié)的內(nèi)存地址呜呐,然后再加上一個(gè)固定偏移量,找到Dog.eat方法纷铣,找到方法之后調(diào)用卵史。

如果Dog沒有重寫Animal的方法,那么類型信息那塊區(qū)域存儲(chǔ)的方法就是搜立,如下圖:

img

如果是兩個(gè)dog對(duì)象,他們的類型信息肯定也是一樣的槐秧,如下圖:

兩個(gè)dog對(duì)象.png

那么Dog的類型信息會(huì)放在代碼段啄踊、數(shù)據(jù)段、堆刁标、棧中的哪一段颠通?
我們可以猜測(cè)一下,因?yàn)轭愋托畔⒁恢痹趦?nèi)存中膀懈,首先排除棧顿锰,代碼段一般放編譯之后的代碼也排除,堆空間一般放alloc启搂,malloc等動(dòng)態(tài)分配的東西硼控,也排除,所以Dog的類型信息只能存放數(shù)據(jù)段胳赌。

總結(jié):程序在編譯的時(shí)候就將函數(shù)地址放到了類型信息那塊內(nèi)存區(qū)域了牢撼,當(dāng)程序運(yùn)行的時(shí)候再到這塊內(nèi)存區(qū)域找函數(shù)地址,找到之后就調(diào)用疑苫。

二. 初始化器

image-20210411153350586

初始化器

枚舉:枚舉可以使?rawValue來給枚舉賦值熏版,沒有自動(dòng)生成的初始化器。
結(jié)構(gòu)體:所有的結(jié)構(gòu)體都有編譯器?動(dòng)?成的初始化器(也許不??個(gè))捍掺,?于初始化所有成員撼短,但是如果你?定義了初始化器,編譯器就不會(huì)幫你了挺勿。
類:編譯器沒有為類?動(dòng)?成可以傳?成員值的初始化器(想讓我們??寫)曲横,但是如果屬性都有默認(rèn)值,也會(huì)幫我們創(chuàng)建?個(gè)?參初始化器满钟。

枚舉胜榔、結(jié)構(gòu)體胳喷、類都可以自定義初始化器。

類有2種初始化器:指定初始化器(designated initializer)夭织、便捷初始化器(convenience initializer)

// 指定初始化器
init(parameters) {
    statements
}

// 便捷初始化器
convenience init(parameters) {
    statements
}

初始化器的唯一原則就是:保證所有成員都有值.在 swift 中,初始化器可以分為兩種:1: 指定初始化器 , 2:便捷初始化器

每個(gè)類至少有一個(gè)指定初始化器吭露,指定初始化器是類的主要初始化器
默認(rèn)初始化器總是類的指定初始化器
類偏向于少量指定初始化器,一個(gè)類通常只有一個(gè)指定初始化器

初始化器的相互調(diào)用規(guī)則:
1. 指定初始化器必須從它的直系父類調(diào)用指定初始化器
2. 便捷初始化器必須從相同的類里調(diào)用另一個(gè)初始化器尊惰,最終必須調(diào)用一個(gè)指定初始化器

1:指定初始化器

指定初始化器就是之前我們使用的初始化器,沒有自動(dòng)生成的初始化器

class Size {
    var width: Int = 0
    var height: Int = 0
    
}

var s4 = Size()
image-20210411154348608

從匯編我們可以看到讲竿,調(diào)用了Size的init()方法。

自定義初始化器

image-20210411155044743

一旦我們自定義了初始化器,編譯器就不會(huì)為我們?cè)偕赡J(rèn)初始化器了.

指定初始化器就是類的主要初始化器,可以理解為主線.一個(gè)類至少有一個(gè)指定初始化器,并且 swift 官方建議不要過多的指定初始化器,一個(gè)類通常只有一個(gè)指定初始化器.

  • 定義便攜初始化器
class Size {
    var width: Int = 0
    var height: Int = 0
    
   convenience init(width: Int, height: Int) {
        self.init()
        self.width = width
        self.height = height
    }
    
}
  • 便捷初始化器必須從相同的類里調(diào)用另一個(gè)初始化器弄屡,最終必須調(diào)用一個(gè)指定初始化器
image-20210411155809398
class Size {
    var width: Int
    var height: Int
    // 指定初始化器(主要初始化器)
    init(width: Int, height: Int) {
        self.width = width
        self.height = height
    }

    convenience init(width: Int) {
        self.init(width: width, height: 0)
    }

    convenience init(height: Int) {
        self.init(width: 0, height: height)
    }

    convenience init() {
        self.init(width: 0, height: 0)
    }
}

如果有繼承關(guān)系的類,子類的指定初始化器必須調(diào)用直系父類的指定初始化器

image-20210411160523521

那便捷初始化器可不可以調(diào)用便捷初始化器呢?

img

從上圖可以看到,便捷初始化器是可以調(diào)用便捷初始化器的,但是必須遵從一條規(guī)則:便捷初始化器最終必須要調(diào)用指定初始化器

總結(jié):
1. 如果是指定初始化器,必須調(diào)用直系父類的指定初始化器;
2. 如果是便捷初始化器,必須調(diào)用自己的指定初始化器;

image-20210411161604346

這一套規(guī)則保證了使用任意初始化器题禀,都可以完整地初始化實(shí)例。

2. 兩段式初始化膀捷、安全檢查

image-20210411161804494
image-20210411162843218
class BasePerson {
    var IDnum: Int
    init(IDnum: Int) {
        
        //這里打勇踵凇:print(self.IDnum)和方法調(diào)用self.test(),都會(huì)報(bào)錯(cuò):Variable 'self.IDnum' used before being initialized
        
        self.IDnum = IDnum //8.父類的指定初始化器初始化自己的屬性
        
        //9.至此第一階段結(jié)束全庸,才可以使用self
        
        print(self.IDnum) //這里打印不會(huì)報(bào)錯(cuò)
        self.test() //方法調(diào)用也不會(huì)報(bào)錯(cuò)
    }
    convenience init() {
        self.init(IDnum: 1234567890)
    }
    
    func test() -> Void {
        print("測(cè)試")
    }
}

class Person : BasePerson {
    var age: Int
    var name: String
     init(age: Int, name: String) { //5.來到父類的指定初始化器
        self.age = age
        self.name = name      //6.父類也是先初始化自己的屬性
        
        super.init(IDnum: 0)  //7.再調(diào)用父類的指定初始化器
        
        self.IDnum = 3412211992 //10.父類的初始化器調(diào)用完秀仲,接著進(jìn)一步定制實(shí)例
        
    }
    convenience init() {
        self.init(age: 0, name: "")
    }
    convenience init(age: Int) {
        self.init(age: age, name: "")
    }
    convenience init(name: String) {
        self.init(age: 0, name: name)
    }
}

class Student : Person {
    var score: Int
    init(age: Int, score:Int) {
        self.score = score; //3.指定初始化器確保當(dāng)前類定義的存儲(chǔ)屬性都初始化
        
        super.init(age: age, name: "") //4.指定初始化器調(diào)用父類的初始化器,不斷向上調(diào)用壶笼,形成初始化器鏈
        
        self.age = 10 //11.父類的初始化器調(diào)用完神僵,接著進(jìn)一步定制實(shí)例
    }
    
    convenience init() {
        self.init(age: 0, score:0) //2.便捷初始化器調(diào)用指定初始化器
        
        //12.至此第二階段結(jié)束,所有初始化完成
    }
}

var stu = Student() //1.外層調(diào)用便捷初始化器
  • 安全檢查
  1. 指定初始化器必須保證其所在類定義的所有存儲(chǔ)屬性都要初始化完成覆劈,再調(diào)用父類初始化器
  2. 指定初始化器必須先調(diào)用父類初始化器保礼,然后才能為繼承的屬性設(shè)置新值
  3. 便捷初始化器必須先調(diào)用同類中的其它初始化器,然后再為任意屬性設(shè)置新值
  4. 初始化器在第1階段初始化完成之前责语,不能調(diào)用任何實(shí)例方法炮障、不能讀取任何實(shí)例屬性的值,也不能引用self
  5. 直到第1階段結(jié)束鹦筹,實(shí)例才算完全合法

初始化器的重寫

image-20210411163500657
  1. 當(dāng)子類重寫父類的指定初始化器時(shí),必須加上override關(guān)鍵字.子類可以把父類的指定初始化器重寫為指定初始化器和便捷初始化器.
    • 重寫為指定初始化器
class Person {
    var age: Int
    init(age: Int) {
        self.age = age
    }
}

class Student : Person {
    var score : Int
    init(age: Int, score: Int) {
        self.score = score
        super.init(age: age)
    }
    
    override init(age: Int) {
        self.score = 0
        super.init(age: age)
    }
}
  • 重寫為便捷初始化器
image-20210411163826942
class Student : Person {
    var score : Int
    init(age: Int, score: Int) {
        self.score = score
        super.init(age: age)
    }
    
   override convenience init(age: Int) {
        self.init(age: age,score: 0)
    }
}
  1. 如果子類重寫了一個(gè)和父類便捷初始化器一模一樣的初始化器,不用加override,嚴(yán)格來講,這并不是重寫.
image-20210411164423419
image-20210411164523518

因?yàn)?strong>便捷初始化器是橫向調(diào)用的,它只能在類本身調(diào)用,子類是不能調(diào)用父類的便捷初始化器的.而重寫可以通過super調(diào)用父類的方法,但是卻不能通過super調(diào)用父類的便捷初始化器,這就沖突了.

初始化器的繼承

  1. 如果子類沒有自定義任何指定初始化器,它將繼承父類所有指定初始化器
class Person {
    var age: Int
    var name: String
    init(age: Int, name: String) {
        self.age = age
        self.name = name
    }
    init() {
        self.age = 0
        self.name = ""
    }
}

class Student : Person {
    
}
image-20210411165224252
image-20210411165632740
  1. 如果子類提供了父類所有指定初始化器的實(shí)現(xiàn)(兩種方式: 1: 通過第一條繼承 2: 通過重寫),子類會(huì)自動(dòng)繼承所有父類的便捷初始化器
class Person {
    var age: Int
    var name: String
    init(age: Int, name: String) {
        self.age = age
        self.name = name
    }
    init() {
        self.age = 0
        self.name = ""
    }
    convenience init(age: Int) {
        self.init(age: age, name: "")
    }
    convenience init(name: String) {
        self.init(age: 0, name: name)
    }
}
image-20210411170033886
image-20210411170350668
image-20210411170632266
特別備注

本系列文章總結(jié)自MJ老師在騰訊課堂Swift編程從入門到精通-MJ大神精選铝阐,相關(guān)圖片素材均取自課程中的課件。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末铐拐,一起剝皮案震驚了整個(gè)濱河市徘键,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌遍蟋,老刑警劉巖吹害,帶你破解...
    沈念sama閱讀 210,914評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異虚青,居然都是意外死亡它呀,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,935評(píng)論 2 383
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來纵穿,“玉大人下隧,你說我怎么就攤上這事∥矫剑” “怎么了淆院?”我有些...
    開封第一講書人閱讀 156,531評(píng)論 0 345
  • 文/不壞的土叔 我叫張陵,是天一觀的道長句惯。 經(jīng)常有香客問我土辩,道長,這世上最難降的妖魔是什么抢野? 我笑而不...
    開封第一講書人閱讀 56,309評(píng)論 1 282
  • 正文 為了忘掉前任拷淘,我火速辦了婚禮,結(jié)果婚禮上指孤,老公的妹妹穿的比我還像新娘启涯。我一直安慰自己,他們只是感情好邓厕,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,381評(píng)論 5 384
  • 文/花漫 我一把揭開白布逝嚎。 她就那樣靜靜地躺著,像睡著了一般详恼。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上引几,一...
    開封第一講書人閱讀 49,730評(píng)論 1 289
  • 那天昧互,我揣著相機(jī)與錄音,去河邊找鬼伟桅。 笑死敞掘,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的楣铁。 我是一名探鬼主播玖雁,決...
    沈念sama閱讀 38,882評(píng)論 3 404
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢(mèng)啊……” “哼盖腕!你這毒婦竟也來了赫冬?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,643評(píng)論 0 266
  • 序言:老撾萬榮一對(duì)情侶失蹤溃列,失蹤者是張志新(化名)和其女友劉穎劲厌,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體听隐,經(jīng)...
    沈念sama閱讀 44,095評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡补鼻,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,448評(píng)論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片风范。...
    茶點(diǎn)故事閱讀 38,566評(píng)論 1 339
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡咨跌,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出硼婿,到底是詐尸還是另有隱情锌半,我是刑警寧澤,帶...
    沈念sama閱讀 34,253評(píng)論 4 328
  • 正文 年R本政府宣布加酵,位于F島的核電站拳喻,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏猪腕。R本人自食惡果不足惜冗澈,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,829評(píng)論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望陋葡。 院中可真熱鬧亚亲,春花似錦、人聲如沸腐缤。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,715評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽岭粤。三九已至惜索,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間剃浇,已是汗流浹背巾兆。 一陣腳步聲響...
    開封第一講書人閱讀 31,945評(píng)論 1 264
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留虎囚,地道東北人角塑。 一個(gè)月前我還...
    沈念sama閱讀 46,248評(píng)論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像淘讥,于是被迫代替她去往敵國和親圃伶。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,440評(píng)論 2 348

推薦閱讀更多精彩內(nèi)容