开发过程中,写 Initializer 应该是日常。会写的可以直接不看。如果没有好好了解过如何写 Initializer,每次都凭着感觉写而且还没有任何问题,说明其中的「玄机」你已经掌握了,可以大概扫一扫。但是每次写 Initializer 都要查 Google,那这篇文章就是给你(我🌚)准备的

Initializer 的种类

一个类中可能会有多个 Initializer,但不管有多少,都只会是下面两种类型中的一种。

Designated Initializer (指定初始化方法)

是类中的一等 Initializer,会把类中所有相关的属性都初始化。需要调用父类的 Designated Initializer。

Convenience Initializer (便捷初始化方法)

是类中的二等 Initializer,会调用类中的 Designated Initializer 或 Convenience Initializer,把其它没有传进来的属性初始化为默认值。主要是给调用者以方便。被其它 Convenience Initializer 调用的 Convenience Initializer,必须调用同类中的 Designated Initializer。

那么如何区分类中的初始化方法是 designated 的还是 convenience 的呢?有几个方法,

  • 通常来说,Designated Initializer 的参数要比 Convenience Initializer 多,因为指定初始化方法希望把这个类中的相关属性都初始化掉,而便捷初始化方法只会初始化掉一部分,其余不太要紧的属性则会提供默认值。
  • 看这个方法的声明里有没有使用 NS_DESIGNATED_INITIALIZER 标记。

结合一个 UIViewController 子类,看看 Initializer 到底该怎么写。

Example

不考虑使用 StoryBoard 的情况

1
2
3
4
5
6
@interface ProfileViewController : UIViewController
@property (nonatomic, readonly, copy) NSString *userID;
@property (nonatomic, readonly, copy) NSString *gender; // "0" or "1"
@end

在初始化的时候,userID 是必须要被初始化的,而 gender 在不知道的情况下可以被初始化为默认值,后续可以通过网络请求更新这个值。那么这个类的初始化方法大概应该长成这个样子,

