因为一个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 的原因:

  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-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 的构造过程,请阅读下面的中英文教程:

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,谢谢大家对我们的支持。

(0)

相关推荐

  • Swift学习笔记之构造器重载

    与函数一样,方法也存在重载,其重载的方式与函数一致.那么作为构造器的特殊方法,是否也存在重载呢?答案是肯定的. 一.构造器重载概念 Swift中函数重载的条件也适用于构造器,条件如下: 函数有相同的名字: 参数列表不同或返回值类型不同,或外部参数名不同: Swift中的构造器可以满足以下两个条件,代码如下: 复制代码 代码如下: class Rectangle {     var width : Double     var height : Double     init(width : Do

  • 因为一个Crash引发对Swift构造器的思考分析

    前言 不久前,公司决定在一个 Objective-C 老工程中,开始使用 Swift 进行混合开发.期间,碰到一个与 Swift 类构造过程相关的 Crash.在解决的过程中,对 Swift 构造过程有了更深刻的理解,特作此记录,期望对刚入坑 Swift 开发的同学能有所帮助. Crash 回顾 先来看一下代码,以下定义了 BaseiewController 和 AViewController 两个类: // BaseViewController.h #import <UIKit/UIKit.h

  • 一个等号引发的血案(谈Nginx正确的404配置)

    这是一个血淋淋的教训,这么说一点也不过分.因为最近发生了一个重大问题,网站流量大幅下跌,跌了近80%了.由于事件发生之前做过一些工作,加了大量友链,而且外站权重都相当高,在那天还发生了一次挂马事件,当然也即时解决了.还做了其它一些关键字内.外链优化等等.这样使得查找问题的原因就变的难上加难.偶然的原因发现,百度收录的链接开始出现错误,由于网站URL方式采用的目录式结构,最后一个字符都是/,然而百度收录的页面却无缘无故把这个线去掉了,而这种访问方式,我并没有做兼容.当时也查看了网站页面上的重写结果

  • Vue一个案例引发的递归组件的使用详解

    今天我们继续使用 Vue 的撸我们的实战项目,只有在实战中我们才会领悟更多,光纸上谈兵然并卵,继上篇我们的 <Vue一个案例引发的动态组件与全局事件绑定总结> 之后,今天来聊一聊我们如何在项目中使用递归组件. 信息的分类展示列表 这次我们主要是实现一个信息的分类展示列表存在二级/三级的分类,如下如所示: 看到这个很多人会想到这个实现起来很简单啊,来个嵌套循环不就完事了. 对,你说的没错,事实就是这样简单.那么就先来看看这么简单的列表怎么实现的,然后这个方案的劣势在哪里. 首先看看我们的数据格式

  • 详解Vue一个案例引发「内容分发slot」的最全总结

    今天我们继续来说说 Vue,目前一直在自学 Vue 然后也开始做一个项目实战,我一直认为在实战中去发现问题然后解决问题的学习方式是最好的,所以我在学习一些 Vue 的理论之后,就开始自己利用业余时间做了一个项目,然后通过项目中的一些案例进行总结. 今天我们来说说 Vue 中的内容分发 <slot> ,首先 Vue 实现了一套内容分发的 API,这套 API 是基于当前的 Web Components 规范草案,将 <slot> 元素作为承载内分发内容的出口,内容分发是 Vue 中一

  • JavaScript编程设计模式之构造器模式实例分析

    本文实例讲述了JavaScript编程设计模式之构造器模式.分享给大家供大家参考,具体如下: 经典的OOP语言中,构造器(也叫构造函数)是一个用于初始化对象的特殊方法.在JS中,因为一切皆对象,对象构造器经常被提起. 对象构造器用于建立制定类型(Class)的对象,可以接受参数用于初始化对象的属性和方法. 对象建立 在JS中,有三个常用的方法用于建立对象: //1, 推荐使用 var newObject = {}; //2, var newObject = Object.create( null

  • oracle导入导出表时因一个分号引发的惨案

    oracle 如何导入导出表 在数据库中导出表后导入,是一个完整的操作,内容中的oracle 11g是安装在windows 上的. oracle的imp/exp就相当于oracle数据的还原与备份,利用这个功能我们可以构建两个相同的数据库,一个用于正式的,一个用户测试,一般情况下,我们常用的是将服务器的数据导出来,放在本地进行测试,以便发现问题并改正. 1.如何导出表和数据库: 1.打开 cmd 进入到 exp.exe 所在目录 2. 语法: exp  userid=用户名/密码@哪个数据库  

  • 一个简单Ajax类库及使用方法实例分析

    本文实例讲述了一个简单Ajax类库及使用方法.分享给大家供大家参考,具体如下: ajax.js function Ajax(recvType){ var aj=new Object(); aj.recvType=recvType ? recvType.toUpperCase() : 'HTML' //HTML XML aj.targetUrl=''; aj.sendString=''; aj.resultHandle=null; aj.createXMLHttpRequest=function(

  • windows的文件系统机制引发的PHP路径爆破问题分析

    1.开场白 此次所披露的是以下网页中提出的问题所取得的测试结果: http://code.google.com/p/pasc2at/wiki/SimplifiedChinese <?php for ($i=0; $i<255; $i++) { $url = '1.ph' . chr($i); $tmp = @file_get_contents($url); if (!empty($tmp)) echo chr($i) . "\r\n"; } ?> 已知1.php存在,

  • 一个PHP数组应该有多大的分析

    虽然通常在PHP中进行大量数组运算从一定程度上反应程序设计上可能存在问题,但是粗略的估计数组占用的内存是很有必要的. 首先感觉一下1000个元素的整数数组占有的内存: 复制代码 代码如下: echo memory_get_usage() . "\n"; $a = Array(); for ($i=0; $i<1000; $i++) { $a[$i] = $i + $i; } echo memory_get_usage() . "\n"; for ($i=100

  • JavaScript:new 一个函数和直接调用函数的区别分析

    复制代码 代码如下: function Test() { this.name = 'Test'; return function() { return true; } } var test = new Test(); // 这里的 test 是什么? 是一个 Test 对象吗?错!这里 test 是一个函数--Test 中返回的 function() { return true; }.这时,new Test() 等效于 Test(),注意,是等效于,不是等于,如果使用new Test() ==

随机推荐