首页 > 解决方案 > 子类是否从 Swift 中的超类扩展继承便利初始化程序?

问题描述

Actor我试图对来自iPad Swift Playgounds 中Learn to Code 2的类型进行扩展,如下所示:

let theWorld = world

extension Actor {
    convenience init(_ x:Int, _ y:Int, facing direction: Direction = .east){
        self.init()
        theWorld.place(self, facing: direction, atColumn: x, row: y)
    }
}

当我打电话时:

Actor(4,1)

操场按预期运行。

但是当我打电话时:

// Expert is a subclass of Actor
Expert(4,1)

出现编译器错误。为什么会这样?

截屏: 操场截图

这个游乐场背后的代码库(由 Apple 提供)非常大,所以我只列出最相关的源代码(ActorExpert类型),如下所示:

演员类型

//
//  Actor.swift
//  
//  Copyright © 2016-2019 Apple Inc. All rights reserved.
//

import SceneKit
import PlaygroundSupport

/**
 The type representing the different characters that can be present and move about in the world.
 - localizationKey: Actor
*/
public class Actor: Item, NodeConstructible {
    // MARK: Static

    static var commandSpeed: Float = WorldConfiguration.Actor.idleSpeed

    public static let identifier: WorldNodeIdentifier = .actor

    // MARK: Item

    public var id = Identifier.undefined

    public let node: NodeWrapper

    public weak var world: GridWorld? {
        didSet {
            if let world = world {
                // Create a new `WorldActionComponent` when setting the world.
                addComponent(WorldActionComponent(actor: self, world: world))
                if !scnNode.childNodes.isEmpty {
                    // Start idling when placed in the world (if the geometry is loaded).
                    idleQueue.start(breathingOnly: true)
                }
            }
            else {
                removeComponent(ofType: WorldActionComponent.self)
                stopContinuousIdle()
            }
        }
    }

    // MARK: Actor Properties

    var type: ActorType

    /// The action that is currently running, if any.
    fileprivate(set) var currentAction: Action?

    var components = [ActorComponent]()

    var result: PerformerResult?

    /// Indicates if the actor has components currently running.
    var isRunning: Bool {
        return runningComponents.isEmpty == false
    }

    /// A flag to indicate if this character is currently being presented in the character picker. 
    var isInCharacterPicker = false

    /// A driver which causes the actor to idle indefinitely.
    lazy var idleQueue: ContinuousIdleQueue = ContinuousIdleQueue(actor: self)

    var isIdle: Bool {
        return idleQueue.isRunning
    }

    lazy var actorCamera: SCNNode = {
        // Programmatically add an actor camera.
        let actorCamera = SCNNode()
        actorCamera.position = SCNVector3Make(0, 0.785, 3.25)
        actorCamera.eulerAngles.x = -0.1530727
        actorCamera.camera = SCNCamera()
        actorCamera.name = "actorCamera"
        self.scnNode.addChildNode(actorCamera)

        return actorCamera
    }()

    // Private properties

    /// Used to synchronously access `runningComponents`.
    private let componentsQueue = DispatchQueue(label: "com.LTC.RunningComponents")
    private var _runningComponents = [ActorComponent]()

    /// Returns the components that are still running for this actor.
    /// Note: This synchronously blocks for access to the underlying array.
    fileprivate var runningComponents: [ActorComponent] {
        get {
            var runningComponents: [ActorComponent]!
            componentsQueue.sync { runningComponents = _runningComponents }
            return runningComponents
        }

        set {
            componentsQueue.sync { _runningComponents = newValue }
        }
    }

    // MARK: Initialization

    /// Creates a character (or actor) with the specified name. The character can then be placed in the world.
    ///
    /// Example usage:
    /// ````
    /// let blu = Character(name: .blu)
    /// ````
    ///
    /// - Parameter name: The name of the character chosen from the `CharacterName` enumeration. If you leave out `name`, the saved character will be used.
    ///
    /// - localizationKey: Actor(name:)
    public init(name: CharacterName? = nil) {
        self.type = name?.type ?? ActorType.loadDefault()
        node = NodeWrapper(identifier: .actor)

        commonInit()
    }

