【问题标题】:Swift 3: Convenience Initializer Extending Foundation's 'Timer' HangsSwift 3:扩展 Foundation 的“计时器”的便利初始化程序挂起
【发布时间】:2016-12-24 19:48:11
【问题描述】:

我正在尝试在 Swift 3 中扩展 Foundation 的 Timer 类,添加一个便利的初始化程序。但它对 Foundation 提供的初始化程序的调用永远不会返回。

这个问题在下面的简单演示中得到了说明,它可以作为 Playground 运行。

import Foundation

extension Timer {
    convenience init(target: Any) {
        print("Next statement never returns")
        self.init(timeInterval: 1.0,
                  target: target,
                  selector: #selector(Target.fire),
                  userInfo: nil,
                  repeats: true)
        print("This never executes")
    }
}

class Target {
    @objc func fire(_ timer: Timer) {
    }
}

let target = Target()
let timer = Timer(target: target)

控制台输出:

下一条语句永远不会返回

为了进一步学习,

• 我编写了类似的代码来扩展URLProtocol(仅有的具有实例初始化程序的其他Foundation 类之一)。结果:没问题。

• 为了消除可能导致 Objective-C 的东西,我将包装的初始化程序更改为 init(timeInterval:repeats:block:) 方法并提供了一个 Swift 闭包。结果:同样的问题。

【问题讨论】:

    标签: swift timer swift3 initializer convenience-methods


    【解决方案1】:

    我实际上并不知道答案,但我通过在带有调试器的实际应用程序中运行它发现存在无限递归(因此挂起)。我怀疑这是因为您实际上并没有调用 Timer 的指定初始化程序。这个事实并不明显,但是如果您尝试将 Timer 子类化并调用 super.init(timeInterval...) 编译器会抱怨,并且标题中的 super.init(timeInterval...) 上还有一个奇怪的“未继承”标记。

    我可以通过调用 self.init(fireAt:...) 来解决此问题:

    extension Timer {
        convenience init(target: Any) {
            print("Next statement never returns") // but it does
            self.init(fireAt: Date(), interval: 1, target: target, 
                selector: #selector(Target.fire), userInfo: nil, repeats: true)
            print("This never executes") // but it does
        }
    }
    

    随心所欲……

    【讨论】:

    • 标头中的“未继承”标记表明它不能用作子类的初始化器,因为它是从 Obj-C 工厂方法导入的 (timerWithTimeInterval:target:selector:userInfo:repeats: ) 仅返回 NSTimer 实例(因此不能用于创建子类实例)。虽然我不明白为什么这应该使它无限递归(不能用我自己的 Obj-C 工厂方法重现),但它可能是 NSTimer 实现中的东西。最奇特的。
    【解决方案2】:

    我看到了与 matt 描述的无限递归相同的问题。 -[NSCFTimer release] 在同一个对象上被一遍又一遍地调用。通过从实例初始化程序中调用类初始化程序,可以在纯 Objective-C 中重现此行为。

    @implementation NSTimer (Foo)
    
    - (instancetype)initWithTarget:(id)t {
        return [NSTimer timerWithTimeInterval:1 target:t selector:@selector(description) userInfo:nil repeats:NO];
    }
    
    @end
    

    编译器抱怨没有调用指定的初始化程序,这似乎与解决问题非常相关,但没有解释递归调用。

    warning: convenience initializer missing a 'self' call to another initializer
    

    【讨论】:

      【解决方案3】:

      @matt 的回答很有效。

      是的,我在我的应用程序中也看到了无限递归 - CFReleases。 Swift 的书很清楚便利构造器必须调用指定构造器。不过,它并没有说明惩罚是什么。无限递归虽然令人惊讶,但似乎是合理的。

      但是,请查看这两个声明,您可以通过在 Xcode 中通过选项单击或命令单击其中一种方法来查看它们:

      init(timeInterval interval: TimeInterval, repeats: Bool, block: @escaping (Timer) -> Void)
      
      convenience init(fire date: Date, interval: TimeInterval, repeats: Bool, block: @escaping (Timer) -> Void)
      

      我认为那里有问题。 @matt 建议的函数带有附加的 ("fire") 参数,它为我解决了这个问题,标记为 convenience。我使用的功能没有标记为方便,我认为(作为 Swift 新手)因此是指定的。但它少了一个参数。嗯?

      我想我应该提交一个错误,说明 Apple 以某种方式在错误的功能上获得了 convenience 关键字。这可能是因为 Swift 真的没有头文件,对吗?所以我想知道当我们选择单击基础功能时会看到什么。他们的工作流程中可能有一个步骤容易受到人为错误的影响?

      【讨论】:

      • Swift 书说 Rule2:便利构造器必须调用同一个类的另一个构造器。Rule3:便利构造器最终必须调用指定的构造器。 规则不禁止便利初始化程序调用同一类的另一个便利初始化程序。在其他类中,定义一个调用另一个便利初始化器的便利初始化器实际上是有效的。因此,您应该发送带有原始问题的错误报告。 Timer 是一个特例。生成的标题如何显示是一件微妙的事情,会被忽略。
      • 你是对的,@OOPer。所有初始化器最终都应该调用一个指定的初始化器,所以只要我的便利初始化器调用另一个初始化器,无论是否指定,它都应该工作。
      • 我现在已就原始问题提交了 Apple Bug Reporter Problem ID 29804952。在附加说明中,我问过函数X如何比函数Y少一个参数,但函数Y是标记为convenience的那个。谢谢大家。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2019-12-08
      • 1970-01-01
      • 1970-01-01
      • 2023-02-18
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多