因为一个Crash引发对Swift构造器的思考分析
作者:郑一一
前言
不久前,公司决定在一个 Objective-C 老工程中,开始使用 Swift 进行混合开发。期间,碰到一个与 Swift 类构造过程相关的 Crash。在解决的过程中,对 Swift 构造过程有了更深刻的理解,特作此记录,期望对刚入坑 Swift 开发的同学能有所帮助。
Crash 回顾
先来看一下代码,以下定义了 BaseiewController 和 AViewController 两个类:
// BaseViewController.h #import <UIKit/UIKit.h> NS_ASSUME_NONNULL_BEGIN @interface BaseViewController : UIViewController - (instancetype)initWithParamenterA:(NSInteger)parameterA; @end NS_ASSUME_NONNULL_END // BaseViewController.m #import "BaseViewController.h" @interface BaseViewController () @property (nonatomic, assign) NSInteger parameterA; @end @implementation BaseViewController - (instancetype)initWithParamenterA:(NSInteger)parameterA { self = [super init]; if (self) { self.parameterA = parameterA; } return self; } @end
以上代码段定义了 Objective-C 类 BaseViewController,并且自定义了构造器 initWithParamenterA。
// AViewController.swift import UIKit class AViewController: BaseViewController { let count: Int init(count: Int, parameterA: Int) { self.count = count super.init(paramenterA: parameterA) } // 后面的 “initCoder 从哪儿来” 小节会讲讲这个构造器 required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } }
第二块代码段定义了 Swift 类 AViewController,继承自 BaseViewController,并且自定义了构造器 init(count: Int, parameterA: Int),这个构造器还调用到了父类的 initWithParamenterA 构造器。细心的同学可能发现了,代码中还出现了 init?(coder aDecoder: NSCoder) 构造器,对此,在 initCoder 从哪儿来小节会有详细解释。
代码就这么多。构建运行工程,前往 AViewController 页面,出乎意料,Crash。控制台输出:
`Fatal error: Use of unimplemented initializer 'init(nibName:bundle:)' for class 'XXX.AViewController'`
意思是 AViewController 没有实现 init(nibName:bundle:) 方法,从而导致了 Crash。
对于刚入坑 Swift 不久的同学可能就会有些懵逼。明明在 Objective-C 的时候这样写根本没有问题啊,怎么到 Swift 这儿就 Crash 了呢?
Swift 类类型的构造过程回顾
如果想要了解 Crash 的原因,就需要了解 UIViewController 所属的类类型(class)构造器的相关知识。
注:本小节大部分内容摘自Swift 官方中文教程。
指定构造器和便利构造器
Swift 为类类型提供了两种构造器,分别是指定构造器和便利构造器。
类倾向于拥有极少的指定构造器,普遍的是一个类只拥有一个指定构造器。每一个类都必须至少拥有一个指定构造器。指定构造器语法如下:
init(parameters) { statements }
便利构造器是类中比较次要的、辅助型的构造器。你可以定义便利构造器来调用同一个类中的指定构造器,并为部分形参提供默认值。一般只在必要的时候为类提供便利构造器。
便利构造器也采用相同样式的写法,但需要在 init 关键字之前放置 convenience 关键字,并使用空格将它们俩分开:
convenience init(parameters) { statements }
类类型的构造器代理
规则 1
指定构造器必须调用其直接父类的的指定构造器。
规则 2
便利构造器必须调用同类中定义的其它构造器。
规则 3
便利构造器最后必须调用指定构造器。
一个更方便记忆的方法是:
- 指定构造器必须总是向上代理
- 便利构造器必须总是横向代理
这些规则可以通过下面图例来说明:
类类型的继承和重写
跟 Objective-C 中的子类不同,Swift 中的子类默认情况下不会继承父类的构造器。Swift 的这种机制可以防止一个父类的简单构造器被一个更精细的子类继承,而在用来创建子类时的新实例时没有完全或错误被初始化。
构造器的自动继承
如上所述,子类在默认情况下不会继承父类的构造器。但是如果满足特定条件,父类构造器是可以被自动继承的。事实上,这意味着对于许多常见场景你不必重写父类的构造器,并且可以在安全的情况下以最小的代价继承父类的构造器。
假设你为子类中引入的所有新属性都提供了默认值,以下 2 个规则将适用:
规则 1
如果子类没有定义任何指定构造器,它将自动继承父类所有的指定构造器。(反之,如果定义了指定构造器,就不会继承父类的指定构造器)
规则 2
如果子类提供了所有父类指定构造器的实现——无论是通过规则 1 继承过来的,还是提供了自定义实现——它将自动继承父类所有的便利构造器。
即使你在子类中添加了更多的便利构造器,这两条规则仍然适用。
注意
子类可以将父类的指定构造器实现为便利构造器来满足规则 2。
UIViewController 的指定构造器
UIViewController 在 Swift 中定义了两个指定构造器。
当使用 StoryBoard 创建 UIViewController 时,最终会调用:
init?(coder: NSCoder)
在使用除了 StoryBoard 之外的其它方式创建时,包括代码、Xib 的创建,最终会调用:
init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?)
分析与解决
讲完了 Swift 类类型构造器知识,先来分析一下 Swift 类 AViewController 。AViewController 定义了一个指定构造器 init(count: Int, parameterA: Int),因此根据构造器的自动继承的规则 1, AViewController 不会自动继承父类的指定构造器,包括 init(nibName:bundle:)。也就是说 AViewController 没有实现 init(nibName:bundle:)。
其次 BaseViewController 是 Objective-C 类,所以可以不遵循 Swift 构造器的规则。我们可以看到在 BaseViewController 的指定构造器 initWithParamenterA 中,调用的是 [super init] ,这个方法并不是其父类的指定构造器,不过就算这样写,编译器也不会报错。
@implementation BaseViewController - (instancetype)initWithParamenterA:(NSInteger)parameterA { // 在 Objective-C 中,子类的指定构造器,不需要强制调用父类的指定构造器。 // 调用 init,编译允许通过 self = [super init]; if (self) { self.parameterA = parameterA; } return self; } @end
而在 AViewController 的构造过程中,BaseViewController 的指定构造器中 [super init] 这句代码最终会调用当前类(AViewController)并没有实现的 init(nibName:bundle:) ,从而导致了 Crash。这也就对应了控制台输出的信息:
Fatal error: Use of unimplemented initializer 'init(nibName:bundle:)' for class 'XXX.AViewController'
再来简单总结一下 Crash 的原因:
- 子类 AViewController 自定义了指定构造器,但没有实现父类的指定构造器 init(nibName:bundle:)
- 父类 BaseViewController 的构造器中直接调用了 [super init],导致最终调用了 AViewController 没有实现的 init(nibName:bundle:) ,从而 Crash。
换句话说,如果子类 AViewController 没有自定义指定构造器或者父类 BaseViewController 遵循了类类型的构造器代理的规则1,就不会发生 Crash。
据此,解决的方案也呼之欲出啦:
方法一:此处定义一个 SwiftBaseViewController 来替代 BaseViewController,其指定构造器不允许调用 super.init ,因此也就避免了 Crash:
import UIKit class SwiftBaseViewController: UIViewController { let parameterA: Int init(parameterA: Int) { self.parameterA = parameterA // 调用 super.init(),编译不通过 // 报错信息:Must call a designated initializer of the superclass 'UIViewController' // super.init() // 必须调用父类的指定构造器 super.init(nibName: nil, bundle: nil) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } }
这个方法的好处是可以从编译器层面阻止直接调用 super.init,避免了程序员犯错的可能。
不过这个方法的缺点是需要改变 BaseViewController 的编写语言。迁移成本较大。
方法二:修改 BaseViewController 的构造器实现,将 self = [super init] 替换为 self = [super initWithNibName:nil bundle:nil]。
@implementation BaseViewController - (instancetype)initWithParamenterA:(NSInteger)parameterA { //self = [super init]; self = [super initWithNibName:nil bundle:nil]; if (self) { self.parameterA = parameterA; } return self; } @end
这种方法是让 Objective-C 类 BaseViewController 强制遵循 Swift 构造器的规则,调用了父类的指定构造器。
方法三:在子类 AViewController 中修改:
class AViewController: BaseViewController { var count: Int = 0 // 使用便利构造器 convenience init(count: Int, parameterA: Int) { self.init(paramenterA: parameterA) self.count = count } }
使用便利构造器代替了原先的指定构造器,根据构造器的自动继承规则 1,AViewController 自动继承了父类所有的指定构造器,包括 init(nibName:bundle:)。这个方法的缺点是,原本的常量属性 count 需要变更为变量,并被赋予默认值。
initCoder 从哪儿来
在 Swift 的 UIViewController 子类中,如果自定义指定构造器后,就必须实现构造器 init?(coder aDecoder: NSCoder),这是为什么呢?
我们可以查看 UIViewController 的接口文件,其遵循 NSCoding 协议:
class UIViewController : NSCoding, ...
再来看一下 NSCoding 协议的内容:
protocol NSCoding { func encode(with coder: NSCoder) init?(coder: NSCoder) // NS_DESIGNATED_INITIALIZER }
其中定义了一个指定构造器 init?(coder: NSCoder)。因为还需要遵循协议,这个构造器同时是一个必要构造器。
必要构造器
在类的构造器前添加 required 修饰符表明所有该类的子类都必须实现该构造器。
根据构造器的自动继承规则 1,如果子类自定义了指定构造器,那么就无法继承父类的指定构造器,恰巧 init?(coder: NSCoder) 还是一个必要构造器,所以就必须在子类中实现该方法。
那么,这种情况就比较尴尬啦。明明就没有在项目中使用到 StoryBoard。可是每次都要加上这么一段代码,显得非常冗余:
required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") }
那么有什么办法可以避免重复写这段代码吗?
答案是有的!方法是在 BaseViewController 中声明该方法不可用,那么继承自 BaseViewController 的所有子类都不需要实现这个方法。
Swift 版本:
@available(*, unavailable, message: "Unsupported init(coder:)") required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") }
Objective-C 版本:
- (nullable instancetype)initWithCoder:(NSCoder *)coder NS_UNAVAILABLE;
Swift 构造器知识拾遗
除了上面讲到的一些构造器知识,这里还会再讲讲一些其它比较重要的点。
默认构造器
如果结构体或类为所有属性提供了默认值,又没有提供任何自定义的构造器,那么 Swift 会给这些结构体或类提供一个默认构造器。这个默认构造器将简单地创建一个所有属性值都设置为它们默认值的实例。
class ShoppingListItem { var name: String? var quantity = 1 var purchased = false } var item = ShoppingListItem()
逐一构造器
只要你曾经了解过 Swift,肯定听说过许许多多关于类和结构体的区别。对于习惯使用类的同学来说,这里不妨再多告诉你一个使用结构体的理由。
官方文档中提到,结构体如果没有定义任何自定义构造器,它们将自动获得逐一成员构造器(memberwise initializer)。不像默认构造器,即使存储型属性没有默认值,结构体也能会获得逐一成员构造器。
struct Size { var width = 0.0, height = 0.0 } let twoByTwo = Size(width: 2.0, height: 2.0) // Swift 5.1 甚至会为你生成省去了有默认值属性的逐一构造器。省去的属性将会直接使用默认值 let zeroByTwo = Size(height: 2.0) let twoByZero = Size(width: 2.0)
某些场景下,如果确实需要自定义一个构造器,但又想保留逐一成员构造器,那么请在 extension 中自定义构造器。
不过对于类来说,所有的构造器都必须自己来实现。所以从使用便利性的角度来说,结构体无疑是一个更好的选择。
可失败构造器
在 Swift 中可以定义一个构造器可失败的类,结构体或者枚举。这里的“失败”指的是,如给构造器传入无效的形参,或缺少某种所需的外部资源,又或是不满足某种必要的条件等。
为了妥善处理这种构造过程中可能会失败的情况。你可以在一个类,结构体或是枚举类型的定义中,添加一个或多个可失败构造器。其语法为在 init 关键字后面添加问号(init?)。比如 Int 存在如下可失败构造器:
init?(exactly source: Float)
推荐阅读
想要更全面深入了解 Swift 的构造过程,请阅读下面的中英文教程:
总结
以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,谢谢大家对脚本之家的支持。