iOS-Swift-多態(tài)實現(xiàn)原理它浅、初始化器

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

多態(tài)就是父類指針指向子類對象荠察。
關(guān)于多態(tài):在編譯的時候并不知道要調(diào)用的是父類還是子類的方法置蜀,運(yùn)行的時候才會根據(jù)實際類型調(diào)用子類的方法。

對于結(jié)構(gòu)體來說悉盆,因為結(jié)構(gòu)體沒有繼承盯荤,編譯的時候就能知道調(diào)用哪個方法。
但是對于類焕盟,只有在運(yùn)行的時候才能知道實際調(diào)用哪個方法秋秤。

多態(tài)實現(xiàn)原理:
OC:通過Runtime將一些方法放到方法列表里面
C++:虛表(虛函數(shù)表)
Swift中的多態(tài):和虛表很像

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() //指向子類對象

//這時候,編譯器提示脚翘,anim是Animal類型的航缀,但是實際是Dog類型的
anim.speak()
anim.eat()
anim.sleep()

打印:

Dog speak
Dog eat
Animal sleep

我們都知道是上面的打印結(jié)果堰怨,但是原理是什么呢芥玉?如下圖,下面的結(jié)論都是MJ老師通過窺探匯編得到的:

多態(tài)原理

我們都知道Dog對象的前8個字節(jié)是指向類型相關(guān)备图,如上圖灿巧,類型相關(guān)的那塊區(qū)域里面,前面一塊區(qū)域存放Dog的類型信息相關(guān)揽涮,后面一塊區(qū)域存放方法的地址抠藕,比如Dog重寫了speak和eat,所以這兩個方法在最前面蒋困,沒有重寫sleep所以再后面是Animal.sleep盾似,最后是Dog.run。

當(dāng)調(diào)用anim.speak()雪标,首先先取出Dog對象的前8個字節(jié)零院,根據(jù)這8個字節(jié)指針找到指針指向的內(nèi)存,找到內(nèi)存地址之后加上一個固定偏移量村刨,就會獲得Dog.speak方法的內(nèi)存地址告抄,找到方法之后調(diào)用。

同理嵌牺,Dog eat也是先取出前8字節(jié)打洼,找到這8字節(jié)的內(nèi)存地址龄糊,然后再加上一個固定偏移量,找到Dog.eat方法募疮,找到方法之后調(diào)用炫惩。

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

沒重寫

如果是兩個dog對象诡必,他們的類型信息肯定也是一樣的,如下圖:

兩個dog對象.png

那么Dog的類型信息會放在代碼段搔扁、數(shù)據(jù)段裳仆、堆嗦锐、棧中的哪一段?
我們可以猜測一下皂冰,因為類型信息要一直在內(nèi)存中鹊奖,首先排除棧苛聘,代碼段一般放編譯之后的代碼也排除,堆空間一般放alloc忠聚,malloc等動態(tài)分配的東西设哗,也排除,所以Dog的類型信息只能存放數(shù)據(jù)段两蟀。
MJ老師通過打斷點查看匯編网梢,比較地址值也得出結(jié)論,的確是在數(shù)據(jù)段赂毯。

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

二. 初始化器

1. 初始化器

枚舉:枚舉可以使?rawValue來給枚舉賦值烦感,沒有自動生成的初始化器。
結(jié)構(gòu)體:所有的結(jié)構(gòu)體都有編譯器?動?成的初始化器(也許不??個)膛堤,?于初始化所有成員手趣,但是如果你?定義了初始化器,編譯器就不會幫你了肥荔。
類:編譯器沒有為類?動?成可以傳?成員值的初始化器(想讓我們??寫)绿渣,但是如果屬性都有默認(rèn)值,也會幫我們創(chuàng)建?個?參初始化器燕耿。

當(dāng)然怯晕,枚舉、結(jié)構(gòu)體缸棵、類都可以自定義初始化器舟茶。

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

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

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

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

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

(其實就是,便捷初始化器是橫向調(diào)用阀捅,指定初始化器是豎向調(diào)用)

便攜指向指定胀瞪,指定指向?類.png

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

2. 兩段式初始化凄诞、安全檢查

Swift在編碼安全方面是煞費(fèi)苦心,為了保證初始化過程的安全忍级,設(shè)定了兩段式初始化帆谍、 安全檢查

① 兩段式初始化

第1階段:初始化所有存儲屬性
① 外層調(diào)用指定\便捷初始化器
② 分配內(nèi)存給實例,但未初始化
③ 指定初始化器確保當(dāng)前類定義的存儲屬性都初始化
④ 指定初始化器調(diào)用父類的初始化器轴咱,不斷向上調(diào)用汛蝙,形成初始化器鏈

第2階段:設(shè)置新的存儲屬性值
⑤ 從頂部初始化器往下,鏈中的每一個指定初始化器都有機(jī)會進(jìn)一步定制實例
⑥ 初始化器現(xiàn)在能夠使用self訪問朴肺、修改它的屬性窖剑,調(diào)用它的實例方法等等
⑦ 最終,鏈中任何便捷初始化器都有機(jī)會定制實例以及使用self

② 安全檢查

  1. 指定初始化器必須保證其所在類定義的所有存儲屬性都要初始化完成戈稿,再調(diào)用父類初始化器
  2. 指定初始化器必須先調(diào)用父類初始化器西土,然后才能為繼承的屬性設(shè)置新值
  3. 便捷初始化器必須先調(diào)用同類中的其它初始化器,然后再為任意屬性設(shè)置新值
  4. 初始化器在第1階段初始化完成之前鞍盗,不能調(diào)用任何實例方法翠储、不能讀取任何實例屬性的值,也不能引用self
  5. 直到第1階段結(jié)束橡疼,實例才算完全合法

光看上面兩段文字可能比較難理解援所,結(jié)合下面代碼,從第1步到第12步欣除,看完你就明白了:

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

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)一步定制實例
        
    }
    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)前類定義的存儲屬性都初始化
        
        super.init(age: age, name: "") //4.指定初始化器調(diào)用父類的初始化器,不斷向上調(diào)用挽牢,形成初始化器鏈
        
        self.age = 10 //11.父類的初始化器調(diào)用完谱煤,接著進(jìn)一步定制實例
    }
    
    convenience init() {
        self.init(age: 0, score:0) //2.便捷初始化器調(diào)用指定初始化器
        
        //12.至此第二階段結(jié)束,所有初始化完成
    }
}

var stu = Student() //1.外層調(diào)用便捷初始化器

補(bǔ)充:一般我們自定義UIView都會重寫它的init(frame: CGRect)方法禽拔,如下:

override init(frame: CGRect) {
    super.init(frame: frame)
    setupLayout()
}

這時候你可能會疑惑為什么這里的super.init在最上面刘离?
其實不管是重寫初始化器還是自定義初始化器室叉,都是定義一個初始化器,重寫初始化器一般沒有新的參數(shù)(作為屬性)硫惕,所以super.init在最上面茧痕,如果是自定義一個新的初始化器,一般都有新的參數(shù)(作為屬性)恼除,所以就是上面的兩段式初始化踪旷。

3. 重寫、自動繼承豁辉、required令野、屬性觀察器

① 重寫

  1. 當(dāng)重寫父類的指定初始化器時,必須加上override(即使子類的實現(xiàn)是便捷初始化器)徽级。

代碼如下:

class Person {
    var age: Int
    init(age: Int) {
        self.age = age
    }
    
    convenience init() {
        self.init(age:0)
    }
}

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)
//    }
    
    // 子類重寫父類的指定初始化器為便捷初始化器
    override convenience init(age: Int) {
        self.init(age: age, score: 0)
    }
}
  1. 如果子類寫了一個匹配父類便捷初始化器的初始化器气破,不用加上override。因為父類的便捷初始化器永遠(yuǎn)不會通過子類直接調(diào)用灰追,因此,嚴(yán)格來說狗超,子類無法重寫父類的便捷初始化器弹澎。

代碼如下:

class Person {
    var age: Int
    init(age: Int) {
        self.age = age
    }
    
    convenience init() {
        self.init(age:0)
    }
}

class Student: Person {
    var score: Int
    init(age: Int, score: Int) {
        self.score = score
        super.init(age: age)
    }
    
    //也不報錯
    init() {
        self.score = 0
        super.init(age: 0)
    }
    
    //不報錯
//    convenience init() {
//        self.init(age: 0, score: 0)
//    }

//上面不叫重寫,我們假設(shè)它是重寫父類的便捷初始化器init()努咐,既然是重寫苦蒿,那么在子類的方法中就可以調(diào)用super.init()
//很顯然,對于指定初始化器只能調(diào)用父類的指定初始化器渗稍,不可以調(diào)用super.init()佩迟。
//對于便捷初始化器只能調(diào)用子類的指定初始化器,更不可能調(diào)用super.init()竿屹,所以上面不叫重寫报强。
}