    public required init?(node: SCNNode) {
        guard node.identifier == .actor
            && node.identifierComponents.count >= 2 else { return nil }
        guard let type = ActorType(rawValue: node.identifierComponents[1]) else { return nil }
        self.type = type
        self.node = NodeWrapper(node)

        commonInit()
    }

    func commonInit() {
        addComponent(AnimationComponent(actor: self))

        // Check if audio is enabled.
        if Persisted.areSoundEffectsEnabled {
            addComponent(AudioComponent(actor: self))
        }

        scnNode.categoryBitMask = WorldConfiguration.characterLightBitMask
    }

    // MARK: Components

    /// Adds an ActorComponent to this Actor.
    /// There may only be one component instance for each component type.
    func addComponent<T: ActorComponent>(_ component: T) {
        // Remove any component of the same type.
        removeComponent(ofType: T.self)

        components.append(component)
    }

    func component<T : ActorComponent>(ofType componentType: T.Type) -> T? {
        for component in components where component is T {
            return component as? T
        }
        return nil
    }

    @discardableResult
    func removeComponent<T : ActorComponent>(ofType componentType: T.Type) -> T? {
        guard let index = components.firstIndex(where: { $0 is T }) else { return nil }

        let component = components.remove(at: index) as? T

        // If a current action is running, make sure this component gets cancelled.
        if let action = currentAction {
            component?.cancel(action)
        }

        return component
    }

    // MARK: Geometry

    public func loadGeometry() {
        guard scnNode.childNodes.isEmpty else { return }
        scnNode.addChildNode(type.createNode())

        if #available(iOS 13.0, *) {
            updateAppearance(traitCollection: UITraitCollection.current)
        }
    }

    public func updateAppearance(traitCollection: UITraitCollection) {
        guard #available(iOS 13.0, *) else { return }

        if let geoBody = scnNode.childNode(withName: "geoBody", recursively: true) {
            if let mat = geoBody.firstGeometry?.firstMaterial {
                mat.selfIllumination.updateAppearance(traitCollection: traitCollection)
            }
        }
    }

    // MARK: Creating Commands

    /// Convenience to create an `Command` by bundling in `self` with the provided action.
    func add(action: Action) {
        guard let world = world else { return }
        let command = Command(performer: self, action: action)
        world.commandQueue.append(command, applyingState: true)
    }

    func animationCommands(_ types: [EventGroup]) -> [Command] {
        return types.map { animationCommand($0) }
    }

    func animationCommand(_ group: EventGroup, variation: Int? = nil) -> Command {
        let highPriority = group == .victory || group == .celebration
        return Command(performer: self, action: .run(group, variation: variation), isHighPriorityCommand: highPriority)
    }

    // MARK: Animation

    /// Starts running the continuous idle. Reset by asking the scene to run.
    func startContinuousIdle() {
        guard !idleQueue.isRunning || idleQueue.isBreathingOnly else { return }

        idleQueue.start(initialAnimations: [.idle])
    }

    func stopContinuousIdle() {
        idleQueue.stop()
    }

    /// Stops the continuous idle,
    /// clears the SCNNode for the actor of actions and animations,
    /// and cancels the running action.
    public func reset() {
        // Stop any upcoming idle animations.
        stopContinuousIdle()

        // Make sure all animations are cleared. 
        let animation = component(ofType: AnimationComponent.self)
        animation?.removeAnimations()
        scnNode.removeAllActions()

        // Cancel any `currentAction` that is running.
        guard let action = currentAction else { return }
        cancel(action)
        currentAction = nil
    }

    // MARK: CharacterPicker Swap

    func swap(with actor: Actor) {
        actor.scnNode.removeAllAnimations()
        actor.scnNode.removeAllActions()

        for child in scnNode.childNodes { child.removeFromParentNode() }
        for child in actor.scnNode.childNodes { scnNode.addChildNode(child) }

        type = actor.type
    }

    // MARK: Jump

    /**
     Instructs the character to jump forward and either up or down one block. 

     If the block the character is facing is one block higher than the block the character is standing on, the character will jump on top of it.
     If the block the character is facing is one block lower than the block the character is standing on, the character will jump down to that block.
     - localizationKey: Actor.jump()
     */
    @discardableResult
    public func jump() -> Coordinate {
        return _jump()
    }
}

