Swift 中的相互非可选引用循环
Posted
技术标签:
【中文标题】Swift 中的相互非可选引用循环【英文标题】:Mutual non-optional reference cycle in Swift 【发布时间】:2016-10-13 17:05:51 【问题描述】:考虑以下用例:
在某些游戏的模型中,您有一个 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
居然换了地址!
我的问题有两个:
是否有一种更简洁、更“快捷”的方式来创建这种相互非可选引用的结构?
如果不是,我对 Swift 中的UnsafeMutablePointer
s 等有什么误解,导致上述代码不起作用?
【问题讨论】:
我认为你的模型是错误的。你有一个Match
,它必须有2个Player
s;并且每个Player
对它们所属的Match
都有一个弱引用。在该模型中,您可以将 opponent
设为计算属性
当然,模型可以重构;我很想知道 Swift 是否可以支持这种模型。更一般地,您可以想象“外部初始化”的概念,其中可以在未初始化状态下创建对象,然后在外部设置其属性,并且在正确初始化所有属性之前无法返回或使用(就像 @987654347 @ 现在行为)。我知道 Swift 在如此高的级别上不支持此功能,但我很想知道该行为是否至少可以被模拟。更改模型使其成为一个不那么有趣的问题。
特别是,该模型失败了,因为Player
s 突然不知道他们是哪个Player
——opponent
必须手动检查是self == match.p1
还是self == match.p2
。我想你可以在Match
上创建一个对手方法,这样你就可以做var opponent return match.opponent(self)
,但我仍然认为Player
s 在概念上不是一个不正确的模型,每个人都知道他们在给定的@中面对的是谁987654356@.
【参考方案1】:
我认为隐式展开的可选是你想要的。您用感叹号 (!
) 声明它。这是对编译器的一个承诺,即使该属性可能在init
调用期间被初始化,但在您使用它时它将具有有效值。将此与私有 setter 相结合,您可以实现您想要的:
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)
// ...
【讨论】:
我不想走 IUO 的路线,因为它们是 Swift 团队正在逐步淘汰的类型系统的一部分,并且它实际上只是为了与 ObjC API 兼容而存在尚未针对可空性进行注释。不过,私人二传手似乎正是正确的解决方案。您对为什么指针黑客不起作用有任何见解吗?即使使用私人二传手解决了最初的问题,我也很想知道我做错了什么! IUO 仍然存在并且不会很快消失。事实上,Interface Builder要求它将出口连接到视图控制器的属性。是的,我知道这个名称混乱的Abolish IUO type 提案。您可以在 Natasha the Robot 上找到它的含义(还有相关 WWDC 视频的链接) 对,我知道他们不会消失,但这个问题的部分动机是找出是否有一种“快速”的方式来建模——必须使用 IUO 是问题所在我试图解决。 IUO 可能不会被废除,但它们会被废除作为类型。 我同意 IUO 在这里是完全正确的。 IUO的意思是“这个值是必需的,但在正式初始化时实际上不能提供”。这就是你所处的情况,@Jumhyn。我认为 Swift 的某些部分(例如 IUO)被妖魔化了很多,但在某些时候它们确实是正确的工具。 我已经在使用 IUO 来解决这个问题。我的问题来自好奇而不是实用性。 IUO 在类型系统之外(也不允许opponent
成为常量),所以我想知道在 Swift 中是否有更“纯粹”的方式来表示它。看起来答案是否定的。【参考方案2】:
在搞砸了一段时间之后,似乎您想要做的事情可能是不可能的,并且与 Swift 不符。更重要的是,这可能是一种有缺陷的方法。
就 Swift 而言,初始化器需要在所有存储的值返回之前对其进行初始化。出于多种原因,我将不再赘述。当在初始化时无法保证/计算值时,使用可选值、IUO 和计算值。如果您不希望 Optionals、IUO 或计算值,但仍希望在初始化后取消设置某些存储的值,那么您也想吃蛋糕。
就设计而言,如果您需要将两个对象紧密链接到在初始化时需要彼此连接,那么您的模型 (IMO) 已损坏。这正是分层数据结构很好地解决的问题。在您的具体示例中,很明显您需要某种 Match 或 Competition 对象来创建和管理两名玩家之间的关系,我知道您的问题更接近“这可能”而不是“应该完成”,但我想不出这不是一个坏主意的任何情况。它从根本上打破了封装。
Player 对象应该管理和跟踪存在于 Player 对象中的事物,并且 Player 类中唯一托管的关系应该是它的子对象。任何兄弟关系都应由其父级访问/设置。
这成为一个更清晰的规模问题。如果你想添加第三个玩家怎么办? 50个呢?然后,您必须初始化每个播放器并将其连接到其他播放器,然后才能使用任何播放器。如果您想添加或删除一个播放器,您必须同时为每个连接的播放器执行此操作,并在此操作发生时阻止任何事情发生。
另一个问题是它在任何其他情况下都无法使用。如果设计得当,玩家可以在所有类型的游戏中使用。而当前的设计只允许在 1v1 的情况下使用它。对于任何其他情况,您都必须重新编写它,您的代码库会出现分歧。
总而言之,你想要的东西在 Swift 中可能是不可能的,但如果或当它成为可能时,无论如何这几乎肯定是个坏主意:)
对这篇文章感到抱歉,希望对您有所帮助!
【讨论】:
【参考方案3】:有一种方法可以在 Swift 中使用惰性属性(用于方便的 API)和包含两个播放器的容器(用于合理的内存管理)来干净地完成此操作。对于 TL;DR,请查看下面的示例代码。如需更长的答案,请继续阅读:
根据定义,两个对象之间的循环在 Swift 中本质上必须是可选的,因为:
-
Swift 规定对象的所有字段都需要在对象的初始化程序执行时进行初始化。因此,如果您想将两个带有引用的对象绑定在一起,可选的或隐式展开的可选引用或无主引用是您的选择(两者都需要初始化,因此至少有一个在其对手之前存在)。
如果对象属于类类型,那么它们应该被弱引用,而且根据定义,弱引用本质上是可选的(自动归零和隐式或显式)。
在带有垃圾收集器的环境中,能够像您所追求的那样创建一对动态分配的对象确实更加自然(Swift 使用自动引用计数,如果它从您的代码中脱离出来,它只会泄漏您的一对对象) .因此,某种包含两个玩家的容器在 Swift 中很有用(如果不是绝对必要的话)。
我认为,尽管语言限制阻止您在初始化时执行您正在尝试的操作,但您的模型还有其他问题,这些问题将受益于两个级别的层次结构。
如果一个玩家只存在于另一个玩家的上下文中,那么您在每场比赛中最多只能创建两个。 您可能还想为玩家定义一个顺序,例如,如果是回合制游戏来决定谁先发,或者为了演示目的将其中一个玩家定义为玩“主场”比赛等。上述两个问题,尤其是第一个问题,确实清楚地指出了某种容器对象的实用性,它可以处理你的玩家的初始化(即只有那个容器知道如何初始化一个玩家,并且会是能够将所有可变属性绑定在一起)。下面示例代码中的这个容器(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)
【讨论】:
以上是关于Swift 中的相互非可选引用循环的主要内容,如果未能解决你的问题,请参考以下文章
在 swift 中使用“默认值”创建非可选 Codable 的最佳方法