Come testate le funzioni e le chiusure per l'uguaglianza?


88

Il libro dice che "le funzioni e le chiusure sono tipi di riferimento". Allora, come fai a scoprire se i riferimenti sono uguali? == e === non funzionano.

func a() { }
let å = a
let b = å === å // Could not find an overload for === that accepts the supplied arguments

5
Per quanto ne so, non puoi nemmeno controllare l'uguaglianza delle metaclassi (ad esempio MyClass.self)
Jiaaro

Non dovrebbe essere necessario confrontare due chiusure per l'identità. Puoi fare un esempio di dove lo faresti? Potrebbe esserci una soluzione alternativa.
Bill

1
Chiusure multicast, a la C #. Sono necessariamente più brutti in Swift, perché non puoi sovraccaricare l '"operatore" (T, U), ma possiamo comunque crearli noi stessi. Senza essere in grado di rimuovere le chiusure da un elenco di chiamate per riferimento, tuttavia, dobbiamo creare la nostra classe wrapper. Questa è una seccatura e non dovrebbe essere necessaria.
Jessy

2
Ottima domanda, ma cosa totalmente separata: il tuo uso di un segno diacritico per åfare riferimento aè davvero interessante. C'è una convenzione che stai esplorando qui? (Non so se mi piaccia o no; ma sembra che potrebbe essere molto potente, specialmente nella pura programmazione funzionale.)
Rob Napier,

2
@Bill Sto memorizzando le chiusure in un Array e non posso usare indexOf ({$ 0 == closing} per trovarle e rimuoverle. Ora devo ristrutturare il mio codice a causa dell'ottimizzazione che credo sia un linguaggio di progettazione scadente.
Zack Morris

Risposte:


72

Chris Lattner ha scritto sui forum degli sviluppatori:

Questa è una funzionalità che intenzionalmente non vogliamo supportare. Ci sono una varietà di cose che causeranno il fallimento o la modifica dell'uguaglianza delle funzioni del puntatore (nel senso di sistema di tipo rapido, che include diversi tipi di chiusure) a seconda dell'ottimizzazione. Se "===" fosse definito sulle funzioni, al compilatore non sarebbe consentito unire corpi di metodo identici, condividere thunk ed eseguire determinate ottimizzazioni di acquisizione nelle chiusure. Inoltre, l'uguaglianza di questo tipo sarebbe estremamente sorprendente in alcuni contesti generici, in cui è possibile ottenere thunk di riabstrazione che regolano la firma effettiva di una funzione a quella prevista dal tipo di funzione.

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

Ciò significa che non dovresti nemmeno provare a confrontare le chiusure per l'uguaglianza perché le ottimizzazioni possono influenzare il risultato.


19
Questo mi ha morso, il che è stato un po 'devastante perché stavo memorizzando le chiusure in un Array e ora non posso rimuoverle con indexOf ({$ 0 == closing} quindi devo rifattorizzare. L'ottimizzazione IMHO non dovrebbe influenzare la progettazione del linguaggio, quindi senza una soluzione rapida come l'ormai deprecato @objc_block nella risposta di matt, direi che Swift non può archiviare e recuperare correttamente le chiusure in questo momento. Quindi non penso sia appropriato sostenere l'uso di Swift nel codice pesante di callback come il tipo riscontrato nello sviluppo web. Questa è stata l'intera ragione per cui siamo passati a Swift in primo luogo ...
Zack Morris,

4
@ZackMorris Memorizza una sorta di identificatore con la chiusura in modo da poterlo rimuovere in seguito. Se stai usando i tipi di riferimento puoi semplicemente memorizzare un riferimento all'oggetto altrimenti puoi inventare il tuo sistema di identificazione. Potresti persino progettare un tipo che abbia una chiusura e un identificatore univoco che puoi usare invece di una semplice chiusura.
pareggiato il

5
@drewag Sì, ci sono soluzioni alternative, ma Zack ha ragione. Questo è davvero davvero noioso. Capisco che vogliano avere ottimizzazioni, ma se c'è da qualche parte nel codice che lo sviluppatore deve confrontare alcune chiusure, allora semplicemente il compilatore non ottimizza quelle particolari sezioni. Oppure crea una sorta di funzione aggiuntiva del compilatore che gli consenta di creare firme di uguaglianza che non si rompono con ottimizzazioni folli. Stiamo parlando di Apple qui ... se possono adattare uno Xeon a un iMac, possono sicuramente rendere le chiusure comparabili. Dammi una pausa!
CommaToast

10

Ho cercato molto. Non sembra esserci alcun modo per confrontare il puntatore di funzione. La soluzione migliore che ho ottenuto è incapsulare la funzione o la chiusura in un oggetto hashable. Piace:

var handler:Handler = Handler(callback: { (message:String) in
            //handler body
}))