extension Actor: Equatable{}

public func ==(lhs: Actor, rhs: Actor) -> Bool {
    return lhs.type == rhs.type && lhs.node === rhs.node
}

extension Actor {
    // MARK: Performer

    func applyStateChange(for action: Action) {
        for performer in components {
            performer.applyStateChange(for: action)
        }
    }

    /// Cycles through the actors components allowing each component to respond to the action.
    func perform(_ action: Action) -> PerformerResult {
        // Clear the `currentAction`.
        if isRunning, let currentAction = currentAction {
            cancel(currentAction)
        }
        currentAction = nil
        runningComponents.removeAll()

        // If the `currentCommand` is running for this actor, ensure the `idleQueue` is stopped.
        let command = world?.commandQueue.currentCommand
        if command?.performer === self {
            stopContinuousIdle()
        }

        // Not all commands apply to the actor, return immediately if there is no action.
        guard let event = action.event else {
            fatalError("The actor has been asked to perform \(action), but there is no valid event associated with this action.")
        }
        currentAction = action

        // Mark all the components as running.
        runningComponents = components

        let index = action.variationIndex
        for component in components {
            let componentResult = component.perform(event: event, variation: index)
            componentResult.completionHandler = { [weak self] performer in
                self?.performerFinished(performer)
            }
        }

        result = PerformerResult(self, isAsynchronous: isRunning)
        return result!
    }

    /// Performs the event only as an animation.
    func perform(event: EventGroup, variation: Int? = nil) -> PerformerResult {
        return perform(.run(event, variation: variation))
    }

    /// Cancels all components.
    func cancel(_ action: Action) {
        result = nil

        // A lot of components don’t hold as running, but need to be reset with cancel.
        for performer in components {
            performer.cancel(action)
        }

        currentAction = nil
    }

    // MARK: Performer Finished

    func performerFinished(_ performer: Performer) {
        // Match the finished performer with the remaining `runningComponents`.
        guard let index = runningComponents.firstIndex(where: { $0 === performer }) else {
            log(message: "\(performer) reported it was finished, but does not belong to \(self)")
            return
        }
        runningComponents.remove(at: index)

        if !isRunning {
            currentAction = nil
            result?.complete()

            let nextCommand = world?.commandQueue.pendingCommands.first
            if !isIdle, nextCommand?.performer !== self {
                // Enter a neutral idle while waiting for the next command.
                idleQueue.start(breathingOnly: true)
            }
        }
    }
}

extension Actor {
    // MARK: Movement Commands

    /**
     Moves the character forward by a certain number of tiles, as determined by the `distance` parameter value.

     Example usage:
     ````
     move(distance: 3)
     // Moves forward three tiles.
     ````

     - parameters:
        - distance: Takes an `Int` value specifying the number of times to call `moveForward()`.
     - localizationKey: Actor.move(distance:)
     */
    public func move(distance: Int) {
        for _ in 1 ... distance {
            moveForward()
        }
    }

    /**
     Moves the character forward one tile.
     - localizationKey: Actor.moveForward()
     */
    @discardableResult
    public func moveForward() -> Coordinate {
        guard let world = world else { return coordinate }
        let movementObstacle = world.movementObstacle(heading: heading, from: position)

        guard movementObstacle.isEmpty else {
            if movementObstacle.isCollisionHazard {
                add(action: .fail(.intoWall))
            }
            else {
                assert(movementObstacle.isFallingHazard, "Unhandled movement case: \(movementObstacle)")
                add(action: .fail(.offEdge))
            }
            return coordinate
        }

        let nextCoordinate = nextCoordinateInCurrentDirection

        // Check for stairs.
        let yDisplacement = position.y + heightDisplacementMoving(to: nextCoordinate)
        let point = nextCoordinate.position

        let destination = SCNVector3Make(point.x, yDisplacement, point.z)
        let displacement = Displacement(from: position, to: destination)
        add(action: .move(displacement, type: .walk))

        // Check for portals.
        addCommandForPortal(at: nextCoordinate)

        return nextCoordinate
    }

