Swift 3:扩展Foundation的'Timer'的方便初始化程序挂起

3

我正在尝试在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(只有少数几个其他基础类之一具有实例初始化器)。结果:没有问题。

• 为了排除Objective-C作为可能的原因,我将包装的初始化器更改为init(timeInterval:repeats:block:)方法,并提供了一个Swift闭包。结果:同样的问题。

3个回答

4
我其实不知道答案,但是通过在实际应用程序中使用调试器运行此代码,我发现存在无限递归(因此挂起)。我猜测这是因为您实际上没有调用计时器的指定初始化程序。这个事实并不明显,但如果您尝试子类化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
    }
}

你可以根据自己的理解来做出判断...


头文件中的“not inherited”标记表示它不能用作子类的初始化器,因为它是从Obj-C工厂方法(timerWithTimeInterval:target:selector:userInfo:repeats:)导入的,该方法仅返回NSTimer实例(因此无法用于创建子类实例)。虽然我不知道为什么这会使它无限递归(使用自己的Obj-C工厂方法无法复现),因此可能是NSTimer实现中的某些问题。非常奇怪。 - Hamish

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

0

@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")参数,这对我解决了问题,被标记为便利。 我使用的函数,并没有被标记为便利,我假设(作为一个Swift新手),因此被称为指定。但它少了一个参数。 嗯?

我想我应该提交一个错误报告,说明苹果不知何故把便利关键字放错了函数上。 这可能是因为Swift确实没有头文件,对吧? 所以我想知道当我们在Foundation函数上选择点击选项时会看到什么。 可能在他们的工作流程中存在一些容易出现人为错误的步骤?


Swift书中提到了两个规则:规则2:便利初始化器必须调用同一类中的另一个初始化器。规则3:便利初始化器必须最终调用指定初始化器。这些规则并不禁止便利初始化器调用同一类中的另一个便利初始化器。在其他类中,定义一个调用另一个便利初始化器的便利初始化器实际上是可行的。因此,您应该使用原始问题发送Bug报告。Timer是一个特殊情况。如何显示生成的头文件是微妙的事情,可以忽略。 - OOPer
你是正确的,@OOPer。所有的初始化方法都应该最终调用一个指定的初始化方法,只要我的便利初始化方法调用了另一个初始化方法,无论是指定的还是非指定的,它就可以工作。 - Jerry Krinock
2
我现在已经在原始问题上提交了Apple Bug Reporter Problem ID 29804952。在附加说明中,我问了一下为什么函数X比函数Y少一个参数,但函数Y被标记为“方便”。谢谢大家。 - Jerry Krinock

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接