4

考虑以下用例:

在某些游戏的模型中,您有一个Player类。每个Player人都有一个unowned let opponent: Player代表他们正在对抗的对手。这些总是成对创建的,并且 aPlayer必须总是有 aopponent因为它是非可选的。但是,这很难建模,因为必须先创建一个玩家,而创建第二个玩家之前,第一个玩家不会有对手!

通过一些丑陋的黑客攻击,我想出了这个解决方案:

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一个不带 a 的计算属性set,由 a 支持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. 如果不是,我对UnsafeMutablePointerSwift 中的 s 等有什么误解,导致上述代码不起作用?

4

3 回答 3

2

我认为一个隐式展开的可选是你想要的。您用感叹号 ( !) 声明它。这是对编译器的一个承诺,即使该属性可能在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) {
    // ...
}
于 2016-10-14T03:52:28.960 回答
1

在搞砸了一段时间之后,似乎您想要做的事情可能是不可能的,并且并没有真正与 Swift 合作。更重要的是,这可能是一种有缺陷的方法。

就 Swift 而言,初始化器需要在所有存储的值返回之前对其进行初始化。出于多种原因,我将不再赘述。当在初始化时无法保证/计算值时,使用可选值、IUO 和计算值。如果您不想要 Optionals、IUO 或计算值,但仍希望在初始化后取消设置某些存储的值,那么您也想吃蛋糕。

就设计而言,如果您需要将两个对象紧密链接到在初始化时需要彼此,那么您的模型(IMO)已损坏。这正是分层数据结构很好地解决的问题。在您的具体示例中,很明显您需要某种 Match 或 Competition 对象来创建和管理两个玩家之间的关系,我知道您的问题更接近“这可能”而不是“应该完成”,但我想不出这不是一个坏主意的任何情况。从根本上讲,它打破了封装。

Player 对象应该管理和跟踪 Player 对象中存在的东西,并且 Player 类中唯一托管的关系应该是它的子对象。任何兄弟关系都应由其父级访问/设置。

这成为一个更清晰的规模问题。如果你想添加第三个玩家怎么办?50个呢?然后,您必须初始化每个播放器并将其连接到其他播放器,然后才能使用任何播放器。如果您想添加或删除一个播放器,您必须同时为每个连接的播放器执行此操作,并在此操作发生时阻止任何事情发生。

另一个问题是它在任何其他情况下都无法使用。如果设计得当,玩家可以在所有类型的游戏中使用。而当前的设计只允许在 1v1 的情况下使用它。对于任何其他情况,您将不得不重新编写它,并且您的代码库会出现分歧。

总而言之,你想要的东西在 Swift 中可能是不可能的,但是如果或当它成为可能时,无论如何这几乎肯定是个坏主意:)

对这篇文章感到抱歉,希望对您有所帮助!

于 2016-10-20T23:18:36.233 回答
0

有一种方法可以在 Swift 中使用惰性属性(用于方便的 API)和包含两个播放器的容器(用于合理的内存管理)来干净地完成此操作。对于 TL;DR,请查看下面的示例代码。如需更长的答案,请继续阅读:

根据定义,两个对象之间的循环在 Swift 中本质上必须是可选的,因为:

  1. Swift 规定对象的所有字段都需要在对象的初始化程序执行时进行初始化。因此,如果您想将两个带有引用的对象绑定在一起,可选的或隐式展开的可选引用或无主引用是您的选择(两者都需要初始化,因此至少有一个在其对手之前存在)。
  2. 如果对象是类类型,那么它们应该被弱引用,而且,根据定义,弱引用本质上是可选的(自动归零和隐式或显式)。

在具有垃圾收集器的环境中,能够创建一对像您所追求的那样动态分配的对象确实更自然(Swift 使用自动引用计数,如果您的代码没有根,它只会泄漏您的一对对象)。因此,某种包含两个玩家的容器在 Swift 中很有用(如果不是绝对必要的话)。

我会争辩说,尽管语言限制阻止您在初始化时执行您正在尝试的操作,但您的模型还有其他问题,这些问题将受益于两个级别的层次结构。

  • 如果一个玩家只存在于另一个玩家的上下文中,那么您每场比赛最多只能创建两个。
  • 您可能还想为玩家定义一个顺序,例如,如果是回合制游戏来决定谁先发,或者出于演示目的将其中一个玩家定义为玩“主场”比赛等。

上述两个问题,尤其是第一个问题,确实清楚地指出了某种容器对象的实用性,它可以处理你的 Player 的初始化(即只有那个容器知道如何初始化一个 Player,并且能够绑定所有可变属性一起)。下面示例代码中的这个容器(Match)是我放置一个opponent(for:Player)方法来查询玩家对手的容器。这个方法在opponentPlayer 的惰性属性中被调用。

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)
    }
}
于 2016-10-23T12:11:44.760 回答