特別備注
本系列文章總結(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è)方法。
- 多態(tài)實(shí)現(xiàn)原理:
OC:通過Runtime將一些方法放到方法列表里面 - C++:虛表(虛函數(shù)表)
- Swift中的多態(tài):和虛表很像
先看下面這段程序的運(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)體方法的匯編:
struct結(jié)構(gòu)體,不存在繼承广鳍,方法在編譯的時(shí)候就和類型綁定了荆几,知道函數(shù)編譯后的地址,直接使用call調(diào)用赊时。
我們把struct
改成class
,再來看看他的的匯編:
可以看到,方法地址不是固定的,而是通過一定計(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)行步驟
運(yùn)行到callq *0x50(%rcx)
可以看到rcx讀取的值為0x000000010000c6b8 type metadata for TestSwift.Dog
為Dog對(duì)象的類型信息metadata
如下圖,通過窺探匯編得到的:
上圖就是子類在調(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ǔ)的方法就是搜立,如下圖:
如果是兩個(gè)dog對(duì)象,他們的類型信息肯定也是一樣的槐秧,如下圖:
那么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)用疑苫。
二. 初始化器
初始化器
枚舉:枚舉可以使?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()
從匯編我們可以看到讲竿,調(diào)用了Size的init()方法。
自定義初始化器
一旦我們自定義了初始化器,編譯器就不會(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è)指定初始化器
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)用
直系父類的指定初始化器
那便捷初始化器可不可以調(diào)用便捷初始化器呢?
從上圖可以看到,便捷初始化器是可以調(diào)用便捷初始化器的,但是必須遵從一條規(guī)則:便捷初始化器最終必須要調(diào)用指定初始化器
總結(jié):
1. 如果是指定初始化器,必須調(diào)用直系父類的指定初始化器;
2. 如果是便捷初始化器,必須調(diào)用自己的指定初始化器;
這一套規(guī)則保證了使用任意初始化器题禀,都可以完整地初始化實(shí)例。
2. 兩段式初始化膀捷、安全檢查
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)用便捷初始化器
- 安全檢查
- 指定初始化器必須保證其所在類定義的所有存儲(chǔ)屬性都要初始化完成覆劈,再調(diào)用父類初始化器
- 指定初始化器必須先調(diào)用父類初始化器保礼,然后才能為繼承的屬性設(shè)置新值
- 便捷初始化器必須先調(diào)用同類中的其它初始化器,然后再為任意屬性設(shè)置新值
- 初始化器在第1階段初始化完成之前责语,不能調(diào)用任何實(shí)例方法炮障、不能讀取任何實(shí)例屬性的值,也不能引用self
- 直到第1階段結(jié)束鹦筹,實(shí)例才算完全合法
初始化器的重寫
- 當(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)
}
}
- 重寫為便捷初始化器
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)
}
}
- 如果子類重寫了一個(gè)和父類便捷初始化器一模一樣的初始化器,不用加
override
,嚴(yán)格來講,這并不是重寫.
因?yàn)?strong>便捷初始化器是橫向調(diào)用的,它只能在類本身調(diào)用,子類是不能調(diào)用父類的便捷初始化器的.而重寫可以通過super
調(diào)用父類的方法,但是卻不能通過super
調(diào)用父類的便捷初始化器,這就沖突了.
初始化器的繼承
- 如果子類沒有自定義任何指定初始化器,它將繼承父類所有指定初始化器
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 {
}
- 如果子類提供了父類所有指定初始化器的實(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)
}
}
特別備注
本系列文章總結(jié)自MJ老師在騰訊課堂Swift編程從入門到精通-MJ大神精選铝阐,相關(guān)圖片素材均取自課程中的課件。