    // `_jump()` included only for organizational purposes
    // (`jump()` is overridden by Expert).
    fileprivate func _jump() -> Coordinate {
        guard let world = world else { return coordinate }
        let movementResult = world.movementObstacle(heading: heading, from: position)

        let nextCoordinate = nextCoordinateInCurrentDirection
        let deltaY = heightDisplacementMoving(to: nextCoordinate)

        // Determine if the y displacement is small enough such that the character can
        // cover it with a jump.
        let toleranceY = abs(deltaY) - WorldConfiguration.heightTolerance
        let isJumpableDisplacement = toleranceY < WorldConfiguration.levelHeight

        switch movementResult {
        case [],
             [.raisedTile] where isJumpableDisplacement,
             [.loweredTile] where isJumpableDisplacement:

            let point = nextCoordinate.position
            let destination = SCNVector3Make(point.x, position.y + deltaY, point.z)
            let displacement = Displacement(from: position, to: destination)
            add(action: .move(displacement, type: .jump))

            // Check for portals.
            addCommandForPortal(at: nextCoordinate)

            return nextCoordinate
        default:
            // Error cases evaluate to the same result as `moveForward()`.
            return moveForward()
        }
    }

    /**
     Turns the character left.
     - localizationKey: Actor.turnLeft()
     */
    public func turnLeft() {
        turnBy(90)
    }

    /**
     Turns the character right.
     - localizationKey: Actor.turnRight()
     */
    public func turnRight() {
        turnBy(-90)
    }

    // MARK: Movement Helpers

    /// Creates a new command if a portal exists at the specified coordinate.
    private func addCommandForPortal(at coordinate: Coordinate) {
        let portal = world?.existingItem(ofType: Portal.self, at: coordinate)
        if let destinationPortal = portal?.linkedPortal, portal!.isActive {
            let displacement = Displacement(from: position, to: destinationPortal.position)

            add(action: .move(displacement, type: .teleport))
        }
    }

    /**
     Rotates the actor by `degrees` around the y-axis.

     - turnLeft() = 90
     - turnRight() = -90/ 270
     */
    @discardableResult
    private func turnBy(_ degrees: Int) -> SCNFloat {
        // Convert degrees to radians.
        let nextDirection = (rotation + degrees.toRadians).truncatingRemainder(dividingBy: 2 * π)

        let currentDir = Direction(radians: rotation)
        let nextDir = Direction(radians: nextDirection)

        let clockwise = currentDir.angle(to: nextDir) < 0
        let displacement = Displacement(from: rotation, to: nextDirection)
        add(action: .turn(displacement, clockwise: clockwise))

        return nextDirection
    }

    /// Returns the next coordinate moving forward 1 tile in the actors `currentDirection`.
    var nextCoordinateInCurrentDirection: Coordinate {
        return coordinateInCurrentDirection(displacement: 1)
    }

    func coordinateInCurrentDirection(displacement: Int) -> Coordinate {
        let heading = Direction(radians: rotation)
        let coordinate = Coordinate(position)

        return coordinate.advanced(by: displacement, inDirection: heading)
    }

    func heightDisplacementMoving(to coordinate: Coordinate) -> SCNFloat {
        guard let world = world else { return 0 }
        let startHeight = position.y
        let endHeight = world.nodeHeight(at: coordinate)

        return endHeight - startHeight
    }
}

extension Actor {
    // MARK: Item Commands

    /**
     Instructs the character to collect a gem on the current tile.
     - localizationKey: Actor.collectGem()
     */
    @discardableResult
    public func collectGem() -> Bool {
        guard let item = world?.existingGems(at: [coordinate]).first else {
            add(action: .fail(.missingGem))
            return false
        }

        add(action: .remove([item.id]))
        return true
    }

    /**
     Instructs the character to toggle a switch on the current tile.
     - localizationKey: Actor.toggleSwitch()
     */
    @discardableResult
    public func toggleSwitch() -> Bool {
        guard let switchNode = world?.existingItem(ofType: Switch.self, at: coordinate) else {
            add(action: .fail(.missingSwitch))
            return false
        }

        // Toggle switch to the opposite of it’s original value.
        let oldValue = switchNode.isOn
        let cont = Controller(identifier: switchNode.id, kind: .toggle, state: !oldValue)
        add(action: .control(cont))

        return true
    }
}

