Wie testen Sie Funktionen und Verschlüsse auf Gleichheit?

87

Das Buch sagt, dass "Funktionen und Verschlüsse Referenztypen sind". Wie finden Sie heraus, ob die Referenzen gleich sind? == und === funktionieren nicht.

func a() { }
let å = a
let b = å === å // Could not find an overload for === that accepts the supplied arguments
Jessy
quelle
5
Soweit ich das MyClass.self
beurteilen
Es sollte nicht notwendig sein, zwei Verschlüsse auf Identität zu vergleichen. Können Sie ein Beispiel geben, wo Sie dies tun würden? Möglicherweise gibt es eine alternative Lösung.
Bill
1
Multicast-Schließungen, a la C #. Sie sind in Swift notwendigerweise hässlicher, weil Sie den (T, U) "Operator" nicht überladen können, aber wir können sie trotzdem selbst erstellen. Ohne Verschlüsse aus einer Aufrufliste durch Referenz entfernen zu können, müssen wir jedoch unsere eigene Wrapper-Klasse erstellen. Das ist eine Belastung und sollte nicht notwendig sein.
Jessy
2
Gute Frage, aber völlig andere Sache: Ihre Verwendung eines diakritischen åHinweises aist wirklich interessant. Gibt es eine Konvention, die Sie hier erkunden? (Ich weiß nicht, ob es mir wirklich gefällt oder nicht; aber es scheint sehr leistungsfähig zu sein, insbesondere in der reinen funktionalen Programmierung.)
Rob Napier
2
@Bill Ich speichere Closures in einem Array und kann indexOf ({$ 0 == Closure} nicht verwenden, um sie zu finden und zu entfernen. Jetzt muss ich meinen Code aufgrund von Optimierungen umstrukturieren, die meiner Meinung nach ein schlechtes Sprachdesign sind.
Zack Morris

Antworten:

72

Chris Lattner schrieb in den Entwicklerforen:

Dies ist eine Funktion, die wir absichtlich nicht unterstützen möchten. Es gibt eine Vielzahl von Faktoren, die dazu führen, dass die Gleichheit der Funktionen von Zeigern (im Sinne eines schnellen Systems, das verschiedene Arten von Verschlüssen umfasst) je nach Optimierung fehlschlägt oder sich ändert. Wenn "===" für Funktionen definiert wäre, könnte der Compiler keine identischen Methodenkörper zusammenführen, Thunks gemeinsam nutzen und bestimmte Erfassungsoptimierungen in Closures durchführen. Darüber hinaus wäre eine solche Gleichheit in einigen generischen Kontexten äußerst überraschend, in denen Sie Reabstraktions-Thunks erhalten können, die die tatsächliche Signatur einer Funktion an die vom Funktionstyp erwartete anpassen.

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

Dies bedeutet, dass Sie nicht einmal versuchen sollten, Abschlüsse auf Gleichheit zu vergleichen, da Optimierungen das Ergebnis beeinflussen können.

Drawag
quelle
18
Das hat mich nur gebissen, was ziemlich verheerend war, weil ich Closures in einem Array gespeichert hatte und sie jetzt nicht mit indexOf ({$ 0 == Closure} entfernen kann, also muss ich umgestalten. IMHO-Optimierung sollte das Sprachdesign nicht beeinflussen, Ohne eine schnelle Lösung wie den jetzt veralteten @objc_block in der Antwort von matt würde ich argumentieren, dass Swift derzeit keine Verschlüsse ordnungsgemäß speichern und abrufen kann. Daher halte ich es nicht für angebracht, die Verwendung von Swift in Callback-Code zu befürworten Das war der ganze Grund, warum wir überhaupt zu Swift gewechselt sind ...
Zack Morris
4
@ZackMorris Speichern Sie eine Art Kennung mit dem Verschluss, damit Sie sie später entfernen können. Wenn Sie Referenztypen verwenden, können Sie einfach eine Referenz auf das Objekt speichern, andernfalls können Sie ein eigenes Identifikationssystem erstellen. Sie können sogar einen Typ entwerfen, der einen Verschluss und eine eindeutige Kennung hat, die Sie anstelle eines einfachen Verschlusses verwenden können.
Drawag
5
@rewag Ja, es gibt Problemumgehungen, aber Zack hat recht. Das ist wirklich sehr, sehr lahm. Ich verstehe, dass ich Optimierungen haben möchte, aber wenn der Entwickler irgendwo im Code einige Abschlüsse vergleichen muss, muss der Compiler diese bestimmten Abschnitte einfach nicht optimieren. Oder machen Sie eine zusätzliche Funktion des Compilers, die es ihm ermöglicht, Gleichheitssignaturen zu erstellen, die nicht mit verdammten Optimierungen brechen. Hier geht es um Apple ... Wenn sie einen Xeon in einen iMac einbauen können, können sie Verschlüsse durchaus vergleichbar machen. Gib mir eine Pause!
CommaToast
10

Ich habe viel gesucht. Es scheint keine Möglichkeit zum Vergleich von Funktionszeigern zu geben. Die beste Lösung besteht darin, die Funktion oder den Abschluss in einem hashbaren Objekt zu kapseln. Mögen:

var handler:Handler = Handler(callback: { (message:String) in
            //handler body
}))
tuncay
quelle
2
Dies ist bei weitem der beste Ansatz. Es ist schade, Verschlüsse ein- und auspacken zu müssen, aber es ist besser als nicht deterministische, nicht unterstützte Fragilität.
8

Einfachste Weg ist , den Blocktyp bezeichnen , wie @objc_block, und jetzt können Sie es zu einem ANYOBJECT gegossen , die mit vergleichbar ist ===. Beispiel:

    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
matt
quelle
Hey, ich versuche, wenn unsafeBitCast (Listener, AnyObject.self) === unsafeBitCast (f, AnyObject.self), erhalte aber einen schwerwiegenden Fehler: Kann unsafeBitCast nicht zwischen Typen unterschiedlicher Größe entfernen. Die Idee ist, ein ereignisbasiertes System zu erstellen, aber die removeEventListener-Methode sollte in der Lage sein, die Funktionszeiger zu überprüfen.
Einfrieren_
2
Verwenden Sie in Swift 2.x @convention (block) anstelle von @objc_block. Gute Antwort!
Gabriel.Massana
6

Ich habe auch nach der Antwort gesucht. Und ich habe es endlich gefunden.

Was Sie brauchen, ist der eigentliche Funktionszeiger und sein im Funktionsobjekt versteckter Kontext.

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
}

