Jak testujesz funkcje i domknięcia pod kątem równości?

88

Książka mówi, że „funkcje i zamknięcia są typami referencyjnymi”. Jak więc sprawdzić, czy odniesienia są równe? == i === nie działają.

func a() { }
let å = a
let b = å === å // Could not find an overload for === that accepts the supplied arguments
Jessy
źródło
5
O ile mi wiadomo, nie można również sprawdzić równości metaklas (np. MyClass.self)
Jiaaro
Nie powinno być konieczne porównywanie dwóch domknięć pod kątem tożsamości. Czy możesz podać przykład, gdzie byś to zrobił? Może istnieć alternatywne rozwiązanie.
Bill
1
Zamknięcia multiemisji, a la C #. W Swift są z konieczności brzydsze, ponieważ nie można przeciążyć „operatora” (T, U), ale nadal możemy je stworzyć sami. Jednak bez możliwości usuwania domknięć z listy wywołań przez odniesienie, musimy utworzyć własną klasę opakowania. To jest problem i nie powinno być konieczne.
Jessy
2
Świetne pytanie, ale zupełnie inna sprawa: użycie znaku diakrytycznego w åodniesieniu do odniesień ajest naprawdę interesujące. Czy jest jakaś konwencja, którą tu eksplorujesz? (Nie wiem, czy mi się to podoba, czy nie; ale wygląda na to, że może być bardzo potężny, szczególnie w programowaniu czysto funkcjonalnym.)
Rob Napier
2
@Bill Przechowuję domknięcia w tablicy i nie mogę użyć indexOf ({$ 0 == closure}, aby je znaleźć i usunąć. Teraz muszę zrestrukturyzować mój kod z powodu optymalizacji, która moim zdaniem jest kiepskim projektem językowym.
Zack Morris

Odpowiedzi:

72

Chris Lattner napisał na forach programistów:

Jest to funkcja, której celowo nie chcemy obsługiwać. Jest wiele rzeczy, które spowodują, że wskaźnik równości funkcji (w sensie systemu typu szybkiego, który obejmuje kilka rodzajów domknięć) zawiedzie lub zmieni się w zależności od optymalizacji. Gdyby w funkcjach zdefiniowano "===", kompilator nie miałby możliwości scalania identycznych treści metod, współużytkowania thunks i wykonywania pewnych optymalizacji przechwytywania w domknięciach. Co więcej, równość tego rodzaju byłaby niezwykle zaskakująca w niektórych kontekstach generycznych, w których można uzyskać operacje reabstrakcyjne, które dostosowują rzeczywistą sygnaturę funkcji do tej, której oczekuje typ funkcji.

https://devforums.apple.com/message/1035180#1035180

Oznacza to, że nie powinieneś nawet próbować porównywać domknięć pod kątem równości, ponieważ optymalizacje mogą wpłynąć na wynik.

drewag
źródło
18
To tylko mnie ugryzło, co było trochę niszczące, ponieważ przechowywałem domknięcia w tablicy i teraz nie mogę ich usunąć za pomocą indexOf ({$ 0 == closure}, więc muszę refaktoryzować. Optymalizacja IMHO nie powinna wpływać na projekt języka, więc bez szybkiej poprawki, takiej jak obecnie przestarzały @objc_block w odpowiedzi Matta, argumentowałbym, że Swift nie może w tej chwili prawidłowo przechowywać i pobierać zamknięć. Więc nie sądzę, aby należało zalecać używanie języka Swift w ciężkim kodzie wywołań zwrotnych podobnie jak w przypadku tworzenia stron internetowych. I właśnie dlatego przeszliśmy na Swift w pierwszej kolejności ...
Zack Morris,
4
@ZackMorris Przechowuj jakiś identyfikator z zamknięciem, abyś mógł go później usunąć. Jeśli używasz typów referencyjnych, możesz po prostu przechowywać odniesienie do obiektu, w przeciwnym razie możesz wymyślić własny system identyfikatorów. Możesz nawet zaprojektować typ, który ma zamknięcie i unikalny identyfikator, którego możesz użyć zamiast zwykłego zamknięcia.
drewag
5
@drewag Tak, są obejścia, ale Zack ma rację. To jest naprawdę kiepskie. Rozumiem, że chcę mieć optymalizacje, ale jeśli jest miejsce w kodzie, które programista musi porównać niektóre zamknięcia, po prostu niech kompilator nie optymalizuje tych konkretnych sekcji. Albo wykonaj jakąś dodatkową funkcję kompilatora, która pozwoli mu tworzyć sygnatury równości, które nie przerywają dziwnych optymalizacji. To jest Apple, o którym tutaj mówimy ... jeśli mogą zmieścić Xeon w iMacu, z pewnością mogą sprawić, że zamknięcia będą porównywalne. Daj mi spokój!
CommaToast
10

Szukałem dużo. Wydaje się, że nie ma możliwości porównania wskaźników funkcji. Najlepszym rozwiązaniem, jakie otrzymałem, jest hermetyzacja funkcji lub zamknięcia w obiekcie, który można skasować. Lubić:

var handler:Handler = Handler(callback: { (message:String) in
            //handler body
}))
tuncay
źródło
2
To zdecydowanie najlepsze podejście. Owijanie i rozpakowywanie zamknięć jest do bani, ale jest to lepsze niż niedeterministyczna, niewspierana kruchość.
8

Najprostszym sposobem jest wyznaczenie typu bloku jako @objc_block, a teraz możesz rzutować go na AnyObject, który jest porównywalny z ===. Przykład:

    typealias Ftype = @objc_block (s:String) -> ()

    let f : Ftype = {
        ss in
        println(ss)
    }
    let ff : Ftype = {
        sss in
        println(sss)
    }
    let obj1 = unsafeBitCast(f, AnyObject.self)
    let obj2 = unsafeBitCast(ff, AnyObject.self)
    let obj3 = unsafeBitCast(f, AnyObject.self)

    println(obj1 === obj2) // false
    println(obj1 === obj3) // true
matowe
źródło
Hej, próbuję, jeśli unsafeBitCast (listener, AnyObject.self) === unsafeBitCast (f, AnyObject.self), ale otrzymuję błąd krytyczny: nie można unsafeBitCast między typami o różnych rozmiarach. Pomysł polega na zbudowaniu systemu opartego na zdarzeniach, ale metoda removeEventListener powinna być w stanie sprawdzić wskaźniki funkcji.
zamrażanie_
2
Użyj @convention (block) zamiast @objc_block w Swift 2.x. Świetna odpowiedź!
Gabriel.Massana
6

Ja też szukałem odpowiedzi. I w końcu go znalazłem.

Potrzebny jest rzeczywisty wskaźnik funkcji i jego kontekst ukryty w obiekcie funkcji.

func peekFunc<A,R>(f:A->R)->(fp:Int, ctx:Int) {
    typealias IntInt = (Int, Int)
    let (hi, lo) = unsafeBitCast(f, IntInt.self)
    let offset = sizeof(Int) == 8 ? 16 : 12
    let ptr  = UnsafePointer<Int>(lo+offset)
    return (ptr.memory, ptr.successor().memory)
}
@infix func === <A,R>(lhs:A->R,rhs:A->R)->Bool {
    let (tl, tr) = (peekFunc(lhs), peekFunc(rhs))
    return tl.0 == tr.0 && tl.1 == tr.1
}

A oto demo:

// simple functions
func genericId<T>(t:T)->T { return t }
func incr(i:Int)->Int { return i + 1 }
var f:Int->Int = genericId
var g = f;      println("(f === g) == \(f === g)")
f = genericId;  println("(f === g) == \(f === g)")
f = g;          println("(f === g) == \(f === g)")
// closures
func mkcounter()->()->Int {
    var count = 0;
    return { count++ }
}
var c0 = mkcounter()
var c1 = mkcounter()
var c2 = c0
println("peekFunc(c0) == \(peekFunc(c0))")
println("peekFunc(c1) == \(peekFunc(c1))")
println("peekFunc(c2) == \(peekFunc(c2))")
println("(c0() == c1()) == \(c0() == c1())") // true : both are called once
println("(c0() == c2()) == \(c0() == c2())") // false: because c0() means c2()
println("(c0 === c1) == \(c0 === c1)")
println("(c0 === c2) == \(c0 === c2)")

Zobacz poniższe adresy URL, aby dowiedzieć się, dlaczego i jak to działa:

Jak widzisz, jest w stanie sprawdzić tylko tożsamość (drugi test daje wyniki false). Ale to powinno wystarczyć.

dankogai
źródło
5
Ta metoda nie będzie niezawodna w przypadku optymalizacji kompilatora devforums.apple.com/message/1035180#1035180
drewag
8
To jest hack oparty na nieokreślonych szczegółach implementacji. Wtedy użycie tego oznacza, że ​​twój program da niezdefiniowany wynik.
eonil
8
Pamiętaj, że opiera się to na nieudokumentowanych danych i nieujawnionych szczegółach implementacji, które mogą spowodować awarię Twojej aplikacji w przyszłości, jeśli się zmienią. Nie zaleca się stosowania w kodzie produkcyjnym.
Cristik
To jest „koniczyna”, ale całkowicie niewykonalna. Nie wiem, dlaczego nagrodzono to nagrodą. Język celowo nie ma równości funkcji, dokładnie w celu zwolnienia kompilatora do swobodnego łamania równości funkcji w celu uzyskania lepszych optymalizacji.
Alexander - Przywróć Monikę
... i dokładnie to podejście jest przeciwne Chrisowi Lattnerowi (patrz górna odpowiedź).
pipacs
4

To świetne pytanie i chociaż Chris Lattner celowo nie chce wspierać tej funkcji, podobnie jak wielu programistów, nie mogę również odpuścić moich uczuć pochodzących z innych języków, w których jest to trywialne zadanie. Istnieje wiele unsafeBitCastprzykładów, większość z nich nie pokazuje pełnego obrazu, oto bardziej szczegółowy :

typealias SwfBlock = () -> ()
typealias ObjBlock = @convention(block) () -> ()

func testSwfBlock(a: SwfBlock, _ b: SwfBlock) -> String {
    let objA = unsafeBitCast(a as ObjBlock, AnyObject.self)
    let objB = unsafeBitCast(b as ObjBlock, AnyObject.self)
    return "a is ObjBlock: \(a is ObjBlock), b is ObjBlock: \(b is ObjBlock), objA === objB: \(objA === objB)"
}

func testObjBlock(a: ObjBlock, _ b: ObjBlock) -> String {
    let objA = unsafeBitCast(a, AnyObject.self)
    let objB = unsafeBitCast(b, AnyObject.self)
    return "a is ObjBlock: \(a is ObjBlock), b is ObjBlock: \(b is ObjBlock), objA === objB: \(objA === objB)"
}

func testAnyBlock(a: Any?, _ b: Any?) -> String {
    if !(a is ObjBlock) || !(b is ObjBlock) {
        return "a nor b are ObjBlock, they are not equal"
    }
    let objA = unsafeBitCast(a as! ObjBlock, AnyObject.self)
    let objB = unsafeBitCast(b as! ObjBlock, AnyObject.self)
    return "a is ObjBlock: \(a is ObjBlock), b is ObjBlock: \(b is ObjBlock), objA === objB: \(objA === objB)"
}

class Foo
{
    lazy var swfBlock: ObjBlock = self.swf
    func swf() { print("swf") }
    @objc func obj() { print("obj") }
}

let swfBlock: SwfBlock = { print("swf") }
let objBlock: ObjBlock = { print("obj") }
let foo: Foo = Foo()

print(testSwfBlock(swfBlock, swfBlock)) // a is ObjBlock: false, b is ObjBlock: false, objA === objB: false
print(testSwfBlock(objBlock, objBlock)) // a is ObjBlock: false, b is ObjBlock: false, objA === objB: false

print(testObjBlock(swfBlock, swfBlock)) // a is ObjBlock: true, b is ObjBlock: true, objA === objB: false
print(testObjBlock(objBlock, objBlock)) // a is ObjBlock: true, b is ObjBlock: true, objA === objB: true

print(testAnyBlock(swfBlock, swfBlock)) // a nor b are ObjBlock, they are not equal
print(testAnyBlock(objBlock, objBlock)) // a is ObjBlock: true, b is ObjBlock: true, objA === objB: true

print(testObjBlock(foo.swf, foo.swf)) // a is ObjBlock: true, b is ObjBlock: true, objA === objB: false
print(testSwfBlock(foo.obj, foo.obj)) // a is ObjBlock: false, b is ObjBlock: false, objA === objB: false
print(testAnyBlock(foo.swf, foo.swf)) // a nor b are ObjBlock, they are not equal
print(testAnyBlock(foo.swfBlock, foo.swfBlock)) // a is ObjBlock: true, b is ObjBlock: true, objA === objB: true

Interesujące jest to, jak szybko rzuca SwfBlock na ObjBlock, ale w rzeczywistości dwa rzucane bloki SwfBlock zawsze będą miały różne wartości, podczas gdy ObjBlocks nie. Kiedy rzucamy ObjBlock na SwfBlock, dzieje się z nimi to samo, stają się dwiema różnymi wartościami. Tak więc, aby zachować odniesienie, należy unikać tego rodzaju rzutowania.

Wciąż rozumiem cały ten temat, ale jedyną rzeczą, której pragnąłem, jest możliwość użycia @convention(block)w metodach klas / struktur, więc złożyłem prośbę o funkcję, która wymaga głosowania w górę lub wyjaśnienia, dlaczego jest to zły pomysł. Mam też poczucie, że to podejście może być złe w sumie, jeśli tak, czy ktoś może wyjaśnić dlaczego?

Ian Bytchek
źródło
1
Myślę, że nie rozumiesz rozumowania Chrisa Latnera, dlaczego nie jest to (i nie powinno być) obsługiwane. "Mam też przeczucie, że to podejście może być złe, jeśli tak, to czy ktoś może wyjaśnić dlaczego?" Ponieważ w zoptymalizowanej kompilacji kompilator może dowolnie modyfikować kod na wiele sposobów, które łamią ideę punktowej równości funkcji. W podstawowym przykładzie, jeśli treść jednej funkcji zaczyna się tak samo, jak inna funkcja, kompilator prawdopodobnie nałoży na siebie te dwa elementy w kodzie maszynowym, zachowując tylko różne punkty wyjścia. Zmniejsza to duplikację
Alexander - Przywróć Monikę
1
Zasadniczo domknięcia to sposoby inicjowania obiektów klas anonimowych (podobnie jak w Javie, ale jest to bardziej oczywiste). Te obiekty domknięć są alokowane na stercie i przechowują dane przechwycone przez zamknięcie, które działają jak niejawny parametr funkcji zamknięcia. Obiekt closure zawiera odniesienie do funkcji, która działa na jawnych (za pośrednictwem func args) i niejawnych (poprzez przechwycony kontekst zamknięcia) argumentach. Podczas gdy treść funkcji może być udostępniona jako pojedynczy unikalny punkt, wskaźnik obiektu zamknięcia nie może być, ponieważ istnieje jeden obiekt zamknięcia na zestaw zamkniętych wartości.
Alexander - Przywróć Monikę
1
Więc kiedy masz Struct S { func f(_: Int) -> Bool }, w rzeczywistości masz funkcję typu, S.fktóra ma typ (S) -> (Int) -> Bool. Ta funkcja może być współdzielona. Jest parametryzowana wyłącznie przez swoje jawne parametry. Gdy używasz go jako metody instancji (albo przez niejawne wiązanie selfparametru przez wywołanie metody na obiekcie, np. S().fLub przez jawne wiązanie go, np. S.f(S())), Tworzysz nowy obiekt zamknięcia. Ten obiekt przechowuje wskaźnik do S.f(który można udostępniać) , but also to your instance (self , the S () `).
Alexander - Przywróć Monikę
1
Ten obiekt zamknięcia musi być unikatowy dla każdego wystąpienia S. Gdyby równość wskaźnika zamknięcia była możliwa, byłbyś zaskoczony, gdybyś odkrył, że s1.fnie jest to ten sam wskaźnik co s2.f(ponieważ jeden jest obiektem zamknięcia, który odwołuje się do s1i f, a drugi jest obiektem zamknięcia, który odwołuje się do s2i f).
Alexander - Przywróć Monikę
To jest genialne, dziękuję! Tak, do tej pory miałem już obraz tego, co się dzieje i to stawia wszystko w odpowiedniej perspektywie! 👍
Ian Bytchek
4

Oto jedno możliwe rozwiązanie (koncepcyjnie to samo, co odpowiedź „tuncay”). Chodzi o to, aby zdefiniować klasę, która opakowuje niektóre funkcjonalności (np. Command):

Szybki:

typealias Callback = (Any...)->Void
class Command {
    init(_ fn: @escaping Callback) {
        self.fn_ = fn
    }

    var exec : (_ args: Any...)->Void {
        get {
            return fn_
        }
    }
    var fn_ :Callback
}

let cmd1 = Command { _ in print("hello")}
let cmd2 = cmd1
let cmd3 = Command { (_ args: Any...) in
    print(args.count)
}

cmd1.exec()
cmd2.exec()
cmd3.exec(1, 2, "str")

cmd1 === cmd2 // true
cmd1 === cmd3 // false

Jawa:

interface Command {
    void exec(Object... args);
}
Command cmd1 = new Command() {
    public void exec(Object... args) [
       // do something
    }
}
Command cmd2 = cmd1;
Command cmd3 = new Command() {
   public void exec(Object... args) {
      // do something else
   }
}

cmd1 == cmd2 // true
cmd1 == cmd3 // false
baso
źródło
Byłoby znacznie lepiej, gdybyś zrobił to jako Ogólny.
Alexander - Przywróć Monikę
2

Cóż, minęły 2 dni i nikt nie włączył rozwiązania, więc zmienię swój komentarz na odpowiedź:

O ile wiem, nie możesz sprawdzić równości lub tożsamości funkcji (jak Twój przykład) i metaklas (np. MyClass.self):

Ale - i to jest tylko pomysł - nie mogę nie zauważyć, że whereklauzula w rodzajach wydaje się być w stanie sprawdzić równość typów. Więc może możesz to wykorzystać, przynajmniej do sprawdzenia tożsamości?

Jiaaro
źródło
2

Nie jest to ogólne rozwiązanie, ale jeśli ktoś próbuje zaimplementować wzorzec nasłuchiwania, w końcu zwróciłem "id" funkcji podczas rejestracji, więc mogę go użyć do późniejszego wyrejestrowania (co jest rodzajem obejścia pierwotnego pytania w przypadku "słuchaczy" jak zwykle wyrejestrowanie sprowadza się do sprawdzenia funkcji pod kątem równości, co przynajmniej nie jest "trywialne" jak w innych odpowiedziach).

Więc coś takiego:

class OfflineManager {
    var networkChangedListeners = [String:((Bool) -> Void)]()

    func registerOnNetworkAvailabilityChangedListener(_ listener: @escaping ((Bool) -> Void)) -> String{
        let listenerId = UUID().uuidString;
        networkChangedListeners[listenerId] = listener;
        return listenerId;
    }
    func unregisterOnNetworkAvailabilityChangedListener(_ listenerId: String){
        networkChangedListeners.removeValue(forKey: listenerId);
    }
}

Teraz wystarczy tylko zapamiętać keyzwracany przez funkcję "register" i przekazać go przy wyrejestrowaniu.

wirus
źródło
0

Moim rozwiązaniem było zawinięcie funkcji do klasy, która rozszerza NSObject

class Function<Type>: NSObject {
    let value: (Type) -> Void

    init(_ function: @escaping (Type) -> Void) {
        value = function
    }
}
Renetik
źródło
Kiedy to robisz, jak je porównujesz? powiedzmy, że chcesz usunąć jeden z nich z tablicy swoich opakowań, jak to zrobić? Dzięki.
Ricardo
0

Wiem, że odpowiadam na to pytanie sześć lat później, ale myślę, że warto przyjrzeć się motywacji stojącej za tym pytaniem. Pytający skomentował:

Jednak bez możliwości usuwania domknięć z listy wywołań przez odniesienie, musimy utworzyć własną klasę opakowania. To jest problem i nie powinno być konieczne.

Więc wydaje mi się, że pytający chce prowadzić listę oddzwonień, taką jak ta:

class CallbackList {
    private var callbacks: [() -> ()] = []

    func call() {
        callbacks.forEach { $0() }
    }

    func addCallback(_ callback: @escaping () -> ()) {
        callbacks.append(callback)
    }

    func removeCallback(_ callback: @escaping () -> ()) {
        callbacks.removeAll(where: { $0 == callback })
    }
}

Ale nie możemy pisać w removeCallbackten sposób, ponieważ ==nie działa dla funkcji. (Ani też ===.)

Oto inny sposób zarządzania listą oddzwonień. Zwróć obiekt rejestracji z addCallbacki użyj obiektu rejestracji, aby usunąć wywołanie zwrotne. Tutaj w 2020 roku możemy użyć Kombinatu AnyCancellablejako rejestracji.

Zmieniony interfejs API wygląda następująco:

class CallbackList {
    private var callbacks: [NSObject: () -> ()] = [:]

    func call() {
        callbacks.values.forEach { $0() }
    }

    func addCallback(_ callback: @escaping () -> ()) -> AnyCancellable {
        let key = NSObject()
        callbacks[key] = callback
        return .init { self.callbacks.removeValue(forKey: key) }
    }
}

Teraz, gdy dodajesz oddzwonienie, nie musisz go przechowywać, aby przejść na removeCallbackpóźniej. Nie ma removeCallbackmetody. Zamiast tego zapisz AnyCancellablei wywołaj jego cancelmetodę, aby usunąć wywołanie zwrotne. Co więcej, jeśli przechowujesz właściwość AnyCancellablein a instance, automatycznie anuluje się ona po zniszczeniu instancji.

rob mayoff
źródło
Najczęstszym powodem, dla którego potrzebujemy, jest zarządzanie wieloma subskrybentami dla wydawców. Połącz rozwiązuje to bez tego wszystkiego. C # pozwala, a Swift nie pozwala, to dowiedzieć się, czy dwa zamknięcia odwołują się do tej samej nazwanej funkcji. Jest to również przydatne, ale znacznie rzadziej.
Jessy