② 自動繼承

  1. 如果子類沒有自定義任何指定初始化器,它會自動繼承父類所有的指定初始化器(一旦你自定義了一個指定初始化器拱燃,那么父類的指定初始化器統(tǒng)統(tǒng)不給你用了)
  2. 如果子類提供了父類所有指定初始化器的實現(xiàn)(要么通過方式1繼承秉溉,要么重寫),那么子類自動繼承所有的父類便捷初始化器
  3. 子類以便捷初始化器的形式重寫父類的指定初始化器碗誉,也可以作為滿足規(guī)則2的一部分
  4. 就算子類添加了更多的便捷初始化器召嘶,這些規(guī)則仍然適用

補(bǔ)充:我們一般不自定義指定初始化器,這樣才能自動繼承父類的指定初始化器哮缺,我們一般自定義便捷初始化器弄跌,如下:

//便捷初始化器必須先調(diào)用同類中的其它初始化器(這里是繼承父類的),然后再為任意屬性設(shè)置新值
convenience init(titles: [String] = [], vcs: [UIViewController] = [], pageStyle: XUPageStyle = .noneStyle) {
    self.init()
    self.titles = titles
    self.vcs = vcs
    self.pageStyle = pageStyle
}

③ required

  1. 用required修飾指定初始化器(修飾便捷初始化器沒啥用)尝苇,表明其所有子類都必須實現(xiàn)該初始化器(通過繼承或者重寫實現(xiàn))
  2. 如果子類重寫了required初始化器铛只,也必須加上required埠胖,不用加override
class Person {
    required init() { }
    init(age: Int) { }
}
class Student : Person {
    required init() {
        super.init()
    }
}

④ 屬性觀察器

父類的屬性在它自己的初始化器中賦值不會觸發(fā)屬性觀察器,但在子類的初始化器中賦值會觸發(fā)屬性觀察器

class Person {
    var age: Int {
        willSet {
            print("willSet", newValue)
        }
        didSet {
            print("didSet", oldValue, age)
        }
    }
    init() {
        self.age = 0
    }
}

class Student : Person {
    override init() {
        super.init()
        self.age = 1
    }
}
// willSet 1
// didSet 0 1
var stu = Student()

這個也很容易理解格仲,age本來就是從父類來的押袍,你修改了父類的屬性,肯定要觸發(fā)父類的屬性觀察器凯肋。

4. 可失敗初始化器谊惭、反初始化器

① 可失敗初始化器

枚舉、結(jié)構(gòu)體侮东、類都可以使用init?定義可失敗初始化器圈盔。

如下:

class Person {
    var name: String
    init?(name: String) {
        if name.isEmpty {
            return nil
            }
        self.name = name
    }
}

之前接觸過的可失敗初始化器,如下:

var num = Int("123")  //源碼就是下一行
public init?(_ description: String)
enum Answer : Int {
    case wrong, right
}
var an = Answer(rawValue: 1) //如果傳入2就創(chuàng)建失敗悄雅,所以是可失敗的
  1. 不允許同時定義參數(shù)標(biāo)簽驱敲、參數(shù)個數(shù)、參數(shù)類型相同的可失敗初始化器和非可失敗初始化器(因為不知道調(diào)用哪個了)
  2. 可以用init!定義隱式解包的可失敗初始化器
  3. 可失敗初始化器可以調(diào)用非可失敗初始化器宽闲,非可失敗初始化器調(diào)用可失敗初始化器需要進(jìn)行解包
  4. 如果初始化器調(diào)用一個可失敗初始化器導(dǎo)致初始化失敗众眨,那么整個初始化過程都失敗,并且之后的代碼都停止執(zhí)行
  5. 可以用一個非可失敗初始化器重寫一個可失敗初始化器容诬,但反過來是不行的娩梨。

② 反初始化器(deinit)

deinit叫做反初始化器,類似于C++的析構(gòu)函數(shù)览徒、OC中的dealloc方法
當(dāng)類的實例對象被釋放內(nèi)存時狈定,就會調(diào)用實例對象的deinit方法

class Person {
    deinit {
        print("Person deinit")
    }
}
  1. deinit不接受任何參數(shù),不能寫小括號
  2. 父類的deinit能被子類繼承
  3. 子類的deinit實現(xiàn)執(zhí)行完畢后會調(diào)用父類的deinit

如下习蓬,子類沒有實現(xiàn)deinit纽什,通過打印可知,子類繼承了父類的deinit

class Person {
    deinit {
        print("Person deinit")
    }
}

class Student: Person {
}

func test() {
    var stu = Student()
}

