[英]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
實際上更改了地址!
我的問題有兩個:
是否有一種更清潔,更“快捷”的方式來創建這種相互非可選引用的結構?
如果不是,那么我對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中必須是可選的,因為:
在具有垃圾收集器的環境中,能夠創建一對動態分配的對象(如您所追求的對象)實際上更加自然(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.