我正在尝试在 Swift 中构建一个树实现来表示国际象棋游戏。
游戏由一系列动作组成,但给定棋盘位置的替代动作是有效的。我想在 GUI 中遍历树,这就是为什么我添加了方法来转到树中的特定节点。
但是我正在努力获得正确的内存模型。对于我的任务,我想保留对给定节点的下一个节点及其父节点的引用。根据我的理解,这些应该是“弱”的,以便不引入保留循环。然而,这样做时我的实现崩溃了(因为我猜我没有对给定节点的引用?)。 有人可以启发我如何更改现有的实现以使其正常工作吗?当我删除weak关键字时,我的测试成功了,但是我认为这是不对的,因为再次因为可能的保留周期。
我删除了一些实现以使其更具可读性:
import Foundation
/// GameNode represents a node of a chess game tree
public final class GameNode {
// MARK: - Properties
/// The position of the node
public let position: Position
/// Uniquely identifies a node
public let nodeId: UUID
/// Is the node at the top of the tree
public let isTopNode: Bool
/// The chess move that gets from the parent node to this one
public let move: Move?
/// An optional move annotation like !!, !?, ??
public let annotation: String?
/// A comment for the move
public let comment: String?
/// The parent node
public internal(set) weak var parent: GameNode?
/// Pointer to the main variation
public internal(set) weak var next: GameNode?
/// Other possible variations from this node
public internal(set) var variations: [GameNode]
// MARK: - Init
/// Creates a root node
public init(position: Position = .initial, comment: String? = nil) {
self.position = position
self.nodeId = UUID()
self.isTopNode = true
self.move = nil
self.annotation = nil
self.parent = nil
self.comment = comment
self.next = nil
self.variations = []
}
/// Creates a node which is the result of making a move in another node
public init(position: Position, move: Move, parent: GameNode, annotation: String? = nil, comment: String? = nil) {
self.position = position
self.nodeId = UUID()
self.isTopNode = false
self.move = move
self.annotation = annotation
self.parent = parent
self.comment = comment
self.next = nil
self.variations = []
}
/// Reconstructs the move sequence from the start of the game to this point
public func reconstructMovesFromBeginning() -> [Move] {
if parent?.isTopNode == true {
return [move].compactMap({ $0 })
}
var moves = parent?.reconstructMovesFromBeginning() ?? []
if let move {
moves.append(move)
}
return moves
}
}
public final class Game {
public private(set) var current: GameNode
public init(root: GameNode = GameNode()) {
self.current = root
}
var root: GameNode? {
var tmp: GameNode? = current
while let currentTmp = tmp, !currentTmp.isTopNode {
tmp = currentTmp.parent
}
return tmp
}
public var isAtEnd: Bool {
current.next == nil
}
public func goBackward() {
guard let parent = current.parent else { return }
self.current = parent
}
public func go(to node: GameNode) {
self.current = node
}
public func play(move: Move, comment: String? = nil, annotation: String? = nil) throws {
let newPosition = try current.position.play(move: move)
let newNode = GameNode(position: newPosition, move: move, parent: current, annotation: annotation, comment: comment)
if !isAtEnd {
current.next?.variations.append(newNode)
} else {
current.next = newNode
}
go(to: newNode)
}
public var uciPath: [String] {
current.reconstructMovesFromBeginning().map(\.uci)
}
}
测试:
func testGameToUCIPath() throws {
let game = try Game(fen: "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1")
try game.play(move: .init(from: Squares.e2, to: Squares.e4))
try game.play(move: .init(from: Squares.e7, to: Squares.e5))
try game.play(move: .init(from: Squares.g1, to: Squares.f3))
try game.play(move: .init(from: Squares.b8, to: Squares.c6))
try game.play(move: .init(from: Squares.f1, to: Squares.b5))
XCTAssertEqual(game.uciPath, ["e2e4", "e7e5", "g1f3", "b8c6", "f1b5"])
game.goBackward()
XCTAssertEqual(game.uciPath, ["e2e4", "e7e5", "g1f3", "b8c6"])
try game.play(move: .init(from: Squares.f1, to: Squares.c4))
XCTAssertEqual(game.uciPath, ["e2e4", "e7e5", "g1f3", "b8c6", "f1c4"])
}
当我们将该规则应用于您的代码时,
parent
将保持较弱,而
next
节点需要较强。public final class GameNode {
...
/// The parent node
public internal(set) weak var parent: GameNode?
/// Pointer to the main variation
public internal(set) var next: GameNode?
}
但是,为了保持整个层次结构的活力,您还需要对初始
root
节点保持强引用。否则,当您重新分配
current
节点时,其父节点将被销毁,因为没有强引用将其保持活动状态。这意味着您将需要 Game
类中的另一个字段
public final class Game {
/// initial root node
public private(set) var root: GameNode
public private(set) var current: GameNode
public init(root: GameNode = GameNode()) {
self.root = root
self.current = root
}
此外,GameNode
类中的引用循环还存在另一个潜在问题。那是
variations
数组,它也保存对节点的强引用。如果您仅存储该数组中的后续节点(这就是您的代码注释所暗示的),那么它会很好,但如果您存储该特定节点或任何先前的节点,您将具有强引用循环。