Jak rozwiązać błąd kompilacji związany z „niejednoznacznym użyciem” przy użyciu składni #selector Swift?

79

[ UWAGA To pytanie zostało pierwotnie sformułowane w języku Swift 2.2. Został poprawiony dla Swift 4, obejmując dwie ważne zmiany językowe: pierwszy parametr metody, zewnętrzny, nie jest już automatycznie pomijany, a selektor musi być jawnie ujawniany jako Objective-C.]

Powiedzmy, że mam te dwie metody w mojej klasie:

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

Teraz chcę użyć nowego Swift 2.2 za #selectorskładnię dokonać wyboru odpowiedniego do pierwszej z tych metod func test(). Jak mam to zrobić? Kiedy próbuję tego:

let selector = #selector(test) // error

... Wyświetla się błąd „Niejednoznaczne użycie test()”. Ale jeśli powiem tak:

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

... błąd znika, ale teraz odnoszę się do niewłaściwej metody , tej z parametrem. Chcę odnieść się do tego bez żadnego parametru. Jak mam to zrobić?

[Uwaga: przykład nie jest sztuczny. NSObject ma metody Objective-C copyi copy:instancji, Swift copy()i copy(sender:AnyObject?); więc problem może łatwo pojawić się w prawdziwym życiu.]

matowe
źródło

Odpowiedzi:

110

[ UWAGA Ta odpowiedź została pierwotnie sformułowana w języku Swift 2.2. Został poprawiony dla Swift 4, obejmując dwie ważne zmiany językowe: pierwszy parametr metody, zewnętrzny, nie jest już automatycznie pomijany, a selektor musi być jawnie uwidoczniony w Objective-C.]

Możesz obejść ten problem, rzutując odwołanie do funkcji na poprawną sygnaturę metody:

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

(Jednak moim zdaniem nie powinieneś tego robić. Uważam tę sytuację za błąd, ujawniając, że składnia Swifta do odwoływania się do funkcji jest nieodpowiednia. Złożyłem raport o błędzie, ale bezskutecznie.)


Podsumowując nową #selectorskładnię:

Celem tej składni jest zapobieganie zbyt częstym awariom środowiska wykonawczego (zazwyczaj „nierozpoznanym selektorowi”), które mogą wystąpić podczas dostarczania selektora jako literału. #selector()pobiera odwołanie do funkcji , a kompilator sprawdzi, czy funkcja naprawdę istnieje i rozwiąże za Ciebie odwołanie do selektora celu-C. Dlatego nie możesz łatwo popełnić żadnego błędu.

( EDYCJA: Okay, tak, możesz. Możesz być kompletnym lunkheadem i ustawić cel na instancję, która nie implementuje komunikatu akcji określonego przez #selector. Kompilator cię nie zatrzyma i wyłączysz się tak jak w stare dobre czasy. Ech ...)

Odwołanie do funkcji może mieć jedną z trzech form:

  • Sama nazwa funkcji. Jest to wystarczające, jeśli funkcja jest jednoznaczna. Tak więc na przykład:

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

    Jest tylko jedna testmetoda, więc #selectorodnosi się do niej, mimo że przyjmuje parametr, a parametr #selectornie wspomina. Rozwiązany selektor celu-C, za kulisami, nadal będzie poprawnie wyglądał "test:"(z dwukropkiem wskazującym parametr).

  • Nazwa funkcji wraz z resztą jej podpisu . Na przykład:

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

    Mamy dwie testmetody, więc musimy rozróżnić; notacja test(_:)sprowadza się do drugiej , tej z parametrem.

  • Nazwa funkcji z resztą podpisu lub bez niej, a także rzutowanie pokazujące typy parametrów. A zatem:

    @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)
    }
    

    Tutaj mamy przeciążenie test(_:) . Przeciążenie nie może być wystawione na Objective-C, ponieważ Objective-C nie zezwala na przeciążanie, więc tylko jeden z nich jest ujawniony i możemy utworzyć selektor tylko dla tego, który jest ujawniony, ponieważ selektory są funkcją Objective-C . Ale nadal musimy ujednoznacznić, jeśli chodzi o Swifta, a obsada to robi.

    (To właśnie ta cecha językowa jest używana - moim zdaniem nadużywana - jako podstawa powyższej odpowiedzi).

Być może będziesz musiał pomóc Swift w rozwiązaniu odwołania do funkcji, wskazując mu, w jakiej klasie znajduje się funkcja:

  • Jeśli klasa jest taka sama jak ta lub w górę łańcucha nadklasy od tej klasy, zwykle nie jest potrzebne dalsze rozwiązanie (jak pokazano w przykładach powyżej); opcjonalnie możesz powiedzieć self, używając notacji kropkowej (np. #selector(self.test)w niektórych sytuacjach może być to konieczne.

  • W przeciwnym razie możesz użyć odwołania do instancji, dla której metoda jest zaimplementowana, z notacją kropkową, jak w tym prawdziwym przykładzie ( self.mpjest to MPMusicPlayerController):

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

    ... lub możesz użyć nazwy klasy z notacją kropkową:

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

    (Wydaje się, że jest to ciekawa notacja, ponieważ wygląda na to, że mówisz, że testjest to metoda klasowa, a nie metoda instancji, ale mimo to zostanie poprawnie rozwiązana do selektora, i tylko to się liczy).

matowe
źródło
2
Cześć @Sulthan, miło cię słyszeć. - Nie, to zinterpretowane wywołanie funkcji. Po prostu nie ma możliwości bezpośredniego zapisania pojęcia „ten bez parametrów”. To dziura; wydaje się, że poszli dalej, nie myśląc o tym przez całą drogę (jak to często bywa) ...
mat.
4
@Sulthan Tak jak się obawiałem, raport o błędzie wrócił „działa zgodnie z przeznaczeniem”. Więc moja odpowiedź jest odpowiedź: mam użyć notacji w celu określenia wariantu bez parametru. as
mat.
1
Kolejną atrakcją tego, jak „niesamowite” doświadczenie jest kodowanie w języku Swift.
Leo Natan
5
Przy obecnym Swift 3, trzeba umieścić listę argumentów w nawiasach: let selector = #selector(test as (Void) -> Void).
Martin R
1
Może nie najlepsze miejsce, ale w Swift 3 jaka będzie preferowana składnia? test as (Void) -> Voidczy krótsza składnia test as () -> ()?
Dam
1

Chcę dodać brakujące ujednoznacznienie: dostęp do metody instancji spoza klasy.

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

Z punktu widzenia klasy pełna sygnatura test()metody to (Foo) -> () -> Void, którą należy określić, aby uzyskać plik Selector.

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

Alternatywnie możesz odwołać się do instancji, Selectorjak pokazano w oryginalnej odpowiedzi.

let foo = Foo()
#selector(foo.test as () -> Void)
#selector(foo.test(_:))
Guy Kogus
źródło
Tak, notacja Foo.xxxjest już dziwna, ponieważ nie są to zewnętrzne metody klasowe. Wygląda więc na to, że kompilator daje ci przepustkę, ale tylko wtedy, gdy nie ma dwuznaczności. Jeśli pojawia się niejasność, musisz cofnąć rękawy i użyć dłuższej notacji, która jest legalna i dokładna, ponieważ metoda instancji jest „potajemnie” metodą klas curried. Bardzo dokładne wykrywanie pozostałej krawędzi!
mat.
0

W moim przypadku (Xcode 11.3.1) błąd występował tylko podczas używania lldb podczas debugowania. Podczas pracy działa poprawnie.

użytkownik23
źródło