从 Swift 初始化说起

原文地址:http://huizhao.win/2016/11/13/swift-init/

从 Objective-C 转到 Swift 后,可能首先就会发觉 Swift 的初始化方法变了,曾经 Objective-C 里面随意信手拈来的初始化代码可能不好使了,一起来学习一下吧。


初始化方法调用顺序

分别创建一个 Swift 类和 Objective-C 类,然后使用 Xcode 模板新建一个初始化方法后,我们可以得到如下代码:

@implementation BlogInitOC

- (instancetype)init
{
    self = [super init];
    if (self) {
      
    }
    return self;
}

@end
class BlogInit: NSObject {

    override init() { // 需要手动添加 override 关键字
        
    }

}

对比发现,Swift 的初始化代码需要加上 override 关键字,方法内部没有调用 super 的 init 方法,并且没有 return 语句。虽然如此,但此时编译是可以通过的。

接着,分别给这两个类加上一个属性变量 param:

@interface BlogInitOC ()
@property (nonatomic, strong) NSString *param;
@end

@implementation BlogInitOC

- (instancetype)init
{
    self = [super init];
    if (self) {
        
    }
    return self;
}

@end
class BlogInit: NSObject {
    
    let param: String
        
    override init() {
        
    }

}

情况变了,Objective-C 类一切正常,而 Swift 类提示了一条错误 Property 'self.param' not initialized at implicitly generated super.init call,意思很明确,param 参数没有在隐式生成 super.init 调用之前完成初始化。原来 Swift 中并不是不调用 super.init,而是为了方便开发者由编译器完成了这一步,但是要求开发者在初始化方法中调用 super.init 之前完成成员变量的初始化。

修改后的代码如下:

class BlogInit: NSObject {
    
    let param: String
      
    override init() {
    self.param = "zhaohui"
    // super.init() // 可不写,编译器隐式生成 
    }

}

对于需要修改父类中成员变量值的情况,我们需要在调用 super.init 之后再进行修改,代码如下:

class Cat {
    var name: String
    
    init() {
        name = "cat"
    }
}

class Tiger: Cat {
    let power: Int
    
    override init() {
        power = 10
        super.init()
        name = "tiger"
    }
}

因此 Swift 中类的初始化顺序可以总结如下:

  1. 初始化自己的成员变量,必须
  2. 调用父类初始化方法,如无需第三步,则这一步也可省略
  3. 修改父类成员变量,可选

这里补充说明两点:

  1. 使用 let 声明的常量是可以在初始化方法中进行赋值的,这是编译器所允许的,因为 Swift 中的 init 方法只会被调用一次,这与 Objective-C 不同;
  2. 即使成员变量是可选类型,如:let param: String?,仍然是需要进行初始化的。

关键词

看完上面这部分,好像 Swift 初始化也没什么,不过是语法上一些变化,不过当我们按照曾经 Objective-C 的习惯添加类间继承关系、自定义初始化方法等,问题又来了。

先来看下面这个例子:

class CustomView: UIView {
    let param: Int
        
    override init() { // error 1
        self.param = 1
        super.init() // error 2
    }
} // error 3

好奇怪,我们只是将父类从 NSObject 修改为 UIView,竟然收到3条错误:

  1. Initializer does not override a designated initializer from its superclass
  2. Must call a designated initializer of the superclass 'UIView'
  3. 'required' initializer 'init(coder:)' must be provided by subclass of 'UIView'

稍等,再看一个例子:

class CustomView: UIView {
    convenience init(param: Int, frame: CGRect) {
        super.init(frame: frame) // error
    }
    
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

这时,我们又得到了一条新的错误:

Convenience initializer for 'CustomView' must delegate (with 'self.init') rather than chaining to a superclass initializer (with 'super.init')

前面两个例子中,我们看到了关键字 designatedconveniencerequired,理解了这几个关键字也就能帮助我们理解整个初始化过程了。

designated

看到 designated,我们很容易联想到 Objective-C 中 NS_DESIGNATED_INITIALIZER,它们的含义比较接近,都是用来设置指定初始化器,关于 Objective-C 中的用法,请参阅《正确编写Designated Initializer的几个原则》,下面我们主要讨论 Swift 中的 designated

在 Apple 的官方文档中讲到,Swift 定义了两种类初始化器类型,用来保证所有成员属性能够获得一个初始化值,即 designated initializersconvenience initializers。对于 designated initializers 的定义如下:

Designated initializers are the primary initializers for a class. A designated initializer fully initializes all properties introduced by that class and calls an appropriate superclass initializer to continue the initialization process up the superclass chain.

加粗部分是几处关键的描述:

  1. primary initializers:designated initializers 是一个类的主初始化器,理论上来说是一个类初始化的必经之路(注:不同的初始化路径可能调用不同的 designated initializers);
  2. fully initializes all properties:这点很明确,必须在 designated initializers 中完成所有成员属性的初始化;
  3. calls an appropriate superclass initializer:需要调用合适的父类初始化器完成初始化,不能随意调用。

下面我们结合前面的 Sample-1 进行解释:

class CustomView: UIView {
    let param: Int
        
