简体   繁体   中英

Mutual non-optional reference cycle in Swift

Consider the following use case:

In a model for some game, you have a Player class. Each Player has an unowned let opponent: Player which represents the opponent they are playing against. These are always created in pairs, and a Player must always have an opponent since it is non-optional. However, this is very difficult to model, since one player must be created before the other, and the first player will not have an opponent until after the second one is created!

Through some ugly hacking, I came up with this solution:

class Player {
    private static let placeholder: Player = Player(opponent: .placeholder, name: "")

    private init(opponent: Player, name: String) {
        self.opponent = opponent
        self.name = name
    }

    unowned var opponent: Player
    let name: String

    class func getPair(named names: (String, String)) -> (Player, Player) {
        let p1 = Player(opponent: .placeholder, name: names.0)
        let p2 = Player(opponent: p1, name: names.1)
        p1.opponent = p2
        return (p1, p2)
    }
}

let pair = Player.getPair(named:("P1", "P2"))

print(pair.0.opponent.name)
print(pair.1.opponent.name)

Which works quite well. However, I am having trouble turning opponent into a constant. One solution is to make opponent a computed property without a set , backed by a private var , but I'd like to avoid this.

I attempted to do some hacking with Swift pointers, and came up with:

class func getPair(named names: (String, String)) -> (Player, Player) {
    var p1 = Player(opponent: .placeholder, name: names.0 + "FAKE")
    let p2 = Player(opponent: p1, name: names.1)

    withUnsafeMutablePointer(to: &p1) {
        var trueP1 = Player(opponent: p2, name: names.0)
        $0.moveAssign(from: &trueP1, count: 1)
    }
    return (p1, p2)
}

But this gives a segfault. Furthermore, when debugging with lldb , we can see that just after p1 is initialized, we have:

(lldb) p p1
(Player2.Player) $R3 = 0x0000000101004390 {
  opponent = 0x0000000100702940 {
    opponent = <uninitialized>
    name = ""
  }
  name = "P1FAKE"
}

But at the end of the function, lldb shows this:

(lldb) p p1
(Player2.Player) $R5 = 0x00000001010062d0 {
  opponent = 0x00000001010062a0 {
    opponent = 0x0000000101004390 {
      opponent = 0x0000000100702940 {
        opponent = <uninitialized>
        name = ""
      }
      name = "P1FAKE"
    }
    name = "P2"
  }
  name = "P1"
}

(lldb) p p2
(Player2.Player) $R4 = 0x00000001010062a0 {
  opponent = 0x0000000101004390 {
    opponent = 0x0000000100702940 {
      opponent = <uninitialized>
      name = ""
    }
    name = "P1FAKE"
  }
  name = "P2"
}

So p1 correctly points to p2 , but p2 still points to the old p1 . What's more, p1 has actually changed addresses!

My question is two-fold:

  1. Is there a cleaner, more 'Swifty' way to create this structure of mutual non-optional references?

  2. If not, what am I misunderstanding about UnsafeMutablePointer s and the like in Swift that makes the above code not work?

I think an implicitly unwrapped optional is what you want. You declare it with an exclamation mark ( ! ). It's a promise to the compiler that even though the property may be initialized during the init call, it will have a valid value when you use it. Combining this with a private setter, you can achieve what you want:

class Player: CustomStringConvertible {
    var name: String
    private(set) weak var opponent: Player!

    init(name: String) {
        self.name = name
    }

    class func getPair(named names: (String, String)) -> (Player, Player) {
        let p1 = Player(name: names.0)
        let p2 = Player(name: names.1)

        p1.opponent = p2
        p2.opponent = p1
        return (p1, p2)
    }

    var description: String {
        return self.name
    }
}

let (p1, p2) = Player.getPair(named: ("Player One", "Player Two"))
print(p1.opponent) // Player Two
print(p2.opponent) // Player One

Since the setter is private the compiler will throw an error if you try to change it:

let p3 = Player(name: "Player Three")
p1.opponent = p3 // Error: Cannot assign to property: 'opponent' setter is inaccessible

Note that since you intended for getPair to be the single method to create a Player instance, you may as well set the init call to private since it doesn't set the opponent property:

private init(name: String) {
    // ...
}

After messing with this for a while it seems what you're wanting to do is likely not possible, and doesn't really jive with Swift. More importantly, it's likely a flawed approach to begin with.

As far as Swift goes, initializers are required to initialize all stored values before they return. This is for a number of reasons I won't go into. Optionals, IUOs, and computed values are used when a value cannot be guaranteed/calculated at initialization. If you don't want Optionals, IUOs or computed values but still want some stored values to be unset after init, you're wanting to have your cake and eat it too.

As far as the design, if you need two objects to be linked so tightly as to require each other at initialization, your model is (IMO) broken. This is the exact problem hierarchical data structures solve so well. In your specific example, it seems clear you need some sort of Match or Competition object that creates and manages the relationship between the two players, I know your question is closer to "is this possible" not "should it be done", but I can't think of any situation where this isn't a bad idea. Fundamentally it breaks encapsulation.

The Player object should manage and track the things that exist within a Player object, and the only managed relationships within the Player class should be with it's children. Any sibling relationship should be accessed/set by it's parent.

This becomes a clearer problem with scale. What if you want to add a third player? what about 50? You will then have to initialize and connect every single player to ever other before you can use any of the players. If you want to add or remove a player, you will have to do it for every single connected player simultaneously, and block anything from happening while that takes place.

Another issues is it makes it unusable in any other situation. If designed properly, a Player could be used in all types of games. Whereas the current design allowed it to be used only in a 1v1 situation. For any other case you would have to re-write it and your code base would diverge.

In summation, what you want is probably not possible in Swift, but if or when it becomes possible, it's almost certainly a bad idea anyway :)

Sorry for the essay, hope you find it helpful!

There is a way to do this cleanly in Swift using lazy properties (for a convenient API) and a container that contains both players (for sane memory management). For a TL;DR, take a look at the example code below. For a longer answer, read on:

By definition, a cycle between two objects must be optional in nature in Swift because:

  1. Swift dictates that all fields of an object need to be initialized by the time the the object's initializer has executed. So, an optional, or an implicitly unwrapped optional reference, or an unowned reference are your options if you want to tie together two objects with references (both need initialization, so at least one exists before its opponent).
  2. If the objects are of a class type, then they should be weakly referenced, and again, weak references are by definition optional in nature (either automatically zeroed and implicit or explicit).

Being able to create a pair of dynamically allocated objects like what you are after is really more natural in an environment with a garbage collector (Swift uses automated reference counting which simply leaks your pair of objects if it goes unrooted from your code). Some kind of container that contains both players is therefore useful (if not absolutely necessary) in Swift.

I would argue that even despite the language limitations which prevent you from doing what you are trying on initialization time, your model has other problems which would benefit from a hierarchy of two levels.

  • If a player only ever exists in context of another player, you should only ever be able to create a maximum of two of them per match.
  • You may want to also define an order to the players, for instance to decide who starts if it is a turn based game, or to define for presentation purposes one of the players as playing a "home" match, etc.

Both of the above concerns, especially the first, is really pointing clearly to the utility of some kind of container object that would handle the initialization of your Players (ie only that container would know how to initialize a Player, and would be able to bind all the mutable properties together). This container (Match) in the below example code is one where I placed an opponent(for:Player) method to query for the opponent for a player. This method gets called in the lazy opponent property of Player.

public class Match {

    public enum PlayerIndex {
        case first
        case second
    }

    private(set) var players:PlayerPair

    init(players:PlayerNamePair) {
        // match needs to be first set to nil because Match fields need setting before 'self' can be referenced.
        self.players = (Player(match: nil, name: players.A, index: .first),
                        Player(match: nil, name: players.A, index: .second))

        // then set the 'match' reference in the Player objects.
        self.players.A.match = self
        self.players.B.match = self
    }

    public func opponent(for player:Player) -> Player {
        switch (player.index) {
        case .first:
            return self.players.B

        case .second:
            return self.players.A
        }
    }

    /* Player modelled here as a nested type to a Match.
     * That's just a personal preference, but incidental to the question posted. */

    typealias PlayerNamePair = (A:String, B:String)
    typealias PlayerPair = (A:Player, B:Player)

    public class Player {
        public let name:String

        fileprivate let index:PlayerIndex
        fileprivate weak var match:Match?

        /* This init method is only visible inside the file, and only called by Match initializer. */
        fileprivate init(match:Match?, name:String, index:PlayerIndex) {
            self.name = name
            self.match = match
            self.index = index
        }

        /* We dare implicitly unwrap here because Player initialization and lifecycle
        * is controlled by the containing Match.
        *
        * That is, Players only ever exists in context of an owning match,
        * therefore it's OK to treat it as a bug which crashes reproducibly
        * if you query for the opponent for the first time only after the match (which we know to have been non-nil) has already been deallocated. */
        public lazy var opponent:Player = public lazy var opponent:Player = self.match!.opponent(for: self)
    }
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM