Как устранить ошибку компиляции «неоднозначное использование» с помощью синтаксиса Swift #selector?

79

[ ПРИМЕЧАНИЕ. Этот вопрос изначально был сформулирован в Swift 2.2. Он был пересмотрен для Swift 4, включая два важных языковых изменения: первый внешний параметр метода больше не подавляется автоматически, а селектор должен быть явно предоставлен 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-C, так copyи copy:методы экземпляра, Swift copy()и copy(sender:AnyObject?); так что проблема может легко возникнуть в реальной жизни.]

матовый
источник

Ответы:

110

[ ПРИМЕЧАНИЕ. Этот ответ был первоначально сформулирован для Swift 2.2. Он был пересмотрен для Swift 4, включая два важных языковых изменения: первый внешний параметр метода больше не подавляется автоматически, а селектор должен быть явно предоставлен 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 разрешить ссылку на функцию, сообщив ему, в каком классе находится функция:

  • Если класс такой же, как этот, или выше по цепочке суперклассов от этого, дальнейшее разрешение обычно не требуется (как показано в примерах выше); необязательно, вы можете сказать 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.
Лео Натан,
5
С текущего Swift 3, вы должны поместить список аргументов в скобках: let selector = #selector(test as (Void) -> Void).
Martin R
1
Может быть, не лучшее место, но в Swift 3 какой синтаксис будет предпочтительнее? test as (Void) -> Voidили более короткий синтаксис test as () -> ()?
Dam
1

Я хочу добавить недостающее значение: доступ к методу экземпляра извне класса.

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

С точки зрения класса полная сигнатура test()метода (Foo) -> () -> Void, которую вам нужно будет указать, чтобы получить Selector.

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

В качестве альтернативы вы можете обратиться к экземпляру Selectors, как показано в исходном ответе.

let foo = Foo()
#selector(foo.test as () -> Void)
#selector(foo.test(_:))
Гай Когус
источник
Да, обозначения Foo.xxxуже странные, потому что это не методы внешнего класса. Кажется, компилятор дает вам пропуск, но только в том случае, если нет двусмысленности. Если есть двусмысленность, вам придется закатать рукава и использовать более длинную нотацию, что является законным и точным, потому что метод экземпляра «тайно» является методом каррированного класса. Очень точное обнаружение оставшегося крайнего случая!
Мэтт
0

В моем случае (Xcode 11.3.1) ошибка была только при использовании lldb во время отладки. При запуске работает исправно.

user23
источник