test() //打佣愕稹:Person deinit
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末芦缰,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子枫慷,更是在濱河造成了極大的恐慌饺藤,老刑警劉巖,帶你破解...
    沈念sama閱讀 210,914評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件流礁,死亡現(xiàn)場離奇詭異涕俗,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)神帅,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,935評論 2 383
  • 文/潘曉璐 我一進(jìn)店門再姑,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人找御,你說我怎么就攤上這事元镀∩芴睿” “怎么了?”我有些...
    開封第一講書人閱讀 156,531評論 0 345
  • 文/不壞的土叔 我叫張陵栖疑,是天一觀的道長讨永。 經(jīng)常有香客問我,道長遇革,這世上最難降的妖魔是什么卿闹? 我笑而不...
    開封第一講書人閱讀 56,309評論 1 282
  • 正文 為了忘掉前任,我火速辦了婚禮萝快,結(jié)果婚禮上锻霎,老公的妹妹穿的比我還像新娘。我一直安慰自己揪漩,他們只是感情好旋恼,可當(dāng)我...
    茶點故事閱讀 65,381評論 5 384
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著奄容,像睡著了一般冰更。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上昂勒,一...
    開封第一講書人閱讀 49,730評論 1 289
  • 那天蜀细,我揣著相機(jī)與錄音,去河邊找鬼叁怪。 笑死审葬,一個胖子當(dāng)著我的面吹牛深滚,可吹牛的內(nèi)容都是我干的奕谭。 我是一名探鬼主播,決...
    沈念sama閱讀 38,882評論 3 404
  • 文/蒼蘭香墨 我猛地睜開眼痴荐,長吁一口氣:“原來是場噩夢啊……” “哼血柳!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起生兆,我...
    開封第一講書人閱讀 37,643評論 0 266
  • 序言:老撾萬榮一對情侶失蹤难捌,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后鸦难,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體根吁,經(jīng)...
    沈念sama閱讀 44,095評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,448評論 2 325
  • 正文 我和宋清朗相戀三年合蔽,在試婚紗的時候發(fā)現(xiàn)自己被綠了击敌。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,566評論 1 339
  • 序言:一個原本活蹦亂跳的男人離奇死亡拴事,死狀恐怖沃斤,靈堂內(nèi)的尸體忽然破棺而出圣蝎,到底是詐尸還是另有隱情,我是刑警寧澤衡瓶,帶...
    沈念sama閱讀 34,253評論 4 328
  • 正文 年R本政府宣布徘公,位于F島的核電站,受9級特大地震影響哮针,放射性物質(zhì)發(fā)生泄漏关面。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,829評論 3 312
  • 文/蒙蒙 一诚撵、第九天 我趴在偏房一處隱蔽的房頂上張望缭裆。 院中可真熱鬧,春花似錦寿烟、人聲如沸澈驼。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,715評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽缝其。三九已至,卻和暖如春徘六,著一層夾襖步出監(jiān)牢的瞬間内边,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,945評論 1 264
  • 我被黑心中介騙來泰國打工待锈, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留漠其,地道東北人。 一個月前我還...
    沈念sama閱讀 46,248評論 2 360
  • 正文 我出身青樓竿音,卻偏偏與公主長得像和屎,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子春瞬,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,440評論 2 348

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

  • 這是16年5月份編輯的一份比較雜亂適合自己觀看的學(xué)習(xí)記錄文檔柴信,今天18年5月份再次想寫文章,發(fā)現(xiàn)簡書還為我保存起的...
    Jenaral閱讀 2,737評論 2 9
  • 官方文檔 初始化 Initialization是為準(zhǔn)備使用類宽气,結(jié)構(gòu)體或者枚舉實例的一個過程随常。這個過程涉及了在實例里...
    hrscy閱讀 1,133評論 0 1
  • 初始化 (Initialization) 自從蘋果2014年發(fā)布Swift,到現(xiàn)在已經(jīng)兩年多了萄涯,而Swift也來到...
    Lebron_James閱讀 1,204評論 0 0
  • 初始化(Initialization) 初始化是類绪氛、結(jié)構(gòu)體、枚舉類型的準(zhǔn)備過程涝影。這個過程涉及到所有存儲屬性的初始化...
    泗哥閱讀 5,628評論 0 3
  • 繁星如水枣察,臺北的夜大多如此。阿婆搖著老舊的蒲扇袄琳。巷子里坐著三兩對情侶询件,倒不說話燃乍,只是微微偏頭看著對方。 小艾每天都...
    坤靈集閱讀 661評論 9 3