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:
Is there a cleaner, more 'Swifty' way to create this structure of mutual non-optional references?
If not, what am I misunderstanding about UnsafeMutablePointer
s and the like in Swift that makes the above code not work?
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!