How does Swift memory management work?

2020-06-16 01:28发布

Specifically, how does Swift memory management work with optionals using the delegate pattern?

Being accustomed to writing the delegate pattern in Objective-C, my instinct is to make the delegate weak. For example, in Objective-C:

@property (weak) id<FooDelegate> delegate;

However, doing this in Swift isn't so straight-forward.

If we have just a normal looking protocol:

protocol FooDelegate {
    func doStuff()
}

We cannot declare variables of this type as weak:

weak var delegate: FooDelegate?

Produces the error:

'weak' cannot be applied to non-class type 'FooDelegate'

So we either don't use the keyword weak, which allows us to use structs and enums as delegates, or we change our protocol to the following:

protocol FooDelegate: class {
    func doStuff()
}

Which allows us to use weak, but does not allow us to use structs or enums.

If I don't make my protocol a class protocol, and therefore do not use weak for my variable, I'm creating a retain cycle, correct?

Is there any imaginable reason why any protocol intended to be used as a delegate protocol shouldn't be a class protocol so that variables of this type can be weak?

I primarily ask, because in the delegation section of the Apple official documentation on Swift protocols, they provide an example of a non-class protocol and a non-weak variable used as the delegate to their class:

protocol DiceGameDelegate {
    func gameDidStart(game: DiceGame)
    func game(game: DiceGame, didStartNewTurnWithDiceRoll diceRoll: Int)
    func gameDidEnd(game: DiceGame)
}
class SnakesAndLadders: DiceGame {
    let finalSquare = 25
    let dice = Dice(sides: 6, generator: LinearCongruentialGenerator())
    var square = 0
    var board: [Int]
    init() {
        board = [Int](count: finalSquare + 1, repeatedValue: 0)
        board[03] = +08; board[06] = +11; board[09] = +09; board[10] = +02
        board[14] = -10; board[19] = -11; board[22] = -02; board[24] = -08
    }
    var delegate: DiceGameDelegate?
    func play() {
        square = 0
        delegate?.gameDidStart(self)
        gameLoop: while square != finalSquare {
            let diceRoll = dice.roll()
            delegate?.game(self, didStartNewTurnWithDiceRoll: diceRoll)
            switch square + diceRoll {
            case finalSquare:
                break gameLoop
            case let newSquare where newSquare > finalSquare:
                continue gameLoop
            default:
                square += diceRoll
                square += board[square]
            }
        }
        delegate?.gameDidEnd(self)
    }
}

Should we take this as a hint that Apple thinks we should be using structs as delegates? Or is this simply a bad example, and realistically, delegate protocols should be declared as class-only protocols so that the delegated object can hold a weak reference to its delegate?

3条回答
劳资没心,怎么记你
2楼-- · 2020-06-16 01:40

Should we take this as a hint that Apple thinks we should be using structs as delegates? Or is this simply a bad example, and realistically, delegate protocols should be declared as class-only protocols so that the delegated object can hold a weak reference to its delegate?

Here's the thing. In real life Cocoa programming, the delegate is likely to be an existing class. It is a class because it exists for some other purpose that only a class can satisfy - because Cocoa demands it.

For example, very often, to take iOS as an example, one view controller needs to act as another view controller's delegate for purposes of arranging a message back and forth between them. Ownership of the view controllers is dictated by the view controller hierarchy and by nothing else. So, in Swift, just as in Objective-C, you had better make that delegate property weak, because it would be terrible if one view controller suddenly took memory management ownership of another view controller!

So, in the real world of the Cocoa framework, there is serious danger of incorrect ownership or of a retain cycle. And that is the problem that weak solves. But it only works, as you rightly say, with classes.

The example in the book, however, is about some objects living off in an abstract made-up artificial Swift-only world. In that world, as long as you aren't in danger of circularity (retain cycle), there's no reason not to use structs and there's no reason to worry about memory management. But that world is not the world you will usually be programming in! And that world is not the framework Cocoa world that your Objective-C delegate pattern comes from and belongs to.

查看更多
爷、活的狠高调
3楼-- · 2020-06-16 01:40

Yes, this example is a bit of an oddity.

Because the example uses a non-class protocol type, it has to expect a possible struct implementing the protocol, which means that the DiceGame instance owns its delegate. And indeed, that violates typical assumptions about the delegate pattern.

It doesn't lead to a reference cycle in this case because the DiceGameTracker is a fictional object that doesn't own the DiceGame itself — but in a real-world app it's possible, even likely, that a delegate might also be the owner of the delegating object. (For example, a view controller might own the DiceGame, and implement DiceGameDelegate so it can update its UI in response to game events.)

That kind of reference cycle would probably turn into a confusing mess if either the game, its delegate, or the type implementing one or both of those protocols were a value type — because value types are copied, some of the variables in your setup would end up being distinct copies of the game (or game's owner).

Realistically one would expect to use reference types (classes) to implement this anyway, even if the protocol declaration leaves open the possibility of doing it otherwise. Even in a hypothetical Swift-only world, it probably makes sense to do it that way... generally whenever you have something with long life, internal mutable state, and a usage pattern that has it being accessed by potentially multiple other actors, you want a class type, even if you can sort of fake otherwise with value types and var.

查看更多
够拽才男人
4楼-- · 2020-06-16 01:40

If you must have structs and emums in your protocol, then if you make your delegate nil just before you close the view controller, then this breaks the retain cycle. I verified this in Allocations in Instruments.

// view controller about to close
objectCreatedByMe.delegate = nil

It's an optional, so this is valid.

查看更多
登录 后发表回答