由一个Crash引发对 Swift 构造器的思考

不久前,公司决定在一个 Objective-C 老工程中,开始使用 Swift 进行混合开发。期间,碰到一个与 Swift 类构造过程相关的 Crash。在解决的过程中,对 Swift 构造过程有了更深刻的理解,特作此记录,期望对刚入坑 Swift 开发的同学能有所帮助。

Crash 回顾

先来看一下代码,以下定义了 BaseiewControllerAViewController 两个类:

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

便利构造器最后必须调用指定构造器。

一个更方便记忆的方法是:

  • 指定构造器必须总是向上代理
  • 便利构造器必须总是横向代理

这些规则可以通过下面图例来说明:

img

类类型的继承和重写

跟 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 类 AViewControllerAViewController 定义了一个指定构造器 init(count: Int, parameterA: Int),因此根据构造器的自动继承的规则 1AViewController 不会自动继承父类的指定构造器,包括 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 的原因:

  1. 子类 AViewController 自定义了指定构造器,但没有实现父类的指定构造器 init(nibName:bundle:)
  2. 父类 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-CBaseViewController 强制遵循 Swift 构造器的规则,调用了父类的指定构造器。

方法三:在子类 AViewController 中修改:

class AViewController: BaseViewController {
    var count: Int = 0
        // 使用便利构造器
    convenience init(count: Int, parameterA: Int) {
        self.init(paramenterA: parameterA)
        self.count = count
    }
}

使用便利构造器代替了原先的指定构造器,根据构造器的自动继承规则 1AViewController 自动继承了父类所有的指定构造器,包括 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 的构造过程,请阅读下面的中英文教程:

Swift 官方教程

Swift 官方中文教程

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

推荐阅读更多精彩内容