深入了解 iOS 的初始化

初始化

在 iOS 里面,无论是 Objective-C 还是 Swift,初始化都有一定的规则要求,只不过在 Objective-C 中会比较宽松,如果不按照规则也不会报错,但会存在隐患,而在 Swift 则需要严格按照规则要求代码才能编译通过,极大提高了代码的安全性。

由于在 Swift 里面,结构体、枚举的初始化有点特殊,为了方便和统一,所以本文以下内容只针对 "类"。

类的初始化有两种初始化器(初始化方法):指定初始化器(Designated Initializers )、便利初始化器(Convenience Initializers)

Designated Initializers

指定初始化器是类的主初始化器,类初始化的时候必须调用自身或者父类的指定初始化器。一个类可以有多个指定初始化器,作用是代表从不同的源进行初始化。一个类除非有多种不同的源进行初始化,否则不建议创建多个指定初始化器。在 iOS 里,视图控件类,如:UIViewUIViewController 就有两个指定初始化器,分别代表从代码初始化、从 Nib 初始化

Convenience Initializers

便利初始化器是类的次要初始化器,作用是使类在初始化时更方便设置相关的属性(成员变量)。既然便利初始化器是为了便利,那么一个类就可以有多个便利初始化器,这些便利初始化器里面最后都需要调用自身的指定初始化器

核心规则

iOS 的初始化最核心两条的规则:

  • 必须至少有一个指定初始化器,在指定初始化器里保证所有非可选类型属性都得到正确的初始化(有值)
  • 便利初始化器必须调用其他初始化器,使得最后肯定会调用指定初始化器

所有的其他规则都根据这两条规则而展开,只是 Objective-C 没有那么多安全检查,很多开发者写起来也比较随意,而 Swift 则有一堆的限制。

Objective-C

Objective-C 在初始化时,会自动给每个属性(成员变量)赋值为 0 或者 nil ,没有强制要求额外为每个属性(成员变量)赋值,方便的同时也缺少了代码的安全性。

Objective-C 中的指定初始化器会在后面被 NS_DESIGNATED_INITIALIZER 修饰,以下为 NSObjectUIView 的指定初始化器

// NSObject
@interface NSObject <NSObject> 

- (instancetype)init
#if NS_ENFORCE_NSOBJECT_DESIGNATED_INITIALIZER
    NS_DESIGNATED_INITIALIZER
#endif
    ;
@end
  
  
// UIView
@interface UIView : UIResponder

- (instancetype)initWithFrame:(CGRect)frame NS_DESIGNATED_INITIALIZER;
- (nullable instancetype)initWithCoder:(NSCoder *)coder NS_DESIGNATED_INITIALIZER;

@end

在 Objective-C 里面,几乎所有类都继承自 NSObject 。当自定义一个类的时候,要么直接继承自 NSObject ,要么继承自 UIView 或者其他类。

无论继承自什么类,都经常需要新的初始化方法,而这个新的初始化方法其实就是新的指定初始化器。如果存在一个新的指定初始化器,那么原来的指定初始化器就会自动退化成便利初始化器。为了遵循必须要调用指定初始化器的规则,就必须重写旧的定初始化器,在里面调用新的指定初始化器,这样就能确保所有属性(成员变量)被初始化

根据这条规则,可以从 NSObjectUIView 中看出,由于 UIView 拥有新的指定初始化器 -initWithFrame: ,导致父类 NSObject 的指定初始化器 -init 退化成便利初始化器。所以当调用 [[UIView alloc] init] 时,-init 里面必然调用了 -initWithFrame:

当存在一个新的指定初始化器的时候,推荐在方法名后面加上 NS_DESIGNATED_INITIALIZER ,主动告诉编译器有一个新的指定初始化器,这样就可以使用 Xcode 自带的 Analysis 功能进行分析,找出初始化过程中可能存在的漏洞

@interface MyView : UIView

@property (nonatomic, strong) NSString *name;

// 推荐加上NS_DESIGNATED_INITIALIZER
- (instancetype)initWithFrame:(CGRect)frame name:(NSString *)name NS_DESIGNATED_INITIALIZER;

@end


@implementation MyView

