SwiftInDepth_05_初始化构造器

初始化详解

前言

  • Swift 初始化规则解读
  • 了解Struct 快速初始化
  • 了解子类化自定义初始化规则解读
  • 子类化时如何降低初始化设定项数量
  • 何时以及如何使用所需的初始化设定项

作为一名iOS Swift开发者,初始化类和结构是核心基础之一。

Swift中的初始化构造器可分为:

  1. 成员初始化构造器
  2. 自定义初始化构造器
  3. 指定初始化构造器
  4. 便捷初始化构造器
  5. 强制初始化构造器
  6. 可选初始化构造器
  7. 失败初始化构造器
  8. 异常初始化构造器

本章将逐一介绍结构体、类、子类的初始化,我们将对(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的子类构建棋盘游戏。

  1. 创建BoardGame的子类MutabilityLand,
  2. 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. 丢失的便捷初始化

  1. 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
}
  1. 新增的属性设置默认值
class MutabilityLand: BoardGame {
    // ScoreBoard is initialized with an empty dictionary
    var scoreBoard = [String: Int]()
    var winner: Player?
    let instructions: String = ""
}   
  1. 创建指定初始化方法(调用父类的初始化方法)
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
  1. 子类失去父类的初始化方法看似奇怪,但子类失去它们是有正当理由的。因为父类初始化方法无法填充其子类的新属性。此外,由于Swift希望填充所有属性,因此它现在只能依赖本类上新指定的初始值设定项。

5. 找回父类初始化方法

实际在开发过程中,子类随时有可能会因为需求变更而新增属性,如果因为新增属性而导致父类初始化方法失效,那么这并不友好。因此Swift 提供了一种方案来保留父类所有的初始化方法,使用override关键字重载父类指定初始化方法,在重载后的指定初始化方法中设置新增属性的默认值。

  1. 在调用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)
    }
}
  1. 在我们重载父类的指定初始化方法时,给新增属性设置默认值,然后调用父类指定初始化方法,子类又可以拥有父类的所有初始化方法。
 let mutabilityLand = MutabilityLand(names: ["Melissa", "SuperJeff", "Dave"])
 let mutabilityLand = MutabilityLand(players: players)
 let mutabilityLand = MutabilityLand(players: players, numberOfTiles: 32)

6. 练习题

  1. 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)
    }
}
  1. 通过在某个地方添加一个初始值设定项,使以下代码行正常工作:
let firstTelevision = Television(room: "Lobby")
let secondTelevision = Television(serialNumber: "abc")
  1. 问题的本质是子类在有新增属性并未初始化时如何调用父类的便捷初始化方法。

    解决方案是:子类使用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中被重载指定初始方法变成重载便捷初始化方法。

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

推荐阅读更多精彩内容