Und hier ist die 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)")

In den folgenden URLs erfahren Sie, warum und wie es funktioniert:

Wie Sie sehen, kann nur die Identität überprüft werden (der 2. Test ergibt false). Das sollte aber gut genug sein.

dankogai
quelle
4
Diese Methode wird mit Compiler-Optimierungen nicht zuverlässig sein devforums.apple.com/message/1035180#1035180
drawag
7
Dies ist ein Hack, der auf undefinierten Implementierungsdetails basiert. Wenn Sie dies verwenden, erzeugt Ihr Programm ein undefiniertes Ergebnis.
Eonil
7
Beachten Sie, dass dies auf undokumentierten Inhalten und nicht genannten Implementierungsdetails beruht, die Ihre App in Zukunft zum Absturz bringen können, wenn sie sich ändern. Nicht zur Verwendung im Produktionscode empfohlen.
Cristik
Dies ist "Klee", aber völlig unbrauchbar. Ich weiß nicht, warum dies als Kopfgeld belohnt wurde. Die Sprache hat absichtlich keine Funktionsgleichheit, um den Compiler frei zu machen , die Funktionsgleichheit frei zu brechen , um bessere Optimierungen zu erzielen.
Alexander - Stellen Sie Monica am
... und genau gegen diesen Ansatz spricht sich Chris Lattner aus (siehe die obere Antwort).
Pipacs
4

Dies ist eine großartige Frage, und obwohl Chris Lattner diese Funktion absichtlich nicht unterstützen möchte, kann ich, wie viele Entwickler, auch meine Gefühle aus anderen Sprachen nicht loslassen, in denen dies eine triviale Aufgabe ist. Es gibt viele unsafeBitCastBeispiele, von denen die meisten nicht das vollständige Bild zeigen. Hier ist ein detaillierteres :

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