// 初始化时加入参数name,这个方法已经成为新的指定初始化器
- (instancetype)initWithFrame:(CGRect)frame name:(NSString *)name {
    if (self = [super initWithFrame:frame]) {
        self.name = name;
    }
    return self;
}

// 旧的指定初始化器就自动退化成便利初始化器,必须在里面调用新的指定初始化器
- (instancetype)initWithFrame:(CGRect)frame {
    return [self initWithFrame:frame name:@"Daniels"];
}

// 旧的指定初始化器就自动退化成便利初始化器,必须在里面调用新的指定初始化器
- (instancetype)initWithCoder:(NSCoder *)coder {
    // 这里的实现是伪代码,只是为了满足规则
    return [self initWithFrame:CGRectNull name:@"Daniels"];
}

@end

如果不想去重写旧的指定初始化器,但又不想存在漏洞和隐患,那么可以使用 NS_UNAVAILABLE 把旧的指定初始化器都废弃,外界就无法调用旧的指定初始化器

@interface MyView : UIView

@property (nonatomic, strong) NSString *name;



// 推荐加上NS_DESIGNATED_INITIALIZER
- (instancetype)initWithFrame:(CGRect)frame name:(NSString *)name NS_DESIGNATED_INITIALIZER;

// 废弃旧的指定初始化器
- (instancetype)init NS_UNAVAILABLE;

// 废弃旧的指定初始化器
- (instancetype)initWithFrame:(CGRect)frame NS_UNAVAILABLE;

// 废弃旧的指定初始化器
- (instancetype)initWithCoder:(NSCoder *)coder NS_UNAVAILABLE;

@end


@implementation MyView

// 初始化时加入参数name,这个方法已经成为新的指定初始化器
- (instancetype)initWithFrame:(CGRect)frame name:(NSString *)name {
    if (self = [super initWithFrame:frame]) {
        self.name = name;
    }
    
    return self;
}


@end

当然,一个新的类也可以不增加新的初始化方法,在 Objective-C 中,子类会直接继承父类所有的初始化方法

Swift

在 Swift 中,初始化器的规则严格且复杂,目的就是为了使代码更加安全,如果不符合规则,会直接报错,常常会让刚接手 Swift 或者一直对 iOS 的初始化没有深入理解的人很头疼。其实核心规则还是一样,只要理解了各个规则的含义和作用,写起来还是没有压力。

从 iOS 初始化的核心规则展开而来,Swift 多了一些规则:

  • 初始化的时候需要保证类的所有非可选类型属性都会有值,否则会报错。
  • 在没有给所有非可选类型属性赋值(初始化完成)之前,不能调用 self 相关的任何东西,例如:调用实例属性,调用实例方法。

不存在继承

处理这种情况十分简单,自己里面的 init 方法就是它的指定初始化器,而且可以随意创建多个它的指定初始化器。如果需要创建便利初始化器,则在方法名前面加上 convenience ,且在里面必须调用其他初始化器,使得最后肯定调用指定初始化器

class Person {

    var name: String

    var age: Int

    // 可以存在多个指定初始化器
    init(name: String, age: Int) {
        self.name = name;
        self.age = age;
    }

    // 可以存在多个指定初始化器
    init(age: Int) {
        self.name = "Daniels";
        self.age = age;
    }

    // 便利初始化器
    convenience init(name: String) {
        // 必须要调用自己的指定初始化器
        self.init(name: name, age: 18)
        // 必须在初始化完成后才能调用实例方法
        jump()
    }
  
    func jump() {

    }
}

存在继承

如果子类没有新的非可选类型属性,或者保证所有非可选类型属性都已经有默认值,则可以直接继承父类的指定初始化器和便利初始化器

class Student: Person {

    var score: Double = 100
  
}

如果子类有新的非可选类型属性,或者无法保证所有非可选类型属性都已经有默认值,则需要新创建一个指定初始化器,或者重写父类的指定初始化器

  • 新创建一个指定初始化器,会覆盖父类的指定初始化器,需要先给当前类所有非可选类型属性赋值,然后再调用父类的指定初始化器
  • 重写父类的指定初始化器,需要先给当前类所有非可选类型属性赋值,然后再调用父类的指定初始化器
  • 在保证子类有指定初始化器,才能创建便利初始化器,且在便利初始化器里面必须调用指定初始化器
