简体   繁体   English

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

[英]Mutual non-optional reference cycle in Swift

Consider the following use case: 考虑以下用例:

In a model for some game, you have a Player class. 在某些游戏的模型中,您有一个Player类。 Each Player has an unowned let opponent: Player which represents the opponent they are playing against. 每个Player都有一个unowned let opponent: Player代表他们与之对抗的对手的玩家。 These are always created in pairs, and a Player must always have an opponent since it is non-optional. 这些始终是成对创建的,并且Player必须始终有一个opponent因为它是非可选的。 However, this is very difficult to model, since one player must be created before the other, and the first player will not have an opponent until after the second one is created! 但是,这很难建模,因为必须先创建一个玩家,而在创建第二个玩家之前,第一个玩家将没有对手!

Through some ugly hacking, I came up with this solution: 通过一些丑陋的黑客攻击,我想到了以下解决方案:

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)

Which works quite well. 哪个效果很好。 However, I am having trouble turning opponent into a constant. 但是,我很难将opponent变成一个常数。 One solution is to make opponent a computed property without a set , backed by a private var , but I'd like to avoid this. 一种解决方案是使opponent成为不带set的计算属性,并由private var后盾,但我想避免这种情况。

I attempted to do some hacking with Swift pointers, and came up with: 我试图用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)
}

But this gives a segfault. 但这会造成段错误。 Furthermore, when debugging with lldb , we can see that just after p1 is initialized, we have: 此外,在使用lldb进行调试时,我们可以看到在初始化p1之后,我们有:

(lldb) p p1
(Player2.Player) $R3 = 0x0000000101004390 {
  opponent = 0x0000000100702940 {
    opponent = <uninitialized>
    name = ""
  }
  name = "P1FAKE"
}

But at the end of the function, lldb shows this: 但是在函数结尾,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"
}

So p1 correctly points to p2 , but p2 still points to the old p1 . 因此p1正确指向p2 ,但是p2仍然指向旧p1 What's more, p1 has actually changed addresses! 而且, p1实际上更改了地址!

My question is two-fold: 我的问题有两个:

  1. Is there a cleaner, more 'Swifty' way to create this structure of mutual non-optional references? 是否有一种更清洁,更“快捷”的方式来创建这种相互非可选引用的结构?

  2. If not, what am I misunderstanding about UnsafeMutablePointer s and the like in Swift that makes the above code not work? 如果不是,那么我对Swift中的UnsafeMutablePointer以及类似的东西会产生什么误解,导致上述代码无法正常工作?

I think an implicitly unwrapped optional is what you want. 我认为您想要一个隐式展开的可选内容。 You declare it with an exclamation mark ( ! ). 您用感叹号( ! )声明它。 It's a promise to the compiler that even though the property may be initialized during the init call, it will have a valid value when you use it. 对编译器的保证是,即使可以在init调用期间初始化该属性,使用时它也将具有有效值。 Combining this with a private setter, you can achieve what you want: 将此功能与专用设置器结合使用,即可实现所需的功能:

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

Since the setter is private the compiler will throw an error if you try to change it: 由于setter是私有的,因此如果您尝试更改它,编译器将抛出错误:

let p3 = Player(name: "Player Three")
p1.opponent = p3 // Error: Cannot assign to property: 'opponent' setter is inaccessible

Note that since you intended for getPair to be the single method to create a Player instance, you may as well set the init call to private since it doesn't set the opponent property: 请注意,由于您打算将getPair创建Player实例的单一方法,因此您最好将init调用设置为private,因为它没有设置opponent属性:

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

After messing with this for a while it seems what you're wanting to do is likely not possible, and doesn't really jive with Swift. 弄乱了一段时间后,似乎您想做的事情似乎不可能实现,并且与Swift并没有真正的契合。 More importantly, it's likely a flawed approach to begin with. 更重要的是,这可能是一个有缺陷的方法。

As far as Swift goes, initializers are required to initialize all stored values before they return. 就Swift而言,初始化器需要在所有存储的值返回之前对其进行初始化。 This is for a number of reasons I won't go into. 这是出于多种原因,我不打算讨论。 Optionals, IUOs, and computed values are used when a value cannot be guaranteed/calculated at initialization. 当在初始化时不能保证/计算某个值时,将使用可选值,IUO和计算值。 If you don't want Optionals, IUOs or computed values but still want some stored values to be unset after init, you're wanting to have your cake and eat it too. 如果您不希望使用Optional,IUU或计算值,但仍希望在初始化后取消某些存储值的设置,那么您也想吃点蛋糕。

As far as the design, if you need two objects to be linked so tightly as to require each other at initialization, your model is (IMO) broken. 就设计而言,如果您需要紧密链接两个对象以在初始化时互相要求,则模型(IMO)会损坏。 This is the exact problem hierarchical data structures solve so well. 这是分层数据结构很好解决的确切问题。 In your specific example, it seems clear you need some sort of Match or Competition object that creates and manages the relationship between the two players, I know your question is closer to "is this possible" not "should it be done", but I can't think of any situation where this isn't a bad idea. 在您的特定示例中,您似乎显然需要某种“比赛”或“竞赛”对象来创建和管理两个玩家之间的关系,我知道您的问题更接近于“这是否可能”而不是“应该做到”,但是我想不到这不是一个坏主意的任何情况。 Fundamentally it breaks encapsulation. 从根本上说,它破坏了封装。