    override init() { // error 1
        self.param = 1
        super.init() // error 2
    }
} // error 3

在 Swift 中,designated initializers 的写法和一般的初始化方法无异,Sample-1 中,我们试图去 override init,可以理解为我们就是在 override 一个 designated initializers,然后我们收到了错误 Initializer does not override a designated initializer from its superclass,可见我们并没有找到合适的 designated initializers,我们进入父类 UIView,可以看到下面两个初始化方法:

    public init(frame: CGRect)
    public init?(coder aDecoder: NSCoder)

原来,这两个类才是父类的 designated initializers,那我们改改试试:

class CustomView: UIView {
    let param: Int
        
    override init(frame: CGRect) { // error 1 fixed
        self.param = 1
        super.init() // error 2
    }
} // error 3

果然,error 1 没了,由此也可以看出,我们去 override 一个不是 designated initializers 的初始化器不满足定义中所说的 primary initializers,这就可能导致这个初始化器不被执行,成员变量没有初始化,这样创建的“半成品”实例可能存在一些不安全的情况。

第二条 fully initializes all properties,这点我们并没有犯错,因为我们已经初始化了 CustomView 类中引入的 param 变量。

第三条 calls an appropriate superclass initializer 很明显就对应了 error 2,我们 override init(frame: CGRect),那我们就必须调用对应的父类初始化方法,修改如下:

class CustomView: UIView {
    let param: Int
        
    override init(frame: CGRect) { // error 1 fixed
        self.param = 1
        super.init(frame: frame) // error 2 fixed
    }
} // error 3

再来看 error 3:'required' initializer 'init(coder:)' must be provided by subclass of 'UIView',这条错误提示我们 init(coder:) 是一个 'required' initializer,子类必须提供,那什么是 required 呢?

required

对于 required,官方给出了一句说明:

Write the required modifier before the definition of a class initializer to indicate that every subclass of the class must implement that initializer.

意思很明白,通过添加 required 关键字强制子类对某个初始化方法进行重写。前面的 error 3 中,init(coder:) 正好对应了父类 UIView 中的第二个初始化方法,所以想要修复这个错误,就需要重写 init(coder:)

其实,在 Xcode 中,双击这个错误就会帮我们插入这个方法,修复后代码如下:

class CustomView: UIView {
    let param: Int
        
    override init(frame: CGRect) { // error 1 fixed
        self.param = 1
        super.init(frame: frame) // error 2 fixed
    }
    
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
} // error 3 fixed

这样,我们修复了 error 3,不过插入的这个方法很奇怪,方法体里直接写 fatalError("init(coder:) has not been implemented"),那岂不是走到这里就 fatal 了,这不是坑我们吗?!

前文中我们讲了,designated initializers 是一个类的主初始化器,理论上来说是一个类初始化的必经之路(注:不同的初始化路径可能调用不同的 designated initializers),其实,这个 init(coder:)init(frame: frame) 就是不同的初始化路径,当我们使用 xib 方式初始化一个 view 时,就会走到 init(coder:)。此时,如果我们没有真正实现这个方法,就会出现 fatal crash,如下图所示:

init(coder:) fatal

所以到目前为止,我们仍然没有提供一套完整的、安全的初始化方法,需要继续补全 init(coder:) 方法,以覆盖全部可能的初始化流程:

class CustomView: UIView {
    let param: Int
        
    override init(frame: CGRect) {
        self.param = 1
        super.init(frame: frame)
    }
    
    required init?(coder aDecoder: NSCoder) {
        self.param = 1
        super.init(coder: aDecoder)
    }
}

这样,我们就完成了一个 UIView 子类的初始化代码。

convenience

在 Apple 的官方文档对 convenience initializers 的定义如下:

Convenience initializers are secondary, supporting initializers for a class. You can define a convenience initializer to call a designated initializer from the same class as the convenience initializer with some of the designated initializer’s parameters set to default values. You can also define a convenience initializer to create an instance of that class for a specific use case or input value type.

convenience initializers 是对类初始化方法的补充,用于为类提供一些快捷的初始化方法,可以不创建这类方法,但如果创建了,就需要遵循原则:call a designated initializer from the same class,那么回到前文的 Sample-2:

class CustomView: UIView {
    convenience init(param: Int, frame: CGRect) {
        super.init(frame: frame) // error
    }
    
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

这里我们得到的错误,正好匹配了上面的原则:

Convenience initializer for 'CustomView' must delegate (with 'self.init') rather than chaining to a superclass initializer (with 'super.init')

看来我们需要调用该类自己的 designated initializer,那么我们应该 override init(frame: CGRect),然后修改 convenience init(param: Int) 中的 super 为 self:

class CustomView: UIView {
    convenience init(param: Int, frame: CGRect) {
        self.init(frame: frame) // error fixed
    }
        
    override init(frame: CGRect) {
        super.init(frame: frame)
    }
        
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

好啦,这下没错了!接着,我要用一个成员变量把 param 的值存起来:

class CustomView: UIView {
    var param: Int
    
    convenience init(param: Int, frame: CGRect) {
        self.param = param // error
        self.init(frame: frame)
    }
        
