I am trying to build a tree implementation in Swift to represent a chess game.

游戏由一系列棋步组成,但给定棋盘位置的替代棋步有效.我想穿越我的图形用户界面中的树,这就是为什么我添加了前往树中特定 node 的方法.

However I am struggling with getting the memory model right.对于我的任务,我想保留对给定 node 的下一个 node 及其父 node 的引用.据我了解,为了不引入保留周期,这些应该是weak.然而,这样做时,我的实现就崩溃了(因为我猜我没有持有对给定 node 的引用?).

有人能告诉我如何更改现有的实现以使其正确工作吗?当我删除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"])
}

推荐答案

为了避免强引用周期,您需要在对象层次 struct 中拥有明确的所有权关系.只有所有者(父 node )应该持有对其拥有的 node 的强引用,而对父 node 的反向引用应该始终是弱引用.

当我们将该规则应用于您的代码时,parent将保持较弱,而next node 需要较强.

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?
}

但是,为了保持整个层次 struct 的活力,您还需要对最初的root个 node 进行强引用.否则,当您重新分配current node 时,其父 node 将被销毁,因为没有强引用将其保留.

这意味着您将需要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数组,它也包含对 node 的强引用.如果您仅存储该数组中的后续 node (这是您的代码注释所暗示的),那么这将很好,但如果您存储该特定 node 或任何之前的 node ,您将具有强引用循环.

Swift相关问答推荐

防止NavigationLink自行返回导航视图

字符串目录不适用于SWIFT包

SwiftUI正在初始化不带状态变量的绑定变量

编写Swift字符串的属性包装时遇到问题

Swift图表';chartXVisibleDomain挂起或崩溃

Swift UI中视图上的值未更新

macOS 的窗口框架中使用的真实类型和真实类型是什么?

从文字创建数组时的 Swift 宏

swift 是否遇到 Java 的 2gb 最大序列化大小问题?

RxSwift 中 Single.just(...) 和 Observable.just(...) 之间有区别吗?

try 制作一个 Swift 扩展来替换字符串中的多次出现

Swift-如何接受多个(联合)类型作为参数

Swift 有没有办法在不使用 switch 语句的情况下获取关联值?

如何更改 Picker 的边框 colored颜色

使 Swift 并发中的任务串行运行

在 Swift 中获取双精度的小数部分

UITableViewAlertForLayoutOutsideViewHierarchy 错误:仅警告一次(iOS 13 GM)

如何在Swift中找出字母是字母数字还是数字

Swift 2.0 中的 do { } catch 不处理从这里抛出的错误

iOS 视图可见性消失了