Як усунути помилку компіляції із «неоднозначним використанням» за допомогою синтаксису 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-C, так copyі copy:методи екземплярів, Swift copy()та copy(sender:AnyObject?); тому проблема може легко виникнути в реальному житті.]

Відповіді:


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 вирішити посилання на функцію, повідомивши, в якому класі функція:

  • Якщо клас такий самий, як цей, або вище ланцюга суперкласу від цього, зазвичай не потрібна подальша роздільна здатність (як показано у прикладах вище); за бажанням, ви можете сказати 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, приємно почути тебе. - Ні, це інтерпретується виклик функції. Просто неможливо прямо зазначити поняття "той, що не має параметрів". Це діра; вони, здається, продовжили це, не думаючи про це до кінця (як часто) ...
matt

4
@Sulthan Як я боявся, звіт про помилку повернувся "працює за призначенням". Отже, моя відповідь - це відповідь: вам потрібно використовувати asпозначення, щоб вказати варіант без параметрів.
matt

1
Ще одна родзинка того, яким "дивовижним" досвідом є кодування в Swift.
Лео Натан

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вже дивно, бо це не зовнішні методи класу. Отже, здається, компілятор дає вам пропуск, але лише в тому випадку, якщо немає двозначності. Якщо є неоднозначність, вам доведеться засунути рукави і використовувати довше позначення, що є законним та точним, оскільки метод екземпляру є "таємно" методом класованого класу. Дуже точне виявлення випадку, що залишився краю!
matt

0

У моєму випадку (Xcode 11.3.1) помилка була лише при використанні lldb під час налагодження. При запуску він працює належним чином.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.