繁体   English   中英

Swift中的相互非可选参考周期

[英]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实际上更改了地址!

我的问题有两个:

  1. 是否有一种更清洁,更“快捷”的方式来创建这种相互非可选引用的结构?

  2. 如果不是,那么我对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中必须是可选的,因为:

  1. Swift要求对象的所有字段都需要在执行该对象的初始化程序之前进行初始化。 因此,如果要将两个对象与引用绑定在一起(可选,因此都需要初始化,因此在其对手之前至少存在一个对象),则可以使用一个可选的,或隐式展开的可选引用或一个无主引用。
  2. 如果对象属于类类型,则应弱引用它们,并且根据定义,弱引用本质上是可选的(自动清零,隐式或显式)。

在具有垃圾收集器的环境中,能够创建一对动态分配的对象(如您所追求的对象)实际上更加自然(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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM