簡體   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