2
Questo è di gran lunga l'approccio migliore. Fa schifo dover avvolgere e scartare chiusure, ma è meglio della fragilità non deterministica e non supportata.

8

Il modo più semplice è designare il tipo di blocco come @objc_block, e ora puoi lanciarlo a un AnyObject che è paragonabile a ===. Esempio:

    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

Ehi, sto provando se unsafeBitCast (listener, AnyObject.self) === unsafeBitCast (f, AnyObject.self) ma ottengo un errore fatale: non può unsafeBitCast tra tipi di dimensioni diverse. L'idea è di costruire un sistema basato sugli eventi ma il metodo removeEventListener dovrebbe essere in grado di controllare i puntatori alle funzioni.
congelamento_

2
Usa @convention (block) invece di @objc_block su Swift 2.x. Bella risposta!
Gabriel.Massana

6

Anch'io ho cercato la risposta. E finalmente l'ho trovato.

Ciò di cui hai bisogno è il puntatore alla funzione effettivo e il suo contesto nascosto nell'oggetto funzione.

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
}

Ed ecco la 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)")

Vedere gli URL di seguito per scoprire perché e come funziona:

Come vedi, è in grado di controllare solo l'identità (il 2 ° test restituisce false). Ma dovrebbe essere abbastanza buono.


5
Questo metodo non sarà affidabile con le ottimizzazioni del compilatore devforums.apple.com/message/1035180#1035180
disegnato il

8
Si tratta di un hack basato su dettagli di implementazione non definiti. Quindi usare questo significa che il tuo programma produrrà un risultato indefinito.
eonil

8
Tieni presente che questo si basa su materiale non documentato e dettagli di implementazione non divulgati, che possono causare l'arresto anomalo della tua app in futuro se cambiano. Non consigliato per essere utilizzato nel codice di produzione.
Cristik

Questo è "trifoglio", ma completamente impraticabile. Non so perché questo sia stato ricompensato con una taglia. Il linguaggio intenzionalmente non ha l'uguaglianza delle funzioni, allo scopo esatto di liberare il compilatore di rompere liberamente l'uguaglianza delle funzioni per ottenere migliori ottimizzazioni.
Alexander

... e questo è esattamente l'approccio contro cui Chris Lattner sostiene (vedi la risposta in alto).
pipacs

4

Questa è una grande domanda e anche se Chris Lattner non vuole intenzionalmente supportare questa funzione, io, come molti sviluppatori, non posso lasciare andare i miei sentimenti provenienti da altre lingue in cui questo è un compito banale. Ci sono molti unsafeBitCastesempi, la maggior parte di loro non mostra il quadro completo, eccone uno più dettagliato :

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

La parte interessante è come swift lancia liberamente SwfBlock su ObjBlock, ma in realtà due blocchi SwfBlock lanciati avranno sempre valori diversi, mentre ObjBlocks no. Quando lanciamo ObjBlock a SwfBlock, succede loro la stessa cosa, diventano due valori diversi. Quindi, al fine di preservare il riferimento, questo tipo di fusione dovrebbe essere evitato.

Sto ancora comprendendo l'intero argomento, ma una cosa che ho lasciato desiderare è la capacità di utilizzare @convention(block)sui metodi class / struct, quindi ho presentato una richiesta di funzionalità che necessita di un voto positivo o di spiegare perché è una cattiva idea. Ho anche la sensazione che questo approccio potrebbe essere cattivo tutto insieme, se è così, qualcuno può spiegare perché?


1
Non credo che tu capisca il ragionamento di Chris Latner sul motivo per cui questo non è (e non dovrebbe essere) supportato. "Ho anche la sensazione che questo approccio potrebbe essere negativo tutti insieme, se è così, qualcuno può spiegare perché?" Perché in una build ottimizzata, il compilatore è libero di manipolare il codice in molti modi che rompono l'idea dell'uguaglianza dei punti delle funzioni. Per un esempio di base, se il corpo di una funzione si avvia allo stesso modo di un'altra funzione, è probabile che il compilatore si sovrapponga ai due nel codice macchina, mantenendo solo punti di uscita diversi. Ciò riduce la duplicazione
Alexander

1
Fondamentalmente, le chiusure sono modi per avviare oggetti di classi anonime (proprio come in Java, ma è più ovvio). Questi oggetti di chiusura vengono allocati nell'heap e memorizzano i dati acquisiti dalla chiusura, che agiscono come parametri impliciti per la funzione di chiusura. L'oggetto di chiusura contiene un riferimento a una funzione che opera sugli argomenti espliciti (tramite func args) e impliciti (tramite il contesto di chiusura catturato). Mentre il corpo della funzione può essere condiviso come un singolo punto univoco, il puntatore dell'oggetto di chiusura non può esserlo, perché c'è un oggetto di chiusura per insieme di valori racchiusi.
Alexander