class Student: Person {

    var score: Double
        
    // 新的指定初始化器,如果有新的指定初始化器,就不会继承父类的所有初始化器,除非重写
    init(name: String, age: Int, score: Double) {
        self.score = score
        super.init(name: name, age: age)
    }
  
    // 重写父类的指定初始化器,如果不重写,则子类不存在这个方法
    override init(name: String, age: Int) {
        score = 100
        super.init(name: name, age: age)
    }
  
  
    // 便利初始化器
    convenience init(name: String) {
        // 必须要调用自己的指定初始化器
        self.init(name: name, age: 10, score: 100)
    }
}

需要注意的是,如果子类重写父类所有指定初始化器,则会继承父类的便利初始化器。原因也是很简单,因为父类的便利初始化器,依赖于自己的指定初始化器

Failable Initializers

在 Swift 中可以定义一个可失败的初始化器(Failable Initializers),表示在某些情况下会创建实例失败。

只有在表示创建失败的时候才有返回值,并且返回值为 nil

子类可以把父类的可失败的初始化器重写为不可失败的初始化器,但不能把父类的不可失败的初始化器重写为可失败的初始化器

class Animal {
    
    let name: String
    // 可失败的初始化器,如果把 ? 换成 !,则为隐式的可失败的初始化器
    init?(name: String) {
        if name.isEmpty {
            return nil
        }
        self.name = name
    }
}

class Dog: Animal {

    override init(name: String) {
        if name.isEmpty {
            super.init(name: "旺财")!
        } else {
            super.init(name: name)!
        }
    }
}

Required Initializers

在 Swift 中,可以使用 required 修饰初始化器,来指定子类必须实现该初始化器。需要注意的是,如果子类可以直接继承父类的指定初始化器和便利初始化器,所以也就可以不用额外实现 required 修饰的初始化器

子类实现该初始化器时,也必须加上 required 修饰符,而不是 override

class MyView: UIView {

    var name: String


    init(frame: CGRect, name: String) {
        self.name = name;
        super.init(frame: frame)
    }

    // 必须实现此初始化器,但由于是可失败的初始化器,所以里面可以不做具体实现
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

总结

iOS 的初始化最核心两条的规则:

  • 必须至少有一个指定初始化器,在指定初始化器里保证所有非可选类型属性都得到正确的初始化(有值)
  • 便利初始化器必须调用其他初始化器,使得最后肯定会调用指定初始化器

展开而来的多条规则:

  • 无论在 Objective-C 还是 Swift 中,都可以有多个指定初始化器和多个便利初始化器。如果不是可以从多个不同的源初始化,最好只创建一个指定初始化器
  • 无论在 Objective-C 还是 Swift 中,都需要在便利初始化器中调用指定初始化器
  • 在 Objective-C 中,初始化的时候不需要保证所有属性(成员变量)都有值
  • 在 Objective-C 中,如果存在一个新的指定初始化器,那么原来的指定初始化器就会自动退化成便利初始化器。必须重写旧的定初始化器,在里面调用新的指定初始化器
  • 在 Swift 中,初始化的时候需要保证类(结构体、枚举)的所有非可选类型属性都会有值
  • 在 Swift 中,必须在初始化完成后才能调用实例属性,调用实例方法
  • 在 Swift 中,如果存在继承,并且子类有新的非可选类型属性,或者无法保证所有非可选类型属性都已经有默认值,那么就需要新创建一个指定初始化器,或者重写父类的指定初始化器,并且在里面调用父类的指定初始化器
  • 在 Swift 中,子类如果没有新创建一个指定初始化器,并且没有重写父类的指定初始化器,则会继承父类的指定初始化器和便利初始化器
  • 在 Swift 中,子类如果新创建一个指定初始化器,或者重写了父类的某个指定初始化器,那么就不会继承父类的指定初始化器和便利初始化器;但是如果重写了父类的所有指定初始化器,就会继承父类的便利初始化器
  • 在 Swift 中,子类可以把父类的指定初始化器重写成便利初始化器
  • 在 Swift 中,如果子类没有直接继承父类的指定初始化器和便利初始化器,则必须实现父类中 required 修饰的初始化器

参考资料

Initialization

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