extension Actor {
    // MARK: Boolean Commands

    /**
     Condition that checks whether the character is on a tile with a gem on it.
     - localizationKey: Actor.isBlocked
     */
    public var isBlocked: Bool {
        guard let world = world else { return false }
        return !world.isValidActorTranslation(heading: heading, from: position)
    }

    /**
     Condition that checks whether the character is blocked on the left.
     - localizationKey: Actor.isBlockedLeft
     */
    public var isBlockedLeft: Bool {
        return isBlocked(heading: .west)
    }

    /**
     Condition that checks whether the character is blocked on the right.
     - localizationKey: Actor.isBlockedRight
     */
    public var isBlockedRight: Bool {
        return isBlocked(heading: .east)
    }

    func isBlocked(heading: Direction) -> Bool {
        guard let world = world else { return false }
        let blockedCheckDir = Direction(radians: rotation - heading.radians)

        return !world.isValidActorTranslation(heading: blockedCheckDir, from: position)
    }

    // MARK: isOn

    /**
     Condition that checks whether the character is currently on a tile with that contains a WorldNode of a specific type.
     - localizationKey: Actor.isOnItem
     */
    public func isOnItem<Node: Item>(ofType type: Node.Type) -> Bool {
        return itemAtCurrentPosition(ofType: type) != nil
    }

    /**
     Condition that checks whether the character is on a tile with a gem on it.
     - localizationKey: Actor.isOnGem
     */
    public var isOnGem: Bool {
        return isOnItem(ofType: Gem.self)
    }

    /**
     Condition that checks whether the character is on a tile with an open switch on it.
     - localizationKey: Actor.isOnOpenSwitch
     */
    public var isOnOpenSwitch: Bool {
        if let switchNode = itemAtCurrentPosition(ofType: Switch.self) {
            return switchNode.isOn
        }
        return false
    }

    /**
    Condition that checks whether the character is on a tile with a closed switch on it.
     - localizationKey: Actor.isOnClosedSwitch
     */
    public var isOnClosedSwitch: Bool {
        if let switchNode = itemAtCurrentPosition(ofType: Switch.self) {
            return !switchNode.isOn
        }
        return false
    }

    func itemAtCurrentPosition<T: Item>(ofType type: T.Type) -> T?  {
        guard let world = world else { return nil }
        return world.existingItem(ofType: type, at: coordinate)
    }
}

extension Actor {
    // MARK: Dance Actions

    /**
     Causes the character to bust out a fancy move.
     - localizationKey: Actor.danceLikeNoOneIsWatching()
     */
    public func danceLikeNoOneIsWatching() {
        add(action: .run(.victory, variation: 1))
    }

    /**
     The character receives a burst of energy, turning it up by several notches.
     - localizationKey: Actor.turnUp()
     */
    public func turnUp() {
        add(action: .run(.victory, variation: 0))
    }

    /**
     The character starts to feel real funky, breaking it down for all to witness.
     - localizationKey: Actor.breakItDown()
     */
    public func breakItDown() {
        add(action: .run(.celebration, variation: nil))
    }


    // MARK: Defeat Actions

    /**
     The character feels a bit bummed.
     - localizationKey: Actor.grumbleGrumble()
     */
    public func grumbleGrumble() {
        add(action: .run(.defeat, variation: 0))
    }

    /**
     The character feels a wave of horror.
     - localizationKey: Actor.argh()
     */
    public func argh() {
        add(action: .run(.defeat, variation: 1))
    }

    /**
     The character performs a head scratch.
     - localizationKey: Actor.headScratch()
     */
    public func headScratch() {
        add(action: .run(.defeat, variation: 2))
    }

    // MARK: Misc Actions

    /**
     The character hits a metaphorical wall.
     - localizationKey: Actor.bumpIntoWall()
     */
    public func bumpIntoWall() {
        add(action: .run(.bumpIntoWall, variation: nil))
    }
}

extension Actor: MessageConstructor {
    // MARK: MessageConstructor

    var message: PlaygroundValue {
        return .array(baseMessage + stateInfo)
    }