1
Quindi, quando lo hai Struct S { func f(_: Int) -> Bool }, hai effettivamente una funzione di tipo S.fche ha tipo (S) -> (Int) -> Bool. Questa funzione può essere condivisa. È parametrizzato esclusivamente dai suoi parametri espliciti. Quando lo si utilizza come metodo di istanza (o legando implicitamente il selfparametro chiamando il metodo su un oggetto, ad esempio S().f, o legandolo esplicitamente, ad esempio S.f(S())), si crea un nuovo oggetto di chiusura. Questo oggetto memorizza un puntatore a S.f(che può essere condiviso) , but also to your instance (self , the S () `).
Alexander

1
Questo oggetto di chiusura deve essere univoco per istanza di S. Se fosse possibile l'uguaglianza del puntatore di chiusura, sareste sorpresi di scoprire che s1.fnon è lo stesso puntatore di s2.f(perché uno è un oggetto di chiusura che fa riferimento a s1e f, e l'altro è un oggetto di chiusura che fa riferimento a s2e f).
Alexander

È fantastico, grazie! Sì, ormai avevo una foto di quello che sta succedendo e questo mette tutto in prospettiva! 👍
Ian Bytchek

4

Ecco una possibile soluzione (concettualmente la stessa della risposta "tuncay"). Il punto è definire una classe che racchiuda alcune funzionalità (ad esempio Command):

Swift:

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

Giava:

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

Sarebbe molto meglio se lo rendessi generico.
Alexander

2

Sono passati 2 giorni e nessuno è intervenuto con una soluzione, quindi cambierò il mio commento in una risposta:

Per quanto ne so, non puoi controllare l'uguaglianza o l'identità di funzioni (come il tuo esempio) e metaclassi (ad es. MyClass.self):

Ma - e questa è solo un'idea - non posso fare a meno di notare che la whereclausola dei generici sembra essere in grado di controllare l'uguaglianza dei tipi. Quindi forse puoi sfruttarlo, almeno per controllare l'identità?


2

Non è una soluzione generale, ma se si sta tentando di implementare un modello di ascolto, ho finito per restituire un "id" della funzione durante la registrazione in modo da poterlo utilizzare per annullare la registrazione in un secondo momento (cheèuna sorta di soluzione alternativa alla domanda originale per il caso "ascoltatori", come di solito l'annullamento della registrazione si riduce al controllo dell'uguaglianza delle funzioni, che almeno non è "banale" come per altre risposte).

Quindi qualcosa del genere:

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

Ora devi solo memorizzare quanto keyrestituito dalla funzione "register" e passarlo all'annullamento della registrazione.


0

La mia soluzione era racchiudere le funzioni in una classe che estende NSObject

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

    init(_ function: @escaping (Type) -> Void) {
        value = function
    }
}

Quando lo fai, come li confronti? diciamo che vuoi rimuovere uno di loro da un array dei tuoi wrapper, come fai? Grazie.
Ricardo

0

So che sto rispondendo a questa domanda con sei anni di ritardo, ma penso che valga la pena guardare la motivazione dietro la domanda. L'interrogante ha commentato:

Senza essere in grado di rimuovere le chiusure da un elenco di invocazioni per riferimento, tuttavia, dobbiamo creare la nostra classe wrapper. Questa è una seccatura e non dovrebbe essere necessaria.

Quindi immagino che l'interrogante voglia mantenere un elenco di richiamate, come questo:

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

Ma non possiamo scrivere in removeCallbackquesto modo, perché ==non funziona per le funzioni. (Nemmeno lo fa ===.)

Ecco un modo diverso per gestire il tuo elenco di richiamate. Restituire un oggetto di registrazione da addCallbacke utilizzare l'oggetto di registrazione per rimuovere il callback. Qui nel 2020, possiamo usare le Combine AnyCancellablecome registrazione.

L'API rivista ha questo aspetto:

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

Ora, quando aggiungi una richiamata, non è necessario tenerla in giro per passare a una removeCallbacksuccessiva. Non esiste un removeCallbackmetodo. Invece, si salva il AnyCancellablee si chiama il suo cancelmetodo per rimuovere il callback. Ancora meglio, se memorizzi la AnyCancellableproprietà in un'istanza, questa si annullerà automaticamente quando l'istanza viene distrutta.


Il motivo più comune per cui abbiamo bisogno di questo è gestire più abbonati per gli editori. Combine risolve che senza tutto questo. Ciò che C # consente, e Swift no, è scoprire se due chiusure fanno riferimento alla stessa funzione denominata. Anche questo è utile, ma molto meno spesso.
Jessy il
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.