Наследует ли подкласс инициализаторы удобства от расширения суперкласса в Swift? - PullRequest
0 голосов
/ 15 апреля 2020

Я пытался сделать расширение для типа Actor, исходя из Учимся кодировать 2 в Swift Playgounds для iPad следующим образом:

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)

, появляется ошибка компилятора. Почему это происходит?

снимок экрана: playground screenshot

База кода (от Apple) за этой игровой площадкой довольно большая, поэтому я просто перечислю наиболее релевантный источник коды (Actor и Expert тип) следующие:

Тип актера

//
//  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))
        }
    }
}

1 Ответ

0 голосов
/ 18 апреля 2020

Исходный код игровой площадки Learn to Code 2 (от Apple) позволяет нам определить часть иерархии наследования классов участвующих типов: inheritance hierarchy

Подклассы не наследуют свои инициализаторы суперкласса по умолчанию , но если подкласс предоставляет значения по умолчанию для любых новых сохраненных свойств, применяются следующие два правила:

  • (Правило 1): если подкласс не определяет никаких назначенных инициализаторов, он автоматически наследует все назначенные инициализаторы суперкласса.
  • (Правило 2): если подкласс обеспечивает реализацию всех своего инициализатора, назначенного суперклассом, затем он автоматически наследует все удобные инициализаторы суперкласса.

Эти правила применяются, даже если подкласс добавляет дополнительные удобные инициализаторы.

Следующая диаграмма показывает инициализаторы Actor и Expert класс: initializers

Из исходного кода мы знаем, что * 102 9 * не вводит никаких новых свойств, но есть инициализатор суперкласса init(name:), который Expert не реализует, а это означает, что Expert не может наследовать ни один из инициализаторов суперкласса.

Еще хуже Мы не можем предоставить convenience override init для этого init(name:) инициализатора в расширении, так как переопределение объявлений в расширениях не поддерживается в Swift.

Так что Expert не может наследовать ни один из его инициализаторы суперкласса.

...