iOS-Swift-多态实现原理、初始化器

一. 多态实现原理

多态就是父类指针指向子类对象。
关于多态:在编译的时候并不知道要调用的是父类还是子类的方法,运行的时候才会根据实际类型调用子类的方法。

对于结构体来说,因为结构体没有继承,编译的时候就能知道调用哪个方法。
但是对于类,只有在运行的时候才能知道实际调用哪个方法。

多态实现原理:
OC:通过Runtime将一些方法放到方法列表里面
C++:虚表(虚函数表)
Swift中的多态:和虚表很像

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

我们都知道是上面的打印结果,但是原理是什么呢?如下图,下面的结论都是MJ老师通过窥探汇编得到的:

多态原理

我们都知道Dog对象的前8个字节是指向类型相关,如上图,类型相关的那块区域里面,前面一块区域存放Dog的类型信息相关,后面一块区域存放方法的地址,比如Dog重写了speak和eat,所以这两个方法在最前面,没有重写sleep所以再后面是Animal.sleep,最后是Dog.run。

当调用anim.speak(),首先先取出Dog对象的前8个字节,根据这8个字节指针找到指针指向的内存,找到内存地址之后加上一个固定偏移量,就会获得Dog.speak方法的内存地址,找到方法之后调用。

同理,Dog eat也是先取出前8字节,找到这8字节的内存地址,然后再加上一个固定偏移量,找到Dog.eat方法,找到方法之后调用。

如果Dog没有重写Animal的方法,那么类型信息那块区域存储的方法就是,如下图:

没重写

如果是两个dog对象,他们的类型信息肯定也是一样的,如下图:

两个dog对象.png

那么Dog的类型信息会放在代码段、数据段、堆、栈中的哪一段?
我们可以猜测一下,因为类型信息要一直在内存中,首先排除栈,代码段一般放编译之后的代码也排除,堆空间一般放alloc,malloc等动态分配的东西,也排除,所以Dog的类型信息只能存放数据段。
MJ老师通过打断点查看汇编,比较地址值也得出结论,的确是在数据段。

总结:程序在编译的时候就将函数地址放到了类型信息那块内存区域了,当程序运行的时候再到这块内存区域找函数地址,找到之后就调用。

二. 初始化器

1. 初始化器

枚举:枚举可以使⽤rawValue来给枚举赋值,没有自动生成的初始化器。
结构体:所有的结构体都有编译器⾃动⽣成的初始化器(也许不⽌⼀个),⽤于初始化所有成员,但是如果你⾃定义了初始化器,编译器就不会帮你了。
类:编译器没有为类⾃动⽣成可以传⼊成员值的初始化器(想让我们⾃⼰写),但是如果属性都有默认值,也会帮我们创建⼀个⽆参初始化器。

当然,枚举、结构体、类都可以自定义初始化器。

类有两种初始化器:指定初始化器(designated initializer)、便捷初始化器(convenience initializer)

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

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

每个类至少有一个指定初始化器,指定初始化器是类的主要初始化器
默认初始化器总是类的指定初始化器
类偏向于少量指定初始化器,一个类通常只有一个指定初始化器

初始化器的相互调用规则:
1. 指定初始化器必须从它的直系父类调用指定初始化器
2. 便捷初始化器必须从相同的类里调用另一个初始化器,最终必须调用一个指定初始化器

(其实就是,便捷初始化器是横向调用,指定初始化器是竖向调用)

便携指向指定,指定指向⽗类.png

这一套规则保证了使用任意初始化器,都可以完整地初始化实例。

2. 两段式初始化、安全检查

Swift在编码安全方面是煞费苦心,为了保证初始化过程的安全,设定了两段式初始化、 安全检查

① 两段式初始化

第1阶段:初始化所有存储属性
① 外层调用指定\便捷初始化器
② 分配内存给实例,但未初始化
③ 指定初始化器确保当前类定义的存储属性都初始化
④ 指定初始化器调用父类的初始化器,不断向上调用,形成初始化器链

第2阶段:设置新的存储属性值
⑤ 从顶部初始化器往下,链中的每一个指定初始化器都有机会进一步定制实例
⑥ 初始化器现在能够使用self访问、修改它的属性,调用它的实例方法等等
⑦ 最终,链中任何便捷初始化器都有机会定制实例以及使用self

② 安全检查

  1. 指定初始化器必须保证其所在类定义的所有存储属性都要初始化完成,再调用父类初始化器
  2. 指定初始化器必须先调用父类初始化器,然后才能为继承的属性设置新值
  3. 便捷初始化器必须先调用同类中的其它初始化器,然后再为任意属性设置新值
  4. 初始化器在第1阶段初始化完成之前,不能调用任何实例方法、不能读取任何实例属性的值,也不能引用self
  5. 直到第1阶段结束,实例才算完全合法

光看上面两段文字可能比较难理解,结合下面代码,从第1步到第12步,看完你就明白了:

class BasePerson {
    var IDnum: Int
    init(IDnum: Int) {
        
        //这里打印:print(self.IDnum)和方法调用self.test(),都会报错:Variable 'self.IDnum' used before being initialized
        
        self.IDnum = IDnum //8.父类的指定初始化器初始化自己的属性
        
        //9.至此第一阶段结束,才可以使用self
        
        print(self.IDnum) //这里打印不会报错
        self.test() //方法调用也不会报错
    }
    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.再调用父类的指定初始化器
        