Der interessante Teil ist, wie schnell SwfBlock frei in ObjBlock umgewandelt wird, aber in Wirklichkeit haben zwei gegossene SwfBlock-Blöcke immer unterschiedliche Werte, während ObjBlocks dies nicht tun. Wenn wir ObjBlock in SwfBlock umwandeln, passiert ihnen dasselbe, sie werden zu zwei verschiedenen Werten. Um die Referenz zu erhalten, sollte diese Art des Gießens vermieden werden.

Ich verstehe das ganze Thema immer noch, aber eine Sache, die ich mir noch gewünscht habe, ist die Fähigkeit, @convention(block)Klassen- / Strukturmethoden zu verwenden. Deshalb habe ich eine Feature-Anfrage eingereicht, die eine Abstimmung erfordert oder erklärt, warum dies eine schlechte Idee ist. Ich habe auch das Gefühl, dass dieser Ansatz insgesamt schlecht sein könnte. Wenn ja, kann jemand erklären, warum?

Ian Bytchek
quelle
1
Ich glaube nicht, dass Sie Chris Latners Argumentation verstehen, warum dies nicht unterstützt wird (und nicht unterstützt werden sollte). "Ich habe auch das Gefühl, dass dieser Ansatz insgesamt schlecht sein könnte. Wenn ja, kann jemand erklären, warum?" Denn in einem optimierten Build kann der Compiler den Code auf viele Arten entstellen, die die Idee der Punktgleichheit von Funktionen brechen. Wenn beispielsweise der Hauptteil einer Funktion auf die gleiche Weise wie eine andere Funktion gestartet wird, überlappt der Compiler wahrscheinlich die beiden im Maschinencode und behält nur unterschiedliche Austrittspunkte bei. Dies reduziert Doppelarbeit
Alexander - Reinstate Monica
1
Grundsätzlich sind Schließungen Möglichkeiten, Objekte anonymer Klassen zu initiieren (genau wie in Java, aber es ist offensichtlicher). Diese Abschlussobjekte sind Heap-zugeordnet und speichern die vom Abschluss erfassten Daten, die für die Funktion des Abschlusses wie implizite Parameter wirken. Das Abschlussobjekt enthält einen Verweis auf eine Funktion, die die expliziten (über func args) und impliziten (über den erfassten Abschlusskontext) Argumente bearbeitet. Während der Funktionskörper als einzelner eindeutiger Punkt gemeinsam genutzt werden kann, kann der Zeiger des Abschlussobjekts nicht gemeinsam genutzt werden, da pro Satz eingeschlossener Werte ein Abschlussobjekt vorhanden ist.
Alexander - Stellen Sie Monica am
1
Wenn Sie also haben Struct S { func f(_: Int) -> Bool }, haben Sie tatsächlich eine Funktion vom Typ, S.fdie Typ hat (S) -> (Int) -> Bool. Diese Funktion kann gemeinsam genutzt werden. Es wird ausschließlich durch seine expliziten Parameter parametrisiert. Wenn Sie es als Instanzmethode verwenden (entweder indem Sie den selfParameter implizit binden, indem Sie die Methode für ein Objekt aufrufen, z. B. S().findem Sie es explizit binden, z. B. S.f(S())), erstellen Sie ein neues Abschlussobjekt. Dieses Objekt speichert einen Zeiger auf S.f(der gemeinsam genutzt werden kann) , but also to your instance (self , the S () `).
Alexander - Stellen Sie Monica am
1
Dieses Abschlussobjekt muss pro Instanz von eindeutig sein S. Wenn die Gleichheit des Abschlusszeigers möglich wäre, wären Sie überrascht zu entdecken, dass dies s1.fnicht derselbe Zeiger ist wie s2.f(weil eines ein Abschlussobjekt ist, das auf s1und verweist f, und das andere ein Abschlussobjekt ist, das auf s2und verweist f).
Alexander - Stellen Sie Monica am
Das ist genial, danke! Ja, jetzt hatte ich ein Bild von dem, was los ist und das bringt alles in eine Perspektive! 👍
Ian Bytchek
4

Hier ist eine mögliche Lösung (konzeptionell die gleiche wie die Antwort "tuncay"). Der Punkt besteht darin, eine Klasse zu definieren, die einige Funktionen einschließt (z. B. Befehl):

Schnell:

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

Java:

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
quelle
Dies wäre viel besser, wenn Sie es generisch machen würden.
Alexander - Stellen Sie Monica am
2

Nun, es sind 2 Tage vergangen und niemand hat eine Lösung gefunden, also werde ich meinen Kommentar in eine Antwort ändern:

Soweit ich das beurteilen kann, können Sie die Gleichheit oder Identität von Funktionen (wie in Ihrem Beispiel) und Metaklassen (z. B.) nicht überprüfen MyClass.self:

Aber - und das ist nur eine Idee - ich kann nicht anders, als zu bemerken, dass die whereKlausel in Generika in der Lage zu sein scheint, die Gleichheit der Typen zu überprüfen. Vielleicht können Sie das nutzen, zumindest um die Identität zu überprüfen?

Jiaaro
quelle
2

Keine allgemeine Lösung, aber wenn man versucht, ein Listener-Muster zu implementieren, habe ich während der Registrierung eine "ID" der Funktion zurückgegeben, damit ich sie später abmelden kann (was eine Art Problemumgehung für die ursprüngliche Frage darstellt Für den Fall "Zuhörer" kommt es normalerweise darauf an, die Funktionen auf Gleichheit zu überprüfen, was nach anderen Antworten zumindest nicht "trivial" ist.

Also so etwas wie das:

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

Jetzt müssen Sie nur noch die keyvon der Funktion "register" zurückgegebene Funktion speichern und beim Aufheben der Registrierung übergeben.

Virus
quelle
0

Meine Lösung bestand darin, Funktionen in eine Klasse zu verpacken, die NSObject erweitert

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

    init(_ function: @escaping (Type) -> Void) {
        value = function
    }
}
Renetik
quelle
Wie vergleichen Sie sie, wenn Sie das tun? Angenommen, Sie möchten einen von ihnen aus einem Array Ihrer Wrapper entfernen. Wie machen Sie das? Vielen Dank.
Ricardo
0

Ich weiß, dass ich diese Frage sechs Jahre zu spät beantworte, aber ich denke, es lohnt sich, die Motivation hinter der Frage zu betrachten. Der Fragesteller kommentierte:

Ohne Verschlüsse aus einer Aufrufliste als Referenz entfernen zu können, müssen wir jedoch unsere eigene Wrapper-Klasse erstellen. Das ist eine Belastung und sollte nicht notwendig sein.

Ich denke, der Fragesteller möchte eine Rückrufliste wie folgt führen:

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

Aber wir können nicht so schreiben removeCallback, weil ==es für Funktionen nicht funktioniert. (Auch nicht ===.)

Hier ist eine andere Möglichkeit, Ihre Rückrufliste zu verwalten. Geben Sie ein Registrierungsobjekt von zurück addCallbackund entfernen Sie den Rückruf mithilfe des Registrierungsobjekts. Hier im Jahr 2020 können wir die Kombinate AnyCancellableals Registrierung verwenden.

Die überarbeitete API sieht folgendermaßen aus:

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

Wenn Sie jetzt einen Rückruf hinzufügen, müssen Sie ihn nicht mehr behalten, um ihn removeCallbackspäter weiterzuleiten . Es gibt keine removeCallbackMethode. Stattdessen speichern Sie die AnyCancellableund rufen ihre cancelMethode auf, um den Rückruf zu entfernen. Noch besser, wenn Sie die AnyCancellableEigenschaft in einer Instanz speichern , bricht sie sich automatisch ab, wenn die Instanz zerstört wird.

Rob Mayoff
quelle
Der häufigste Grund dafür ist die Verwaltung mehrerer Abonnenten für Publisher. Kombinieren löst das ohne all das. Was C # erlaubt und Swift nicht, ist herauszufinden, ob zwei Verschlüsse auf dieselbe gleichnamige Funktion verweisen. Das ist auch nützlich, aber viel seltener.
Jessy