    var stateInfo: [PlaygroundValue] {
        return [.string(type.rawValue)]
    }
}

专家型

//
//  Expert.swift
//  
//  Copyright © 2016-2019 Apple Inc. All rights reserved.
//

import SceneKit

/**
 The type representing the expert; capable of turning locks.
 - localizationKey: Expert
 */
public final class Expert: Actor {

    public convenience init() {
        let node = WorldNodeIdentifier.actor.makeNode()
        node.name! += "-" + ActorType.expert.rawValue
        self.init(node: node)!
    }

    public required init?(node: SCNNode) {
        super.init(node: node)

        self.node.isModular = true
    }

    @available(*, unavailable, message:"The Expert can't jump . Only the Character type can use the `jump()` method.")
    @discardableResult
    public override func jump() -> Coordinate {
        return coordinate
    }

    public override func loadGeometry() {
        guard scnNode.childNodes.isEmpty else { return }
        scnNode.addChildNode(ActorType.expert.createNode())

        if #available(iOS 13.0, *) {
            updateAppearance(traitCollection: UITraitCollection.current)
        }
    }

    override public func updateAppearance(traitCollection: UITraitCollection) {
        guard #available(iOS 13.0, *) else { return }

        if let geoGroup = scnNode.childNode(withName: "GEOgrp", recursively: true) {
            for node in geoGroup.childNodes {
                if let mat = node.firstGeometry?.firstMaterial {
                    mat.selfIllumination.updateAppearance(traitCollection: traitCollection)
                }
            }
        }
    }
}

extension Expert {

    /**
    Method that turns a lock up, causing all linked platforms to rise by the height of one block.
     - localizationKey: Expert.turnLockUp()
     */
    public func turnLockUp() {
        turnLock(up: true)
    }

    /**
     Method that turns a lock down, causing all linked platforms to fall by the height of one block.
     - localizationKey: Expert.turnLockDown()
     */
    public func turnLockDown() {
        turnLock(up: false)
    }

    /**
    Method that turns a lock up or down a certain number of times.

     Example usage:
     ````
     turnLock(up: false, numberOfTimes: 3)
     // Turns the lock down three times.

     turnLock(up: true, numberOfTimes: 4)
     // Turns the lock up four times.
     ````

     - parameters:
        - up: Takes a Boolean specifying whether the lock should be turned up (`true`) or down (`false`).
        - numberOfTimes: Takes an `Int` specifying the number of times to turn the lock.
     - localizationKey: Expert.turnLock(up:numberOfTimes:)
     */
    public func turnLock(up: Bool, numberOfTimes: Int) {
        for _ in 1...numberOfTimes {
            turnLock(up: up)
        }
    }

    func turnLock(up: Bool) {
        let lock = world?.existingItem(ofType: PlatformLock.self, at: nextCoordinateInCurrentDirection)

        if let lock = lock, lock.height == height {
            let controller = Controller(identifier: lock.id, kind: .movePlatforms, state: up)

            add(action: .control(controller))
        }
        else {
            add(action: .fail(.missingLock))
        }
    }
}

标签: swiftswift-playground

解决方案


Learn to Code 2 playground(Apple)的源代码中,我们可以找出所涉及类型的部分类继承层次结构: 继承层次

默认情况下,子类不继承它们的超类初始值设定项,但如果子类为任何新存储的属性提供默认值,则适用以下两条规则:

  • (规则 1):如果子类没有定义任何指定的初始化器,它会自动继承其所有超类的指定初始化器。
  • (规则 2):如果子类提供了其所有超类指定初始化器的实现,那么它会自动继承所有超类便利初始化器。

即使子类添加了更多便利初始化器,这些规则也适用。

下图显示了ActorExpert类的初始化器: 初始化器

从源代码中我们知道它Expert没有引入任何新的属性,但是有一个超类指定的初始化init(name:)Expert没有实现,这意味着Expert不能继承它的任何超类初始化器。

更糟糕的是,我们无法在扩展中convenience override init为该初始化器提供一个,因为 Swift 不支持覆盖扩展中的声明。init(name:)

所以没有办法Expert继承它的任何超类初始化器。


推荐阅读