如何使用Swift #selector语法解决“模棱两可”的编译错误?


79

[注意此问题最初是在Swift 2.2下提出的。它已针对Swift 4进行了修订,涉及两个重要的语言更改:第一个方法参数external不再被自动抑制,并且选择器必须显式地暴露给Objective-C。

假设我在课堂上有以下两种方法:

@objc func test() {}
@objc func test(_ sender:AnyObject?) {}

现在,我想使用Swift 2.2的新#selector语法创建一个与这些方法中的一个相对应的选择器func test()。我该怎么做?当我尝试这个:

let selector = #selector(test) // error

...我收到一个错误消息:“的歧义使用” test()。但是如果我这样说:

let selector = #selector(test(_:)) // ok, but...

...错误消失了,但是我现在指的是错误的方法,即带有参数的方法。我想引用不带任何参数的那个。我该怎么做?

[注意:该示例不是人为的。NSObject同时具有Objective-Ccopycopy:实例方法Swiftcopy()copy(sender:AnyObject?); 因此这个问题很容易在现实生活中出现。]

Answers:


110

[注意此答案最初是在Swift 2.2下提出的。它已针对Swift 4进行了修订,涉及两个重要的语言更改:第一个方法参数external不再被自动抑制,并且选择器必须显式地暴露给Objective-C。

您可以解决这个问题,通过铸造你的函数引用正确的方法签名:

let selector = #selector(test as () -> Void)

(但是,我认为您不必这样做。我认为这种情况是一个错误,表明Swift引用函数的语法不足。我提交了错误报告,但无济于事。)


只是总结一下新#selector语法:

此语法的目的是为了防止在将选择器作为文字字符串提供时出现常见的运行时崩溃(通常为“无法识别的选择器”)。#selector()接受一个函数引用,编译器将检查该函数是否确实存在,并将为您解析对Objective-C选择器的引用。因此,您不能轻易犯任何错误。

编辑:好的,是的。您可以成为一个完整的笨蛋,并将目标设置为一个实例,该实例未实现由所指定的操作消息#selector。过去的美好时光。

函数引用可以三种形式出现:

  • 函数的裸名。如果功能是明确的,这就足够了。因此,例如:

    @objc func test(_ sender:AnyObject?) {}
    func makeSelector() {
        let selector = #selector(test)
    }
    

    只有一种test方法,因此#selector即使它需要一个参数并且#selector没有提到该参数,也要引用它。解析后的Objective-C选择器在幕后仍将正确存在"test:"(带有冒号,指示参数)。

  • 函数的名称及其签名的其余部分。例如:

    func test() {}
    func test(_ sender:AnyObject?) {}
    func makeSelector() {
        let selector = #selector(test(_:))
    }
    

    我们有两种test方法,因此我们需要区分;该表示法test(_:)解析为第二个,带有参数的表示法。

  • 带有或不带有其签名的其余部分的功能,再加上名,显示类型的参数。从而:

    @objc func test(_ integer:Int) {}
    @nonobjc func test(_ string:String) {}
    func makeSelector() {
        let selector1 = #selector(test as (Int) -> Void)
        // or:
        let selector2 = #selector(test(_:) as (Int) -> Void)
    }
    

    在这里,我们超载了 test(_:)。超载不能接触到的Objective-C,因为Objective-C中不允许超载,所以只有一个被暴露出来,我们可以形成只为一个选择暴露出来,因为选择是一个Objective-C功能。但是就Swift而言,我们仍然必须消除歧义,而演员阵容可以做到这一点。

    (在我看来,正是这种语言功能被误用了,作为上述答案的基础。)

另外,您可能需要通过告诉函数该函数在哪个类中来帮助Swift解析函数引用:

  • 如果该类与该类相同,或者在该类的超类链的上游,则通常不需要进一步的解决方法(如上例所示);您也可以选择self使用点号(例如#selector(self.test),)说出,在某些情况下,您可能必须这样做。

  • 否则,您可以使用点标记来引用对其实现了该方法的实例,如下面的真实示例(self.mp是MPMusicPlayerController):

    let pause = UIBarButtonItem(barButtonSystemItem: .pause, 
        target: self.mp, action: #selector(self.mp.pause))
    

    ...或者您可以使用类名,并使用点符号:

    class ClassA : NSObject {
        @objc func test() {}
    }
    class ClassB {
        func makeSelector() {
            let selector = #selector(ClassA.test)
        }
    }
    

    (这似乎是一种奇怪的表示法,因为您似乎是在说test类方法而不是实例方法,但是尽管如此,它将正确地解析为选择器,这很重要。)


2
嗨,@ Sulthan,很高兴收到您的来信。-不,这是对函数调用的解释。根本没有办法直接注明“无参数的概念”。这是一个洞;他们似乎一直在不考虑整个过程中(一直如此)进行着……
马特

4
@Sulthan正如我所担心的那样,错误报告“按预期工作”返回。所以,我的回答是答案是:你必须使用as符号,以指定无参数的变异。
马特

1
另一个“惊人的”体验的亮点是在Swift中进行编码。
Leo Natan

5
在当前的Swift 3中,您必须将参数列表放在括号中:let selector = #selector(test as (Void) -> Void)
Martin R

1
也许不是最好的地方,但是在Swift 3中,首选语法是什么?test as (Void) -> Void还是较短的语法test as () -> ()
大坝

1

我想添加一个消除歧义的方法:从类外部访问实例方法。

class Foo {
    @objc func test() {}
    @objc func test(_ sender: AnyObject?) {}
}

从类的角度来看,该test()方法的完整签名是(Foo) -> () -> Void,您需要指定该完整签名才能获取Selector

#selector(Foo.test as (Foo) -> () -> Void)
#selector(Foo.test(_:))

或者,您可以参考Selector原始答案中所示的实例的。

let foo = Foo()
#selector(foo.test as () -> Void)
#selector(foo.test(_:))

是的,这种表示法Foo.xxx已经很奇怪了,因为它们不是外部类方法。因此,似乎编译器为您提供通过,但前提是没有歧义。如果存在歧义,则必须退缩并使用更长的表示法,这是合法且准确的,因为实例方法是“秘密地”使用咖喱类方法。可以很好地检测剩余边缘情况!
马特

0

就我而言(Xcode 11.3.1),该错误仅在调试时使用lldb时出现。运行时,它可以正常工作。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.