Swift

时间:2016-10-13 17:05:51

标签: swift macos unsafemutablepointer

考虑以下用例:

在某个游戏的模型中,您有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实际上已经改变了地址!

我的问题是双重的:

  1. 是否有更清洁,更多&#39; Swifty&#39;如何创建这种相互非可选引用的结构?

  2. 如果没有,我对Swift中的UnsafeMutablePointer之类的东西有什么误解,导致上述代码不起作用?

3 个答案:

答案 0 :(得分:2)

我认为隐式展开的可选就是你想要的。你用感叹号(!)声明它。这是对编译器的承诺,即使该属性可能在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调用设为私有,因为它未设置opponent财产:

private init(name: String) {
    // ...
}

答案 1 :(得分:1)

在弄乱这一段时间之后,似乎你想要做的事情可能是不可能的,并且与Swift并没有真正相同。更重要的是,它可能是一个有缺陷的方法。

就Swift而言,初始化程序需要在返回之前初始化所有存储的值。这是出于我未能进入的多种原因。当初始化时无法保证/计算值时,将使用选项,IUO和计算值。如果您不想要Optionals,IUO或计算值,但仍希望在初始化后取消设置一些存储的值,那么您也想要吃蛋糕并吃掉它。

就设计而言,如果您需要将两个对象链接得如此紧密以至于在初始化时需要彼此,则您的模型(IMO)会被破坏。这是分层数据结构解决得很好的确切问题。在您的具体示例中,您似乎需要某种匹配或竞争对象来创建和管理两个玩家之间的关系,我知道您的问题更接近&#34;这是否可能&#34;不是&#34;它应该完成&#34;但是我无法想到这不是一个坏主意的任何情况。从根本上说它破坏了封装。

Player对象应该管理和跟踪Player对象中存在的内容,而Player类中唯一的托管关系应该是它的子对象。任何兄弟关系都应由其父母访问/设定。

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

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

总之,你想要的东西在Swift中可能是不可能的,但是如果它成为可能的话,它几乎肯定是一个坏主意:)

对于这篇文章感到抱歉,希望你觉得它有用!

答案 2 :(得分:0)

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

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

  1. Swift规定对象的所有字段都需要在对象的初始化程序执行时初始化。因此,如果要将两个对象与引用绑定在一起(两者都需要初始化,因此至少有一个对象存在于其对手之前),可选的,或隐式展开的可选引用或无主引用是您的选项。
  2. 如果对象是类类型,那么它们应该被弱引用,同样,弱引用本质上是可选的(自动归零,隐式或显式)。
  3. 在具有垃圾收集器的环境中能够创建一对动态分配的对象,就像你所追求的一样真的更自然(Swift使用自动引用计数,如果它从你的代码中无根,就会泄漏你的对象) 。因此,某种包含两个玩家的容器在Swift中是有用的(如果不是绝对必要的话)。

    我认为即使语言限制阻止你在初始化时间做你正在尝试的事情,你的模型还有其他问题可以从两个层次的层次结构中受益。

    • 如果玩家只存在于另一个玩家的上下文中,那么每场比赛你应该只能创建两个玩家。
    • 您可能还希望为玩家定义一个订单,例如,如果它是一个回合制游戏,则决定谁开始,或者为演示目的定义其中一个玩家作为“主场”比赛等。< / LI>

    上述两个问题,尤其是第一个问题,都清楚地指出了某种容器对象的实用程序,它可以处理玩家的初始化(即只有那个容器会知道如何初始化一个Player,并且会能够将所有可变属性绑定在一起)。以下示例代码中的此容器(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)
        }
    }