考虑以下用例:
在某个游戏的模型中,您有Player
个班级。每个Player
都有unowned let opponent: Player
代表他们正在对抗的对手。这些始终是成对创建的,Player
必须始终具有opponent
,因为它不是可选的。但是,这很难建模,因为一个玩家必须先创建另一个玩家,并且第一个玩家在创建第二个玩家之前不会有对手!
通过一些丑陋的黑客行为,我想出了这个解决方案:
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)
哪个效果很好。但是,我无法将opponent
转换为常量。一种解决方案是使opponent
成为没有set
的计算属性,由私有var
支持,但我希望避免这种情况。
我试图用Swift指针进行一些黑客攻击,然后提出:
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)
}
但是这给了一个段错误。此外,在使用lldb
进行调试时,我们可以看到在p1
初始化之后,我们有:
(lldb) p p1
(Player2.Player) $R3 = 0x0000000101004390 {
opponent = 0x0000000100702940 {
opponent = <uninitialized>
name = ""
}
name = "P1FAKE"
}
但是在函数结束时,lldb显示了这个:
(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"
}
因此p1
正确指向p2
,但p2
仍然指向旧p1
。而且,p1
实际上已经改变了地址!
我的问题是双重的:
是否有更清洁,更多&#39; Swifty&#39;如何创建这种相互非可选引用的结构?
如果没有,我对Swift中的UnsafeMutablePointer
之类的东西有什么误解,导致上述代码不起作用?
答案 0 :(得分:2)
我认为隐式展开的可选就是你想要的。你用感叹号(!
)声明它。这是对编译器的承诺,即使该属性可能在init
调用期间被初始化,但在使用它时它将具有有效值。将其与私人制定者相结合,您可以实现您想要的目标:
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
由于setter是私有的,如果你尝试更改它,编译器将抛出错误:
let p3 = Player(name: "Player Three")
p1.opponent = p3 // Error: Cannot assign to property: 'opponent' setter is inaccessible
请注意,由于您希望getPair
成为创建Player
实例的唯一方法,因此您也可以将init
调用设为私有,因为它未设置opponent
财产:
private init(name: String) {
// ...
}
答案 1 :(得分:1)
在弄乱这一段时间之后,似乎你想要做的事情可能是不可能的,并且与Swift并没有真正相同。更重要的是,它可能是一个有缺陷的方法。
就Swift而言,初始化程序需要在返回之前初始化所有存储的值。这是出于我未能进入的多种原因。当初始化时无法保证/计算值时,将使用选项,IUO和计算值。如果您不想要Optionals,IUO或计算值,但仍希望在初始化后取消设置一些存储的值,那么您也想要吃蛋糕并吃掉它。
就设计而言,如果您需要将两个对象链接得如此紧密以至于在初始化时需要彼此,则您的模型(IMO)会被破坏。这是分层数据结构解决得很好的确切问题。在您的具体示例中,您似乎需要某种匹配或竞争对象来创建和管理两个玩家之间的关系,我知道您的问题更接近&#34;这是否可能&#34;不是&#34;它应该完成&#34;但是我无法想到这不是一个坏主意的任何情况。从根本上说它破坏了封装。
Player对象应该管理和跟踪Player对象中存在的内容,而Player类中唯一的托管关系应该是它的子对象。任何兄弟关系都应由其父母访问/设定。
这成为一个更清晰的规模问题。如果你想添加第三个玩家怎么办? 50岁左右?然后,您必须初始化并将每个玩家连接到其他玩家,然后才能使用任何玩家。如果您想要添加或删除播放器,您必须同时为每个连接的播放器执行此操作,并阻止发生任何事情。
另一个问题是它在任何其他情况下都无法使用。如果设计得当,玩家可以在所有类型的游戏中使用。而目前的设计允许它仅在1v1情况下使用。对于任何其他情况,您必须重新编写它,并且您的代码库会发散。
总之,你想要的东西在Swift中可能是不可能的,但是如果它成为可能的话,它几乎肯定是一个坏主意:)
对于这篇文章感到抱歉,希望你觉得它有用!
答案 2 :(得分:0)
有一种方法可以使用延迟属性(对于方便的API)和包含两个播放器的容器(用于合理的内存管理)在Swift中干净利落地完成此操作。对于TL; DR,请查看下面的示例代码。如需更长的答案,请继续阅读:
根据定义,两个对象之间的循环在Swift中必须是可选的,因为:
在具有垃圾收集器的环境中能够创建一对动态分配的对象,就像你所追求的一样真的更自然(Swift使用自动引用计数,如果它从你的代码中无根,就会泄漏你的对象) 。因此,某种包含两个玩家的容器在Swift中是有用的(如果不是绝对必要的话)。
我认为即使语言限制阻止你在初始化时间做你正在尝试的事情,你的模型还有其他问题可以从两个层次的层次结构中受益。
上述两个问题,尤其是第一个问题,都清楚地指出了某种容器对象的实用程序,它可以处理玩家的初始化(即只有那个容器会知道如何初始化一个Player,并且会能够将所有可变属性绑定在一起)。以下示例代码中的此容器(Match)是我放置opponent(for:Player)
方法来查询玩家的对手的容器。在Player的惰性opponent
属性中调用此方法。
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)
}
}