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 中的UnsafeMutablePointers 等有什么误解,导致上述代码不起作用?

【问题讨论】:

我认为你的模型是错误的。你有一个Match,它必须有2个Players;并且每个Player 对它们所属的Match 都有一个弱引用。在该模型中,您可以将 opponent 设为计算属性 当然,模型可以重构;我很想知道 Swift 是否可以支持这种模型。更一般地,您可以想象“外部初始化”的概念,其中可以在未初始化状态下创建对象,然后在外部设置其属性,并且在正确初始化所有属性之前无法返回或使用(就像 @987654347 @ 现在行为)。我知道 Swift 在如此高的级别上不支持此功能,但我很想知道该行为是否至少可以被模拟。更改模型使其成为一个不那么有趣的问题。 特别是,该模型失败了,因为Players 突然不知道他们是哪个Player——opponent 必须手动检查是self == match.p1 还是self == match.p2。我想你可以在Match 上创建一个对手方法,这样你就可以做var opponent return match.opponent(self) ,但我仍然认为Players 在概念上不是一个不正确的模型,每个人都知道他们在给定的@中面对的是谁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 中为可选类型分配非可选值?

Swift之深入解析如何处理非可选的可选项类型

在 swift 中使用“默认值”创建非可选 Codable 的最佳方法

斯威夫特 3;用非可选字符串附加可选字符串

防止 Obj-C 代码将 `nil` 传递给具有非可选参数的 Swift 方法

Swift:在运行时在非可选中检测到意外的 nil 值:强制转换为可选失败