2.15 反初始化

原文链接: https://docs.swift.org/latest/documentation/the-swift-programming-language/deinitialization/

2.15 反初始化

释放需要自定义清理的资源。

反初始化器在类实例被释放之前立即调用。反初始化器用 deinit 关键字书写,就像构造器用 init 关键字书写一样。反初始化器只对类类型可用。

反初始化如何工作

当实例不再需要时,Swift 会自动释放它们以腾出资源。Swift 通过自动引用计数(ARC)管理实例的内存,详见自动引用计数。通常,实例被释放时你不需要手动做清理工作。不过,当你处理自己创建的资源时,可能需要自己做一些额外的清理。例如,如果你创建了一个自定义类来打开文件并写入数据,可能需要在类实例被释放之前关闭该文件。

类的定义最多只能有一个反初始化器。反初始化器不接受任何参数,书写时不带圆括号:

1
2
3
deinit {
    // 执行反初始化
}

反初始化器会在实例释放之前自动调用,你无权自己调用它。父类的反初始化器会被子类继承,并且在子类反初始化器实现结束时自动调用父类的反初始化器。即使子类没有提供自己的反初始化器,父类的反初始化器也总会被调用。

由于实例要等到它的反初始化器被调用之后才会被释放,反初始化器可以访问调用它的那个实例的所有属性,并根据这些属性调整它的行为(例如查出一个需要关闭的文件的名字)。

反初始化器的实际运用

下面是一个反初始化器实际工作的例子。这个例子为一个小游戏定义了两个新类型:Bank 和 Player。Bank 类管理一种虚拟货币,流通中的金币数量永远不会超过 10,000 枚。游戏中只可能有一个 Bank,因此 Bank 被实现为一个带类型属性和方法的类,用来存储和管理它的当前状态:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
class Bank {
    static var coinsInBank = 10_000
    static func distribute(coins numberOfCoinsRequested: Int) -> Int {
        let numberOfCoinsToVend = min(numberOfCoinsRequested, coinsInBank)
        coinsInBank -= numberOfCoinsToVend
        return numberOfCoinsToVend
    }
    static func receive(coins: Int) {
        coinsInBank += coins
    }
}

Bank 用它的 coinsInBank 属性记录当前持有的金币数量,还提供 distribute(coins:) 和 receive(coins:) 两个方法来处理金币的发放与回收。

distribute(coins:) 方法在发放之前先检查银行中的金币是否足够。如果不够,Bank 就返回比请求数量更少的数字(银行里没有金币时返回零)。它返回一个整数值,表示实际给出的金币数量。

receive(coins:) 方法则只是把收到的金币数量加回银行的储备中。

Player 类描述游戏中的一名玩家。每名玩家在任何时刻钱袋里都存有一定数量的金币,这由玩家的 coinsInPurse 属性表示:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
class Player {
    var coinsInPurse: Int
    init(coins: Int) {
        coinsInPurse = Bank.distribute(coins: coins)
    }
    func win(coins: Int) {
        coinsInPurse += Bank.distribute(coins: coins)
    }
    deinit {
        Bank.receive(coins: coinsInPurse)
    }
}

每个 Player 实例在初始化时都会从银行领取指定数量的金币作为启动资金,不过如果银行里的金币不足,Player 实例可能领到比这个数量更少的金币。

Player 类定义了 win(coins:) 方法,从银行取出一定数量的金币并加入玩家的钱袋。Player 类还实现了一个反初始化器,它会在 Player 实例被释放之前调用。这里反初始化器只是把玩家的所有金币还给银行:

1
2
3
4
5
var playerOne: Player? = Player(coins: 100)
print("A new player has joined the game with \(playerOne!.coinsInPurse) coins")
// 输出 "A new player has joined the game with 100 coins"。
print("There are now \(Bank.coinsInBank) coins left in the bank")
// 输出 "There are now 9900 coins left in the bank"。

这里创建了一个新的 Player 实例,如果银行里有金币就请求 100 枚。这个 Player 实例存放在一个可选 Player 变量 playerOne 中。之所以使用可选变量,是因为玩家可以在任何时刻离开游戏,可选类型让你能够跟踪当前游戏中是否还有玩家。

由于 playerOne 是可选值,访问它的 coinsInPurse 属性来打印初始金币数量时,以及调用它的 win(coins:) 方法时,都要用感叹号(!)限定:

1
2
3
4
5
playerOne!.win(coins: 2_000)
print("PlayerOne won 2000 coins & now has \(playerOne!.coinsInPurse) coins")
// 输出 "PlayerOne won 2000 coins & now has 2100 coins"。
print("The bank now only has \(Bank.coinsInBank) coins left")
// 输出 "The bank now only has 7900 coins left"。

这里玩家赢了 2,000 枚金币,钱袋里现在有 2,100 枚金币,银行则只剩下 7,900 枚。

1
2
3
4
5
playerOne = nil
print("PlayerOne has left the game")
// 输出 "PlayerOne has left the game"。
print("The bank now has \(Bank.coinsInBank) coins")
// 输出 "The bank now has 10000 coins"。

玩家现在已经离开游戏,这通过把可选变量 playerOne 设为 nil 来表示,意思是"没有 Player 实例"。此时 playerOne 变量对 Player 实例的引用被断开,不再有其他属性或变量引用这个 Player 实例,因此它被释放以腾出内存。就在释放之前,它的反初始化器被自动调用,把金币还给了银行。