[英]Mutual non-optional reference cycle in Swift
考虑以下用例:
在某些游戏的模型中,您有一个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
的计算属性,并由private 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
实际上更改了地址!
我的问题有两个:
是否有一种更清洁,更“快捷”的方式来创建这种相互非可选引用的结构?
如果不是,那么我对Swift中的UnsafeMutablePointer
以及类似的东西会产生什么误解,导致上述代码无法正常工作?
我认为您想要一个隐式展开的可选内容。 您用感叹号( !
)声明它。 对编译器的保证是,即使可以在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
调用设置为private,因为它没有设置opponent
属性:
private init(name: String) {
// ...
}
弄乱了一段时间后,似乎您想做的事情似乎不可能实现,并且与Swift并没有真正的契合。 更重要的是,这可能是一个有缺陷的方法。
就Swift而言,初始化器需要在所有存储的值返回之前对其进行初始化。 这是出于多种原因,我不打算讨论。 当在初始化时不能保证/计算某个值时,将使用可选值,IUO和计算值。 如果您不希望使用Optional,IUU或计算值,但仍希望在初始化后取消某些存储值的设置,那么您也想吃点蛋糕。
就设计而言,如果您需要紧密链接两个对象以在初始化时互相要求,则模型(IMO)会损坏。 这是分层数据结构很好解决的确切问题。 在您的特定示例中,您似乎显然需要某种“比赛”或“竞赛”对象来创建和管理两个玩家之间的关系,我知道您的问题更接近于“这是否可能”而不是“应该做到”,但是我想不到这不是一个坏主意的任何情况。 从根本上说,它破坏了封装。
Player对象应该管理和跟踪Player对象中存在的事物,并且Player类中唯一受管理的关系应该与其子对象有关。 任何同级关系都应由其父级访问/设置。
这成为规模上更明显的问题。 如果要添加第三名玩家怎么办? 那50呢? 然后,您将必须初始化每个播放器并将其彼此连接,然后才能使用任何播放器。 如果要添加或删除播放器,则必须同时为每个连接的播放器执行此操作,并阻止发生该情况的任何事情。
另一个问题是它使它在任何其他情况下都无法使用。 如果设计合理,则可以在所有类型的游戏中使用播放器。 而当前的设计只允许在1v1的情况下使用它。 在任何其他情况下,您都必须重新编写它,并且代码库会有所不同。
总而言之,您想要的东西可能在Swift中是不可能的,但是如果或当它成为可能时,无论如何这几乎肯定是个坏主意:)
对不起这篇文章,希望对您有所帮助!
在Swift中,有一种方法可以使用惰性属性(用于方便的API)和包含两个播放器的容器(用于合理的内存管理)来干净地执行此操作。 对于TL; DR,请看下面的示例代码。 要获得更长的答案,请继续阅读:
根据定义,两个对象之间的循环本质上在Swift中必须是可选的,因为:
在具有垃圾收集器的环境中,能够创建一对动态分配的对象(如您所追求的对象)实际上更加自然(Swift使用自动引用计数,如果它从代码中脱颖而出,只会泄漏该对对象)。 因此,包含两个玩家的某种容器在Swift中很有用(如果不是绝对必要的话)。
我会争辩说,尽管语言限制使您无法在初始化时做任何事情,但是您的模型还有其他问题,可以从两个层次的层次结构中受益。
以上两个问题,尤其是第一个问题,实际上都清楚地指向了某种容器对象的实用程序,该容器对象可以处理Player的初始化(即只有该容器才知道如何初始化Player并能够绑定所有可变属性)。 在下面的示例代码中,此容器(匹配)是我放置了opponent(for:Player)
方法以查询opponent(for:Player)
的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)
}
}
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.