1
2
3
4
5
6
7
+ (instancetype)new NS_UNAVAILABLE; // (1)
- (instancetype)init NS_UNAVAILABLE; // (2)
- (instancetype)initWithCoder:(NSCoder *)aDecoder NS_UNAVAILABLE; // (3)
- (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil NS_UNAVAILABLE; // (4)
- (instancetype)initWithUserID:(NSString *)userID gender:(NSUInteger)gender NS_DESIGNATED_INITIALIZER; // (5)
- (instancetype)initWithUserID:(NSString *)userID; // (6)

从上往下看,(1) ~ (4) 都是从父类中继承来的初始化方法,虽然我们的类能够使用这些方法初始化,但是我们不希望调用者使用这些方法,因为这会导致 userID 没有被初始化,逻辑就会不正确了,作为一个有节操的 programmer,把这些方法标记为 NS_UNAVAILABLE,告诉调用者,不要使用这些方法初始化,Xcode 也会有调用这些方法的地方抛出错误。

(5) 会初始化掉所有相关的属性,所以它是作为 Designated Initializer 的存在,同样作为一个有节操的 programmer,我们把它标记为 NS_DESIGNATED_INITIALIZER,这个标记会做以下几件事情,

  • 在实现这个指定初始化方法时,如果没有调用父类的 Designated Initializer,Xcode 会抛出警告。
  • Xcode 会把其它没有这个标记的初始化方法自动认为是 Convenience Initializer,包括从父类继承来的 Designated Initializer,并在实现的时候检查函数内有没有调用同类中的其它初始化方法 (Convenience 和 Designated 都可以)。

所以这个标记是非常有用的,如果你在适当的初始化方法前面加了这个标记,而且实现类中的所有初始化方法后还没有警告,那么出错几乎是不可能的。所以在实现新类初始化方法的时候都应该添加这个标记,让编译器提醒你到底应该做什么事情,减少犯错误的可能。

(6) 是一个 Convenience Initializer。

看到这里,实现上面类中的两个初始化方法应该很容易了吧,

1
2
3
4
5
6
7
8
9
10
11
- (instancetype)initWithUserID:(NSString *)userID {
return [self initWithUserID:userID gender:@"1"];
}
- (instancetype)initWithUserID:(NSString *)userID gender:(NSString *)gender {
if (self = [super initWithNibName:@"" bundle:nil]) {
_userID = userID;
_gender = gender;
}
return self;
}

一句话总结,子类的指定初始化方法必须调父类的指定初始化方法;子类的便捷初始化方法必须调用同类的其它初始化方法。

Swift

Swift 跟 Objective-C 不太一样,不会默认要求你重写父类的指定初始化方法。如果覆写了父类中的指定初始化方法,一定要加上 override 关键字。

加了 convience 关键字的就是 Convenience Initializer,规则与 Objective-C 的相同。

对于初始化方法前面没有 convience 关键字的,编译器都会把它当作 Designated Initializer,跟 Objective-C 初始化方法前面加 NS_DESIGNATED_INITIALIZER 是一样的。在实现这些方法时,可以参考上面的规则。

对于可能失败的初始化方法,需要把 init 关键字换成 init?,调用此类初始化方法时,返回值也会从确认有值,变成一个 optional。

所以一个 Swift 的 UIViewController 子类大概需要这样写,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class ProfileViewController: UIViewController {
let userID: String
let gender: String
init(userID: String, gender: String) {
self.userID = userID
self.gender = gender
super.init(nibName: nil, bundle: nil)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}

感谢 @四娘 提醒,所以重写 init(nibName, bundle) 并不必要

Q&A

Q: 为啥写个初始化方法还有这么多讲究?
A: 保证调用初始化方法后,由这个类引入的 property 都正确地被初始化,从而确保逻辑正确。

Q: Convenience Initializer 到底有啥用?
A: 就是给调用者图个方便嘛。比如 Texture 中的 ASDisplayNode 就提供了 4 个 Convenience Initializer,不需要初始化之后再 node.xxxBlock = ^() {}; ,而是把常用的 block property 在初始化的时候就给你了,就是为了方便。

1
2
3
4
5
- (instancetype)init NS_DESIGNATED_INITIALIZER;
- (instancetype)initWithViewBlock:(ASDisplayNodeViewBlock)viewBlock;
- (instancetype)initWithViewBlock:(ASDisplayNodeViewBlock)viewBlock didLoadBlock:(nullable ASDisplayNodeDidLoadBlock)didLoadBlock;
- (instancetype)initWithLayerBlock:(ASDisplayNodeLayerBlock)layerBlock;
- (instancetype)initWithLayerBlock:(ASDisplayNodeLayerBlock)layerBlock didLoadBlock:(nullable ASDisplayNodeDidLoadBlock)didLoadBlock;

Q: 父类中的 Designated Initializer 在子类中还是 Designated Initializer 吗?
A: 父类中的 Designated Initializer 在子类中不一定必须是 Designated Initializer。如果在头文件中没对方法进行任何标记,那么父类中的 Designated Initializer 在子类中依旧是 Designated Initializer,如果在子类没有覆写这个方法,编译器会给出警告;但如果在子类中对父类的 Designated Initializer 标记为 NS_UNAVAILABLE,那么父类中的这个方法在子类中不再是 Designated Initializer,即使不覆写,编译器也不会有任何警告。

Q: Objective-C 中的初始化方法中,引用 propery 的时候,到底该使用下划线的方式还是 self. 的方式?
A: Prefer _userID to self.userID

Q: 为何在初始化方法中, Swift 类的变量需要在调用父类的初始化方法之前初始化?
A:
根据文档,

Class initialization in Swift is a two-phase process. In the first phase, each stored property is assigned an initial value by the class that introduced it. Once the initial state for every stored property has been determined, the second phase begins, and each class is given the opportunity to customize its stored properties further before the new instance is considered ready for use.

根 Objective-C 初始化不同的地方在第一步,就是把此类加入的 property 都初始化掉。因为 Swift 并不会像 Objective-C 一样,在没有显式初始化这些 property 的时候,自动把它们设为 nil0

举个例子解释下上面的情况。新手写 Swift 通常会遇到这样的情况,

编译器抱怨 userID 需要在 super.init 之前被初始化,就是因为 userID 没有默认值,如果在初始化的时候,我们还没给它提供值,那就会产生问题了(从编译器的角度来看,初始化的时候需要确保所有 property 都确认有值(nil 也算确认有值)。但你现在并没有告诉我它的值是什么。 )可是编译器为何没有抱怨 genderage 呢?因为它们都有默认值(optional 在默认的情况下为 nil)。

Q: 为什么每次创建 Swift UIViewController 子类,只要添加了任何一个初始化方法,Xcode 都会提醒要添加 required init?(coder:) 方法?
A: 因为 UIViewController 符合了 NSCoding 协议,其中有一个方法是 Designated Initializer,子类必须要实现这个协议的初始化方法。

Q: 一个类可能会有多个 Designated Initializer 吗?
A: 可能。比如 NSArray 就有三个。但一般情况下都是只有一个。

上面只是一些我自己的困惑,如果你也有困惑,欢迎留言。

这下应该可以开开心心地写 Initializer 了吧 :]