    override init(frame: CGRect) {
        super.init(frame: frame) // error
    }
        
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

又出来两个错误:

  1. Use of 'self' in property access 'param' before self.init initializes self
  2. Property 'self.param' not initialized at super.init call

第二个错误我们清楚,是需要在调用 super.init 之前初始化本类成员属性。第一个错误其实,这是 Swift 编译器提供的安全检查,文档原文如下:

A convenience initializer must delegate to another initializer before assigning a value to any property (including properties defined by the same class). If it doesn’t, the new value the convenience initializer assigns will be overwritten by its own class’s designated initializer.

原来 Swift 防止 convenience initializers 中赋值之后又被该类自己的 designated initializer 覆盖而做了检查,因此,正确的方式应该是调用该类的其他初始化方法之后再修改属性值,最终修改如下:

class CustomView: UIView {
    var param: Int
    
    convenience init(param: Int, frame: CGRect) {
        self.init(frame: frame)
        self.param = param // error fixed
    }
        
    override init(frame: CGRect) {
        self.param = 0 // error fixed
        super.init(frame: frame)
    }
        
    required init?(coder aDecoder: NSCoder) {
        self.param = 0
        super.init(coder: aDecoder)
    }
}

小结

对于 Swift 中的初始化方法,总结如下:

  1. 子类中初始化方法必须覆盖全部初始化路径,以保证对象完全初始化;
  2. 子类中 designated initializer 必须调用父类中对应的 designated initializer,以保证父类也能完成初始化;
  3. 子类中如果重写父类中 convenience initializer 所需要的全部 init 方法,就可以在子类中使用父类的 convenience initializer 了;
  4. 子类如果没有定义任何 designated initializer,则默认继承所有父类的 designated initializerconvenience initializer
  5. 子类中必须实现的 designated initializer,可以通过添加 required 关键字强制子类重写其实现,以保证依赖该方法的 convenience initializer 始终可以使用;
  6. convenience initializer 必须调用自身类中的其他初始化方法,并在最终必须调用一个 designated initializer
  7. 在构造器完成初始化之前, 不能调用任何实例方法,或读取任何实例属性的值,self 本身也不能被引用。

看上去 Swift 中对初始化过程添加了很多“规矩”,开发上繁琐了不少,但是却更有利于帮助我们开发更规范、更安全的初始化方法,从而减少一些潜在的问题,所以掌握这些“规矩”是非常有用且值得的。


可失败初始化器

可失败初始化器(Failable Initializers),即可以返回 nil 的初始化方法,这在 Objective-C 的初始化过程中本来就支持,但这种支持反而导致逻辑上的模糊,什么时候返回 nil 其实我们并不明确,而 Swift 对这些情况进行了明确。

官方文档对 Failable Initializers 的定义如下:

A failable initializer creates an optional value of the type it initializes. You write return nil within a failable initializer to indicate a point at which initialization failure can be triggered.

很容易理解,就是将初始化返回值变成 optional value(在 init 后面加上 ?),并在不满足初始化条件的地方 return nil,这样,我们通过调用处判断是否有值即可知道是否初始化成功。

我们以官方例子进行解释:

class Product {
    let name: String
    init?(name: String) {
        if name.isEmpty { return nil }
        self.name = name
    }
}
 
class CartItem: Product {
    let quantity: Int
    init?(name: String, quantity: Int) {
        if quantity < 1 { return nil }
        self.quantity = quantity
        super.init(name: name)
    }
}

CartItem 类的初始化方法先对传入参数 quantity 的值进行判断,小于 1 则为无效参数,然后 return nil(初始化失败),大于或等于 1 则继续调用父类 Product 的初始化方法,再次判断传入参数 name,为空则 return nil(初始化失败),否则继续初始化。

这样,我们通过下面几种不同参数进行初始化,即可得到不同的初始化结果:

if let twoSocks = CartItem(name: "sock", quantity: 2) {
    print("Item: \(twoSocks.name), quantity: \(twoSocks.quantity)")
}
// Prints "Item: sock, quantity: 2"

if let zeroShirts = CartItem(name: "shirt", quantity: 0) {
    print("Item: \(zeroShirts.name), quantity: \(zeroShirts.quantity)")
} else {
    print("Unable to initialize zero shirts")
}
// Prints "Unable to initialize zero shirts"

if let oneUnnamed = CartItem(name: "", quantity: 1) {
    print("Item: \(oneUnnamed.name), quantity: \(oneUnnamed.quantity)")
} else {
    print("Unable to initialize one unnamed product")
}
// Prints "Unable to initialize one unnamed product"

总的来说,可失败初始化器的设定,是在保证安全性的基础上提供了逻辑上更清晰的初始化方式。Failable Initializers 所有的结果都将是 T? 类型,通过 Optional Binding 方式,我们就能知道初始化是否成功,并安全地使用它们了。

注:本文所有描述均针对类类型初始化,对于结构体或枚举类型基本类似,还有一些其他特性大家可以参考官方文档进行学习。


参考资料

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

推荐阅读更多精彩内容