初始化详解
前言
- Swift 初始化规则解读
- 了解Struct 快速初始化
- 了解子类化自定义初始化规则解读
- 子类化时如何降低初始化设定项数量
- 何时以及如何使用所需的初始化设定项
作为一名iOS Swift开发者,初始化类和结构是核心基础之一。
Swift中的初始化构造器可分为:
- 成员初始化构造器
- 自定义初始化构造器
- 指定初始化构造器
- 便捷初始化构造器
- 强制初始化构造器
- 可选初始化构造器
- 失败初始化构造器
- 异常初始化构造器
本章将逐一介绍结构体、类、子类的初始化,我们将对(BoardGame)一个棋盘游戏进行建模,使用结构和类构建该体系。在构建过程中,将体验到Swift奇妙的初始化规则和乐趣,以及如何运用这些规则。由于创建游戏机制本身就是本书的主题,因此本章中我们关注初始化的基础知识,探究类初始化以及子类化规则,如何在子类化时减少初始化参数数量并保持在最小,以及如何运用它们。
1. 结构体初始化规则
1. 前言
1. Struct可以简单的进行初始化
2. Struct无法子类化,因此适用于结构简单的模型,例如:Player
2. Player建模
enum Pawn {
case dog;
case car;
case ketchupBottle;
case iron;
case shoe;
case hat
}
struct Player {
let name: String
let pawn: String
}
类似棋盘游戏中的棋子,可以通过传入一个名称和类型创建一个玩家。
let player = Player(name: "eric", pawn: .car)
/// Player(name: "eric", pawn: SwiftInDepth.Pawn.car)
- Player 并没有显式定义初始化构造器,编译器默认会生成一个成员初始化方法。
3. 自定义初始化
1. Swift 语法要求Struct 和 Class 必须给所有属性在初始化时赋值,初始化Player时编译器不允许我们只给name赋值而忽略pawn,否则编译器会报错,参数缺失。
wlet player = Player(name: "eric")
Missing argument for parameter 'pawn' in call
2. 为了简化创建角色的参数,Swift 支持Struct自定义初始化,可以只传递个别参数,但在自定义初始化时需要给其他所有参数赋值。例如:Player自定义初始化参数只有name,初始化中会给pawn随机赋值。
enum Pawn: CaseIterable {
case dog;
case car;
case ketchupBottle;
case iron;
case shoe;
case hat
}
struct Player {
let name: String
let pawn: Pawn
init(name: String) {
self.name = name
self.pawn = Pawn.allCases.randomElement()!
}
}
3. Pawn遵守CaseIterable协议;可以通过allCases属性获取所有案例的数组。然后在Player的自定义初始化方法中,可以使用randomElement()方法来获取随机元素。
4. 注意CaseIterable仅适用于没有关联值的枚举,因为有关联值的枚举理论上可能有无限多的变化。
5. randomElement()获取的随机元素是Optional 类型,可以使用! 强制展开Optional, 此时是安全的。
5. 结构体初始化癖好
这时我们发现成员初始化方法无法执行,编译器会报错
let player = Player(name: "eric", pawn: .car)
Cannot infer contextual base in reference to member 'car'
成员初始化方法报错的原因是为了确保开发人员不绕过自定义初始值化逻辑,这是一个保护机制!如果可以同时提供自定义和成员初始化岂不更好?
Swift支持两种初始化同时存在,将自定义初始化放在结构体的扩展中。这样编译器就会保留成员初始化和自定义初始化。
struct Player {
let name: String
let pawn: Pawn
}
extension Player {
init(name: String) {
self.name = name
self.pawn = Pawn.allCases.randomElement()!
}
}
let player = Player(name: "eric", pawn: .car)
/// Player(name: "eric", pawn: SwiftInDepth.Pawn.car)
let playerYan = Player(name: "eric.yan")
/// Player(name: "eric.yan", pawn: SwiftInDepth.Pawn.dog)
6. 练习题
struct Pancakes {
enum SyrupType {
case corn
case molasses
case maple
}
let syrupType: SyrupType
let stackSize: Int
init(syrupType: SyrupType) {
self.stackSize = 10
self.syrupType = syrupType
}
}
let pancakes = Pancakes(syrupType: .corn, stackSize: 8)
let morePancakes = Pancakes(syrupType: .maple)
1. 根据Pancakes结构体,以上代码可以执行吗?
不可以!
2. 如何解决?
成员初始化和自定义初始化如果要同时支持的话,自定义初始化移至extension中。
struct Pancakes {
enum SyrupType {
case corn
case molasses
case maple
}
let syrupType: SyrupType
let stackSize: Int
}
extension Pancakes {
init(syrupType: SyrupType) {
self.syrupType = syrupType
self.stackSize = 10
}
}
let corn = Pancakes(syrupType: .corn)
let molasses = Pancakes(syrupType: .molasses, stackSize: 20)
print(corn, molasses)
/// Pancakes(syrupType: SwiftInDepthTests.Pancakes.SyrupType.corn, stackSize: 10)
/// Pancakes(syrupType: SwiftInDepthTests.Pancakes.SyrupType.molasses, stackSize: 20)
2. 初始化和子类化
- 子类化(继承)是实现多态性的一种方式。使用多态性,多态是面向对象语言的三大特性之一,一个接口多种实现方案。
- 子类化在Swift社区中并不流行,因为Swift以面向协议的语言自称,这是子类化的替代方案。第2章中学过子类是如何成为一个僵化的数据结构。枚举如何灵活地替代子类化,当使用协议和泛型时,将看到更灵活的方法。
- Swift中仍可以使用子类化,本章主要目的是了解类、子类化 初始化过程,以便更合理的使用子类化。
1. 创建一个BoardGame基类
创建一个BoardGame类作为Game基类,然后子类化BoardGame创建MutablilityLand帮助Swift 开发者编写代码
2. BoardGame 初始化
BoardGame有三个初始化方法:一个指定初始化, 两个便捷初始化
class BoardGame {
static let numberOfTitlesConst = 32
let players: [Player]
let numberOfTitles: Int
init(players: [Player], numberOfTitles: Int) {
self.players = players
self.numberOfTitles = numberOfTitles
}
convenience init(players: [Player]) {
self.init(players: players, numberOfTitles: Self.numberOfTitlesConst)
}
convenience init(names: [String]) {
var players = [Player]()
for name in names {
players.append(Player(name: name))
}
self.init(players: players, numberOfTitles: Self.numberOfTitlesConst)
}
}
BoardGame 有两个属性,players数组存储Player,numberOfTitles 标识BoardGame的Player存储容量,可以通过指定初始化传入两个参数创建BoardGame,也可以通过便捷初始化传部分参数初始化BoardGame。
//Designated initializer
let players = [Player(name: "Melissa"),
Player(name: "SuperJeff"),
Player(name: "Dave")]
let boardGame = BoardGame(players: players, numberOfTitles: players.count)
//Convenience initializer
let players = [Player(name: "Melissa"),
Player(name: "SuperJeff"),
Player(name: "Dave")]
let boardGame = BoardGame(players: players)
//Convenience initializer
let names = ["Melissa", "SuperJeff", "Dave"]
let boardGame = BoardGame(names: names)
- 便捷初始化可以只传递部分参数进行初始化对象,指定初始化必须传入全部参数
- 类Class不同于结构体Struct,成员默认是private,而结构体的成员默认public,因此类无法像结构体一样,编译器默认设置初始化成员函数。
3. 创建一个子类
BoardGame的基类已创建成功,现在可以创建更多BoardGame的子类构建棋盘游戏。
- 创建BoardGame的子类MutabilityLand,
- MutabilityLand 将继承BoardGame的所有初始化方法
class MutabilityLand: BoardGame {
}
let players = [Player(name: "Melissa"),
Player(name: "SuperJeff"),
Player(name: "Dave")]
let mutabilityLandBoardGame = MutabilityLand(players: players, numberOfTitles: players.count)
print(mutabilityLandBoardGame)
let players = [Player(name: "Melissa"),
Player(name: "SuperJeff"),
Player(name: "Dave")]
let mutabilityLandBoardGame = MutabilityLand(players: players)
print(mutabilityLandBoardGame)
let names = ["Melissa", "SuperJeff", "Dave"]
let mutabilityLandBoardGame = MutabilityLand(names: names)
print(mutabilityLandBoardGame)
现在我们对MutabilityLand进行扩展,它不仅有继承自BoardGame的两个属性和三个初始化方法,还有自己的两个属性,分别是scoreBoard<记分板>(已初始化,可以在初始化之后进行update)和winner<胜出者>(可选类型可以为nil)。
class MutabilityLand: BoardGame {
// ScoreBoard is initialized with an empty dictionary
var scoreBoard = [String: Int]()
var winner: Player?
}
至此,MutabilityLand由于已经有了父类的初始化方法,并且自己特有的两个属性不需要指定初始化方法。
4. 丢失的便捷初始化
- Swift 中子类一旦添加了未初始化的属性,初始化子类对象时就会丢失所有父类的初始值设定项,编译器会报错提示,此类有未初始化的存储属性,并失去父类的初始化方法,需要创建指定初始化方法来填充其新增属性,或者给新增的属性填充默认值。
class MutabilityLand: BoardGame {
// ScoreBoard is initialized with an empty dictionary
var scoreBoard = [String: Int]()
var winner: Player?
let instructions: String
// Class 'MutabilityLand' has no initializers Stored property 'instructions' without initial value prevents synthesized initializers
}
- 新增的属性设置默认值
class MutabilityLand: BoardGame {
// ScoreBoard is initialized with an empty dictionary
var scoreBoard = [String: Int]()
var winner: Player?
let instructions: String = ""
}
- 创建指定初始化方法(调用父类的初始化方法)
class MutabilityLand: BoardGame {
// ScoreBoard is initialized with an empty dictionary
var scoreBoard = [String: Int]()
var winner: Player?
let instructions: String
init(players: [Player], numberOfTitles: Int, instructions: String) {
self.instructions = instructions
super.init(players: players, numberOfTitles: numberOfTitles)
}
}
至此,MutabilityLand失去了父类的三个初始化方法,只有本类的一个指定初始化方法。
// 父类的三个初始化方法已失效,编译器会报错,参数缺失.
let mutabilityLand = MutabilityLand(names: ["Melissa", "SuperJeff", "Dave"])
/// Missing argument for parameter 'instructions' in call
let mutabilityLand = MutabilityLand(players: players)
/// Missing argument for parameter 'instructions' in call
let mutabilityLand = MutabilityLand(players: players, numberOfTiles: 32)
/// Missing argument for parameter 'instructions' in call
- 子类失去父类的初始化方法看似奇怪,但子类失去它们是有正当理由的。因为父类初始化方法无法填充其子类的新属性。此外,由于Swift希望填充所有属性,因此它现在只能依赖本类上新指定的初始值设定项。
5. 找回父类初始化方法
实际在开发过程中,子类随时有可能会因为需求变更而新增属性,如果因为新增属性而导致父类初始化方法失效,那么这并不友好。因此Swift 提供了一种方案来保留父类所有的初始化方法,使用override关键字重载父类指定初始化方法,在重载后的指定初始化方法中设置新增属性的默认值。
- 在调用super之前,需要初始化MutabilityLand的属性。
class MutabilityLand: BoardGame {
// ScoreBoard is initialized with an empty dictionary
var scoreBoard = [String: Int]()
var winner: Player?
let instructions: String
override init(players: [Player], numberOfTitles: Int) {
self.instructions = "Just Read the manual"
super.init(players: players, numberOfTitles: numberOfTitles)
}
}
- 在我们重载父类的指定初始化方法时,给新增属性设置默认值,然后调用父类指定初始化方法,子类又可以拥有父类的所有初始化方法。
let mutabilityLand = MutabilityLand(names: ["Melissa", "SuperJeff", "Dave"])
let mutabilityLand = MutabilityLand(players: players)
let mutabilityLand = MutabilityLand(players: players, numberOfTiles: 32)
6. 练习题
- Device有两个属性,一个指定初始化方法,两个便捷初始化方法,Television继承自Device,有两个新增属性,一个指定初始化方法。
class Device {
var serialNumber: String
var room: String
init(serialNumber: String, room: String) {
self.serialNumber = serialNumber
self.room = room
}
convenience init() {
self.init(serialNumber: "Unknown", room: "Unknown")
}
convenience init(serialNumber: String) {
self.init(serialNumber: serialNumber, room: "Unknown")
}
convenience init(room: String) {
self.init(serialNumber: "Unknown", room: room)
}
}
class Television: Device {
enum ScreenType {
case led
case oled
case lcd
case unknown
}
enum Resolution {
case ultraHd
case fullHd
case hd
case sd
case unknown
}
let resolution: Resolution
let screenType: ScreenType
init(resolution: Resolution, screenType: ScreenType, serialNumber:
String, room: String) {
self.resolution = resolution
self.screenType = screenType
super.init(serialNumber: serialNumber, room: room)
}
}
- 通过在某个地方添加一个初始值设定项,使以下代码行正常工作:
let firstTelevision = Television(room: "Lobby")
let secondTelevision = Television(serialNumber: "abc")
-
问题的本质是子类在有新增属性并未初始化时如何调用父类的便捷初始化方法。
解决方案是:子类使用override关键字重载父类指定初始化方法,即可让丢失的父类初始化方法回归。
override init(serialNumber: String, room: String) { self.resolution = Resolution.unknown self.screenType = ScreenType.unknown super.init(serialNumber: serialNumber, room: room) }
3. 最小化类初始化
BoardGame有一个指定的初始化方法;MutabilityLand继承自BoardGame有两个指定的初始化方法,假如将MutabilityLand子类化并添加一个存储属性,则该子类将有三个初始值设定项,依此类推,子类越多,就必须覆盖更多的初始化方法,从而使层次结构变得复杂。Swift提供一个解决方案可以将指定的初始值设定项的数量保持在较低的水平,这样每个子类只包含一个指定的初始方法。
1. 重载便捷初始化方法
在上一节中,MutabilityLand重载了BoardGame类中指定的初始化方法,这里有一个技巧是将MutabilityLand中被重载指定初始方法变成重载便捷初始化方法。
网友评论