        self.IDnum = 3412211992 //10.父类的初始化器调用完,接着进一步定制实例
        
    }
    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.指定初始化器确保当前类定义的存储属性都初始化
        
        super.init(age: age, name: "") //4.指定初始化器调用父类的初始化器,不断向上调用,形成初始化器链
        
        self.age = 10 //11.父类的初始化器调用完,接着进一步定制实例
    }
    
    convenience init() {
        self.init(age: 0, score:0) //2.便捷初始化器调用指定初始化器
        
        //12.至此第二阶段结束,所有初始化完成
    }
}

var stu = Student() //1.外层调用便捷初始化器

补充:一般我们自定义UIView都会重写它的init(frame: CGRect)方法,如下:

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

这时候你可能会疑惑为什么这里的super.init在最上面?
其实不管是重写初始化器还是自定义初始化器,都是定义一个初始化器,重写初始化器一般没有新的参数(作为属性),所以super.init在最上面,如果是自定义一个新的初始化器,一般都有新的参数(作为属性),所以就是上面的两段式初始化。

3. 重写、自动继承、required、属性观察器

① 重写

  1. 当重写父类的指定初始化器时,必须加上override(即使子类的实现是便捷初始化器)。

代码如下:

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。因为父类的便捷初始化器永远不会通过子类直接调用,因此,严格来说,子类无法重写父类的便捷初始化器。

代码如下:

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)
//    }

//上面不叫重写,我们假设它是重写父类的便捷初始化器init(),既然是重写,那么在子类的方法中就可以调用super.init()
//很显然,对于指定初始化器只能调用父类的指定初始化器,不可以调用super.init()。
//对于便捷初始化器只能调用子类的指定初始化器,更不可能调用super.init(),所以上面不叫重写。
}

② 自动继承

  1. 如果子类没有自定义任何指定初始化器,它会自动继承父类所有的指定初始化器(一旦你自定义了一个指定初始化器,那么父类的指定初始化器统统不给你用了)
  2. 如果子类提供了父类所有指定初始化器的实现(要么通过方式1继承,要么重写),那么子类自动继承所有的父类便捷初始化器
  3. 子类以便捷初始化器的形式重写父类的指定初始化器,也可以作为满足规则2的一部分
  4. 就算子类添加了更多的便捷初始化器,这些规则仍然适用

补充:我们一般不自定义指定初始化器,这样才能自动继承父类的指定初始化器,我们一般自定义便捷初始化器,如下:

//便捷初始化器必须先调用同类中的其它初始化器(这里是继承父类的),然后再为任意属性设置新值
convenience init(titles: [String] = [], vcs: [UIViewController] = [], pageStyle: XUPageStyle = .noneStyle) {
    self.init()
    self.titles = titles
    self.vcs = vcs
    self.pageStyle = pageStyle
}

③ required

  1. 用required修饰指定初始化器(修饰便捷初始化器没啥用),表明其所有子类都必须实现该初始化器(通过继承或者重写实现)
  2. 如果子类重写了required初始化器,也必须加上required,不用加override
class Person {
    required init() { }
    init(age: Int) { }
}
class Student : Person {
    required init() {
        super.init()
    }
}

④ 属性观察器

父类的属性在它自己的初始化器中赋值不会触发属性观察器,但在子类的初始化器中赋值会触发属性观察器

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本来就是从父类来的,你修改了父类的属性,肯定要触发父类的属性观察器。

4. 可失败初始化器、反初始化器

① 可失败初始化器

枚举、结构体、类都可以使用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就创建失败,所以是可失败的
  1. 不允许同时定义参数标签、参数个数、参数类型相同的可失败初始化器和非可失败初始化器(因为不知道调用哪个了)
  2. 可以用init!定义隐式解包的可失败初始化器
  3. 可失败初始化器可以调用非可失败初始化器,非可失败初始化器调用可失败初始化器需要进行解包
  4. 如果初始化器调用一个可失败初始化器导致初始化失败,那么整个初始化过程都失败,并且之后的代码都停止执行
  5. 可以用一个非可失败初始化器重写一个可失败初始化器,但反过来是不行的。

② 反初始化器(deinit)

deinit叫做反初始化器,类似于C++的析构函数、OC中的dealloc方法
当类的实例对象被释放内存时,就会调用实例对象的deinit方法

class Person {
    deinit {
        print("Person deinit")
    }
}
  1. deinit不接受任何参数,不能写小括号
  2. 父类的deinit能被子类继承
  3. 子类的deinit实现执行完毕后会调用父类的deinit

如下,子类没有实现deinit,通过打印可知,子类继承了父类的deinit

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

class Student: Person {
}

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

test() //打印:Person deinit
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容

  • 这是16年5月份编辑的一份比较杂乱适合自己观看的学习记录文档,今天18年5月份再次想写文章,发现简书还为我保存起的...
    Jenaral阅读 2,727评论 2 9
  • 官方文档 初始化 Initialization是为准备使用类,结构体或者枚举实例的一个过程。这个过程涉及了在实例里...
    hrscy阅读 1,132评论 0 1
  • 初始化 (Initialization) 自从苹果2014年发布Swift,到现在已经两年多了,而Swift也来到...
    Lebron_James阅读 1,195评论 0 0
  • 初始化(Initialization) 初始化是类、结构体、枚举类型的准备过程。这个过程涉及到所有存储属性的初始化...
    泗哥阅读 5,621评论 0 3
  • 繁星如水,台北的夜大多如此。阿婆摇着老旧的蒲扇。巷子里坐着三两对情侣,倒不说话,只是微微偏头看着对方。 小艾每天都...
    坤灵集阅读 658评论 9 3