The Player object should manage and track the things that exist within a Player object, and the only managed relationships within the Player class should be with it's children. Player对象应该管理和跟踪Player对象中存在的事物,并且Player类中唯一受管理的关系应该与其子对象有关。 Any sibling relationship should be accessed/set by it's parent. 任何同级关系都应由其父级访问/设置。

This becomes a clearer problem with scale. 这成为规模上更明显的问题。 What if you want to add a third player? 如果要添加第三名玩家怎么办? what about 50? 那50呢? You will then have to initialize and connect every single player to ever other before you can use any of the players. 然后,您将必须初始化每个播放器并将其彼此连接,然后才能使用任何播放器。 If you want to add or remove a player, you will have to do it for every single connected player simultaneously, and block anything from happening while that takes place. 如果要添加或删除播放器,则必须同时为每个连接的播放器执行此操作,并阻止发生该情况的任何事情。

Another issues is it makes it unusable in any other situation. 另一个问题是它使它在任何其他情况下都无法使用。 If designed properly, a Player could be used in all types of games. 如果设计合理,则可以在所有类型的游戏中使用播放器。 Whereas the current design allowed it to be used only in a 1v1 situation. 而当前的设计只允许在1v1的情况下使用它。 For any other case you would have to re-write it and your code base would diverge. 在任何其他情况下,您都必须重新编写它,并且代码库会有所不同。

In summation, what you want is probably not possible in Swift, but if or when it becomes possible, it's almost certainly a bad idea anyway :) 总而言之,您想要的东西可能在Swift中是不可能的,但是如果或当它成为可能时,无论如何这几乎肯定是个坏主意:)

Sorry for the essay, hope you find it helpful! 对不起这篇文章,希望对您有所帮助!

There is a way to do this cleanly in Swift using lazy properties (for a convenient API) and a container that contains both players (for sane memory management). 在Swift中,有一种方法可以使用惰性属性(用于方便的API)和包含两个播放器的容器(用于合理的内存管理)来干净地执行此操作。 For a TL;DR, take a look at the example code below. 对于TL; DR,请看下面的示例代码。 For a longer answer, read on: 要获得更长的答案,请继续阅读:

By definition, a cycle between two objects must be optional in nature in Swift because: 根据定义,两个对象之间的循环本质上在Swift中必须是可选的,因为:

  1. Swift dictates that all fields of an object need to be initialized by the time the the object's initializer has executed. Swift要求对象的所有字段都需要在执行该对象的初始化程序之前进行初始化。 So, an optional, or an implicitly unwrapped optional reference, or an unowned reference are your options if you want to tie together two objects with references (both need initialization, so at least one exists before its opponent). 因此,如果要将两个对象与引用绑定在一起(可选,因此都需要初始化,因此在其对手之前至少存在一个对象),则可以使用一个可选的,或隐式展开的可选引用或一个无主引用。
  2. If the objects are of a class type, then they should be weakly referenced, and again, weak references are by definition optional in nature (either automatically zeroed and implicit or explicit). 如果对象属于类类型,则应弱引用它们,并且根据定义,弱引用本质上是可选的(自动清零,隐式或显式)。

Being able to create a pair of dynamically allocated objects like what you are after is really more natural in an environment with a garbage collector (Swift uses automated reference counting which simply leaks your pair of objects if it goes unrooted from your code). 在具有垃圾收集器的环境中,能够创建一对动态分配的对象(如您所追求的对象)实际上更加自然(Swift使用自动引用计数,如果它从代码中脱颖而出,只会泄漏该对对象)。 Some kind of container that contains both players is therefore useful (if not absolutely necessary) in Swift. 因此,包含两个玩家的某种容器在Swift中很有用(如果不是绝对必要的话)。

I would argue that even despite the language limitations which prevent you from doing what you are trying on initialization time, your model has other problems which would benefit from a hierarchy of two levels. 我会争辩说,尽管语言限制使您无法在初始化时做任何事情,但是您的模型还有其他问题,可以从两个层次的层次结构中受益。

  • If a player only ever exists in context of another player, you should only ever be able to create a maximum of two of them per match. 如果一个玩家仅在另一个玩家的上下文中存在,则每次比赛最多只能创建两个。
  • You may want to also define an order to the players, for instance to decide who starts if it is a turn based game, or to define for presentation purposes one of the players as playing a "home" match, etc. 您可能还需要为玩家定义一个顺序,例如,确定谁是回合制游戏的开始者,或者出于演示目的将其中一位玩家定义为进行“主场”比赛等。

Both of the above concerns, especially the first, is really pointing clearly to the utility of some kind of container object that would handle the initialization of your Players (ie only that container would know how to initialize a Player, and would be able to bind all the mutable properties together). 以上两个问题,尤其是第一个问题,实际上都清楚地指向了某种容器对象的实用程序,该容器对象可以处理Player的初始化(即只有该容器才知道如何初始化Player并能够绑定所有可变属性)。 This container (Match) in the below example code is one where I placed an opponent(for:Player) method to query for the opponent for a player. 在下面的示例代码中,此容器(匹配)是我放置了opponent(for:Player)方法以查询opponent(for:Player)opponent(for:Player)容器。 This method gets called in the lazy opponent property of 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