You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Swift类继承与循环依赖下的存储属性初始化问题

现有代码实现

1. 可点击对象基类 ObjectNode

class ObjectNode: SKSpriteNode {
    let onTap: () -> ()
    let objectManager: ObjectManager

    init(onTap: @escaping () -> (), pos: CGPoint, objectManager: ObjectManager) {
        self.onTap = onTap
        self.objectManager = objectManager
        let texture = SKTexture(imageNamed: "objectImage")
        super.init(texture: texture, color: SKColor.clear, size: texture.size())
        
        isUserInteractionEnabled = true
        self.position = pos
    }

    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        if objectManager.objectTouched {
            return
        }
        onTap()
    }
}

2. 可点击对象容器类 Object

class Object: SKNode {
    let object1: ObjectNode
    let objectMenu: ObjectMenu
    let pos: CGPoint
    
    init(size: CGSize, objectTapped: @escaping () -> (), objectManager: ObjectManager, objectMenu: ObjectMenu) {
        self.objectMenu = objectMenu
        pos = CGPoint(x: size.width / 2, y: size.height / 2)
        object1 = ObjectNode(onTap: objectTapped, pos: pos, objectManager: objectManager)
        addChild(object1)
        super.init()
    }
}

3. 菜单类 ObjectMenu

class ObjectMenu: SKNode {
    let menuImage: SKSpriteNode
    let menuNode: ObjectNode
    let objectManager: ObjectManager
    
    init(size: CGSize, objectManager: ObjectManager) {
        self.objectManager = objectManager
        let texture = SKTexture(imageNamed: "menuImage")
        menuImage = SKSpriteNode(texture: texture, color: SKColor.clear, size: texture.size())
        menuImage.position = CGPoint(x: size.width / 2, y: size.height / 2)
        menuNode = ObjectNode(onTap: objectManager.objectAction, pos: menuImage.position, objectManager: objectManager)
        super.init()
    }

    func objectTapped() {
        addChild(menuImage)
        objectManager.objectTouched = true
    }
}

4. 对象管理类 ObjectManager

class ObjectManager: Object {
    var objectTouched = false

    func objectAction() {
        let object2 = ObjectNode(onTap: objectMenu.objectTapped, pos: pos, objectManager: objectMenu.objectManager)
        addChild(object2)
    }
}

遇到的问题

当前类之间存在循环强依赖:ObjectManager 继承自 Object,Object 需要 ObjectMenu,ObjectMenu 又依赖 ObjectManager,导致初始化顺序(ObjectManager → ObjectMenu → Object)无法满足所有类的初始化要求,总会出现某个类依赖的属性要在自身初始化完成后才创建的情况。

考虑过将属性设为可选类型,但存在两个疑问:

  1. 如何将无返回值闭包作为可选参数传递
  2. 如何安全解包这些可选属性

解决方案

一、打破循环强依赖:用弱引用重构类关系

循环强依赖的核心是类互相持有强引用,导致初始化死锁,同时 ObjectManager 继承自 Object 的设计混淆了“管理”和“容器”的职责,先重构这部分:

  1. 修改 ObjectMenu,将 objectManager 改为弱引用可选类型:
class ObjectMenu: SKNode {
    let menuImage: SKSpriteNode
    let menuNode: ObjectNode
    weak var objectManager: ObjectManager? // 弱引用避免循环持有
    
    init(size: CGSize, objectManager: ObjectManager?) {
        self.objectManager = objectManager
        let texture = SKTexture(imageNamed: "menuImage")
        menuImage = SKSpriteNode(texture: texture, color: SKColor.clear, size: texture.size())
        menuImage.position = CGPoint(x: size.width / 2, y: size.height / 2)
        // 安全获取管理器的action,无依赖时用空闭包兜底
        let tapAction = objectManager?.objectAction ?? {}
        menuNode = ObjectNode(onTap: tapAction, pos: menuImage.position, objectManager: objectManager!)
        super.init()
    }

    func objectTapped() {
        addChild(menuImage)
        objectManager?.objectTouched = true // 可选链调用,避免解包崩溃
    }
}
  1. 重构 ObjectManager 为独立类,不再继承 Object,用弱引用关联 ObjectMenu:
class ObjectManager: SKNode {
    var objectTouched = false
    weak var objectMenu: ObjectMenu?
    var pos: CGPoint? // 存储容器对象的位置信息

    func objectAction() {
        guard let pos = pos, let objectMenu = objectMenu else { return }
        let object2 = ObjectNode(onTap: objectMenu.objectTapped, pos: pos, objectManager: self)
        addChild(object2)
    }
}
  1. 修改 Object 类,调整初始化逻辑,传递必要的依赖:
class Object: SKNode {
    let object1: ObjectNode
    let objectMenu: ObjectMenu
    let pos: CGPoint
    
    init(size: CGSize, objectManager: ObjectManager, objectMenu: ObjectMenu) {
        self.objectMenu = objectMenu
        pos = CGPoint(x: size.width / 2, y: size.height / 2)
        // 给管理器传递位置和菜单引用
        objectManager.pos = pos
        objectManager.objectMenu = objectMenu
        // 直接传递菜单的tap方法作为闭包
        object1 = ObjectNode(onTap: objectMenu.objectTapped, pos: pos, objectManager: objectManager)
        addChild(object1)
        super.init()
    }
}

二、可选闭包的传递与安全使用

如果必须使用可选闭包,语法和使用方式如下:

  • 声明可选闭包:var onTap: (() -> ())?
  • 传递闭包:可以直接传已有的闭包或 nil
  • 安全调用:用可选链或可选绑定
// 方式1:可选链调用,无闭包时直接跳过
onTap?()

// 方式2:可选绑定+默认处理
if let tapAction = onTap {
    tapAction()
} else {
    // 无闭包时的默认逻辑
    print("No tap action set")
}

三、调整初始化顺序

在 GameScene 中按以下顺序初始化,确保每个类的依赖都已创建:

class GameScene: SKScene {
    override init(size: CGSize) {
        super.init(size: size)
        
        // 1. 先创建管理器
        let objectManager = ObjectManager()
        // 2. 创建菜单,传入管理器
        let objectMenu = ObjectMenu(size: size, objectManager: objectManager)
        // 3. 创建容器对象,传入管理器和菜单
        let object = Object(size: size, objectManager: objectManager, objectMenu: objectMenu)
        
        addChild(object)
        addChild(objectMenu)
    }
    
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

内容的提问来源于stack exchange,提问作者quickj

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.12 04:00:01