Comment testez-vous l'égalité des fonctions et des fermetures?

88

Le livre dit que "les fonctions et les fermetures sont des types de référence". Alors, comment savoir si les références sont égales? == et === ne fonctionnent pas.

func a() { }
let å = a
let b = å === å // Could not find an overload for === that accepts the supplied arguments
Jessy
la source
5
Pour autant que je sache, vous ne pouvez pas non plus vérifier l'égalité des métaclasses (par exemple MyClass.self)
Jiaaro
Il ne devrait pas être nécessaire de comparer deux fermetures d'identité. Pouvez-vous donner un exemple de ce que vous feriez? Il pourrait y avoir une solution alternative.
Projet de loi le
1
Fermetures multicast, à la C #. Ils sont forcément plus laids dans Swift, car vous ne pouvez pas surcharger l '"opérateur" (T, U), mais nous pouvons toujours les créer nous-mêmes. Sans pouvoir supprimer les fermetures d'une liste d'appels par référence, cependant, nous devons créer notre propre classe wrapper. C'est un frein et cela ne devrait pas être nécessaire.
Jessy
2
Excellente question, mais chose totalement distincte: votre utilisation d'un diacritique åcomme référence aest vraiment intéressante. Y a-t-il une convention que vous explorez ici? (Je ne sais pas si je l'aime vraiment ou non; mais il semble que cela pourrait être très puissant, en particulier en programmation fonctionnelle pure.)
Rob Napier
2
@Bill Je stocke les fermetures dans un tableau et je ne peux pas utiliser indexOf ({$ 0 == fermeture} pour les trouver et les supprimer. Maintenant, je dois restructurer mon code en raison d'une optimisation que je pense être une mauvaise conception du langage.
Zack Morris

Réponses:

72

Chris Lattner a écrit sur les forums des développeurs:

C'est une fonctionnalité que nous ne voulons pas prendre en charge intentionnellement. Il existe une variété de choses qui entraîneront l'échec ou le changement de l'égalité des pointeurs des fonctions (au sens du système de type swift, qui comprend plusieurs types de fermetures) en fonction de l'optimisation. Si "===" était défini sur des fonctions, le compilateur ne serait pas autorisé à fusionner des corps de méthode identiques, à partager des thunks et à effectuer certaines optimisations de capture dans les fermetures. En outre, une égalité de ce type serait extrêmement surprenante dans certains contextes génériques, où vous pouvez obtenir des thunks de réobstruction qui ajustent la signature réelle d'une fonction à celle attendue par le type de fonction.

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

Cela signifie que vous ne devriez même pas essayer de comparer les fermetures pour l'égalité, car les optimisations peuvent affecter le résultat.

dessiner
la source
18
Cela m'a juste mordu, ce qui était un peu dévastateur parce que j'avais stocké des fermetures dans un tableau et que je ne peux plus les supprimer avec indexOf ({$ 0 == fermeture} donc je dois refactoriser. L'optimisation IMHO ne devrait pas influencer la conception du langage, donc sans une solution rapide comme le @objc_block désormais obsolète dans la réponse de matt, je dirais que Swift ne peut pas stocker et récupérer correctement les fermetures pour le moment. Je ne pense donc pas qu'il soit approprié de préconiser l'utilisation de Swift dans le code lourd de rappel comme le genre rencontré dans le développement Web. C'est la raison pour laquelle nous sommes passés à Swift en premier lieu ...
Zack Morris
4
@ZackMorris Stockez une sorte d'identifiant avec la fermeture afin de pouvoir la supprimer plus tard. Si vous utilisez des types de référence, vous pouvez simplement stocker une référence à l'objet, sinon vous pouvez créer votre propre système d'identification. Vous pouvez même concevoir un type qui a une fermeture et un identifiant unique que vous pouvez utiliser à la place d'une fermeture simple.
tirage du
5
@drewag Oui, il existe des solutions de contournement, mais Zack a raison. C'est vraiment vraiment nul. Je comprends que vous souhaitiez avoir des optimisations, mais s'il y a quelque part dans le code que le développeur a besoin de comparer certaines fermetures, alors demandez simplement au compilateur d'optimiser ces sections particulières. Ou créez une sorte de fonction supplémentaire du compilateur qui lui permet de créer des signatures d'égalité qui ne rompent pas avec les optimisations effrayantes. C'est Apple dont nous parlons ici ... s'ils peuvent installer un Xeon dans un iMac, ils peuvent certainement rendre les fermetures comparables. Laisse-moi tranquille!
CommaToast
10

J'ai beaucoup cherché. Il ne semble y avoir aucun moyen de comparer les pointeurs de fonction. La meilleure solution que j'ai obtenue est d'encapsuler la fonction ou la fermeture dans un objet hachable. Comme:

var handler:Handler = Handler(callback: { (message:String) in
            //handler body
}))
tuncay
la source
2
C'est, de loin, la meilleure approche. Ça craint de devoir envelopper et déballer les fermetures, mais c'est mieux qu'une fragilité non déterministe et non prise en charge.
8

Le moyen le plus simple est de désigner le type de bloc comme @objc_block, et vous pouvez maintenant le convertir en un AnyObject comparable à ===. Exemple:

    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
mat
la source
Hé, j'essaie si unsafeBitCast (auditeur, AnyObject.self) === unsafeBitCast (f, AnyObject.self) mais j'obtiens une erreur fatale: impossible unsafeBitCast entre les types de tailles différentes. L'idée est de construire un système basé sur les événements, mais la méthode removeEventListener devrait être capable de vérifier les pointeurs de fonction.
freezing_
2
Utilisez @convention (block) au lieu de @objc_block sur Swift 2.x. Très bonne réponse!
Gabriel.Massana
6

J'ai aussi cherché la réponse. Et je l'ai enfin trouvé.

Ce dont vous avez besoin est le pointeur de fonction réel et son contexte cachés dans l'objet de fonction.

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
}

Et voici la démo:

// 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)")

Consultez les URL ci-dessous pour savoir pourquoi et comment cela fonctionne:

Comme vous le voyez, il est capable de vérifier uniquement l'identité (le deuxième test donne false). Mais cela devrait suffire.

Dankogai
la source
5
Cette méthode ne sera pas fiable avec les optimisations du compilateur devforums.apple.com/message/1035180#1035180
drewag
8
Il s'agit d'un hack basé sur des détails d'implémentation non définis. Ensuite, utiliser cela signifie que votre programme produira un résultat indéfini.
eonil
8
Notez que cela repose sur des éléments non documentés et des détails d'implémentation non divulgués, qui peuvent planter votre application à l'avenir s'ils changent. Non recommandé pour être utilisé dans le code de production.
Cristik
C'est du "trèfle", mais totalement irréalisable. Je ne sais pas pourquoi cela a été récompensé par une prime. Le langage n'a intentionnellement pas d'égalité de fonction, dans le but exact de libérer le compilateur pour qu'il rompe librement l'égalité de fonction afin de produire de meilleures optimisations.
Alexander - Réintègre Monica le
... et c'est exactement l'approche que Chris Lattner préconise (voir la réponse du haut).
pipacs
4

C'est une excellente question et bien que Chris Lattner ne veuille intentionnellement pas supporter cette fonctionnalité, je ne peux pas, comme beaucoup de développeurs, abandonner mes sentiments venant d'autres langues où c'est une tâche triviale. Il y a beaucoup d' unsafeBitCastexemples, la plupart d'entre eux ne montrent pas l'image complète, en voici un plus détaillé :

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 partie intéressante est la façon dont Swift convertit librement SwfBlock en ObjBlock, mais en réalité, deux blocs SwfBlock coulés auront toujours des valeurs différentes, contrairement à ObjBlocks. Lorsque nous convertissons ObjBlock en SwfBlock, la même chose leur arrive, ils deviennent deux valeurs différentes. Ainsi, afin de préserver la référence, ce type de casting doit être évité.

Je comprends toujours tout ce sujet, mais une chose que j'ai laissée espérer est la possibilité d'utiliser @convention(block)des méthodes de classe / structure, j'ai donc déposé une demande de fonctionnalité qui nécessite un vote ascendant ou expliquant pourquoi c'est une mauvaise idée. J'ai aussi le sentiment que cette approche pourrait être mauvaise dans l'ensemble, si oui, quelqu'un peut-il expliquer pourquoi?

Ian Bytchek
la source
1
Je ne pense pas que vous compreniez le raisonnement de Chris Latner quant à la raison pour laquelle cela n'est pas (et ne devrait pas être) pris en charge. "J'ai aussi le sentiment que cette approche pourrait être mauvaise dans l'ensemble, si c'est le cas, quelqu'un peut-il expliquer pourquoi?" Parce que dans une version optimisée, le compilateur est libre de modifier le code de nombreuses manières qui rompent l'idée d'égalité de points des fonctions. Pour un exemple de base, si le corps d'une fonction démarre de la même manière qu'une autre fonction, le compilateur est susceptible de chevaucher les deux dans le code machine, ne conservant que des points de sortie différents. Cela réduit la duplication
Alexander - Réintègre Monica le
1
Fondamentalement, les fermetures sont des moyens d'initier des objets de classes anonymes (tout comme en Java, mais c'est plus évident). Ces objets de fermeture sont alloués en tas et stockent les données capturées par la fermeture, qui agissent comme des paramètres implicites de la fonction de fermeture. L'objet de fermeture contient une référence à une fonction qui opère sur les arguments explicites (via func args) et implicites (via le contexte de fermeture capturé). Alors que le corps de la fonction peut être partagé en tant que point unique unique, le pointeur de l'objet de fermeture ne peut pas l' être, car il existe un objet de fermeture par ensemble de valeurs incluses.
Alexander - Réintègre Monica le
1
Ainsi, lorsque vous avez Struct S { func f(_: Int) -> Bool }, vous avez en fait une fonction de type S.fqui a un type (S) -> (Int) -> Bool. Cette fonction peut être partagée. Il est uniquement paramétré par ses paramètres explicites. Lorsque vous l'utilisez comme méthode d'instance (soit en liant implicitement le selfparamètre en appelant la méthode sur un objet, par exemple S().f, soit en la liant explicitement, par exemple S.f(S())), vous créez un nouvel objet de fermeture. Cet objet stocke un pointeur vers S.f(qui peut être partagé) , but also to your instance (self , the S () `).
Alexander - Réintègre Monica le
1
Cet objet de fermeture doit être unique par instance de S. Si l'égalité du pointeur de fermeture était possible, alors vous seriez surpris de découvrir que ce s1.fn'est pas le même pointeur que s2.f(car l'un est un objet de fermeture qui fait référence à s1et f, et l'autre est un objet de fermeture qui référence s2et f).
Alexander - Réintègre Monica le
C'est génial, merci! Oui, j'avais maintenant une image de ce qui se passait et cela met tout en perspective! 👍
Ian Bytchek
4

Voici une solution possible (conceptuellement identique à la réponse «tuncay»). Le but est de définir une classe qui englobe certaines fonctionnalités (par exemple, Command):

Rapide:

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
la source
Ce serait beaucoup mieux si vous le faisiez générique.
Alexander - Réintègre Monica le
2

Eh bien, cela fait 2 jours et personne n'a proposé de solution, je vais donc changer mon commentaire en réponse:

Pour autant que je sache, vous ne pouvez pas vérifier l'égalité ou l'identité des fonctions (comme votre exemple) et des métaclasses (par exemple MyClass.self):

Mais - et ce n'est qu'une idée - je ne peux m'empêcher de remarquer que la whereclause des génériques semble pouvoir vérifier l'égalité des types. Alors peut-être pouvez-vous en tirer parti, au moins pour vérifier votre identité?

Jiaaro
la source
2

Ce n'est pas une solution générale, mais si l'on essaie d'implémenter un modèle d'écoute, j'ai fini par renvoyer un "id" de la fonction lors de l'enregistrement afin que je puisse l'utiliser pour me désinscrire plus tard (ce qui est une sorte de solution de contournement à la question d'origine pour les "auditeurs", le désenregistrement revient généralement à vérifier l'égalité des fonctions, ce qui n'est au moins pas "trivial" comme pour d'autres réponses).

Donc quelque chose comme ça:

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

Il ne vous reste plus qu'à stocker le key retourné par la fonction "register" et à le transmettre lors de la désinscription.

virus
la source
0

Ma solution était d'encapsuler des fonctions dans une classe qui étend NSObject

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

    init(_ function: @escaping (Type) -> Void) {
        value = function
    }
}
Renetik
la source
Quand vous faites cela, comment les comparer? disons que vous souhaitez supprimer l'un d'entre eux d'un tableau de vos wrappers, comment faites-vous cela? Merci.
Ricardo
0

Je sais que je réponds à cette question avec six ans de retard, mais je pense qu'il vaut la peine d'examiner la motivation derrière la question. L'intervenant a commenté:

Sans pouvoir supprimer les fermetures d'une liste d'appels par référence, cependant, nous devons créer notre propre classe wrapper. C'est un frein et cela ne devrait pas être nécessaire.

Donc, je suppose que l'interlocuteur veut maintenir une liste de rappel, comme ceci:

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

Mais nous ne pouvons pas écrire de removeCallbackcette façon, car ==cela ne fonctionne pas pour les fonctions. (Ni l'un ni l'autre ===.)

Voici une autre façon de gérer votre liste de rappel. Renvoyez un objet d'inscription à partir de addCallbacket utilisez l'objet d'inscription pour supprimer le rappel. Ici en 2020, nous pouvons utiliser le Combine AnyCancellablecomme enregistrement.

L'API révisée ressemble à ceci:

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

Désormais, lorsque vous ajoutez un rappel, vous n'avez pas besoin de le conserver pour le transmettre removeCallbackplus tard. Il n'y a pas de removeCallbackméthode. Au lieu de cela, vous enregistrez le AnyCancellableet appelez sa cancelméthode pour supprimer le rappel. Mieux encore, si vous stockez la AnyCancellablepropriété dans une instance, elle s'annulera automatiquement lorsque l'instance sera détruite.

Rob Mayoff
la source
La raison la plus courante pour laquelle nous en avons besoin est de gérer plusieurs abonnés pour les éditeurs. Combine résout cela sans tout cela. Ce que C # permet, et Swift ne le permet pas, c'est de savoir si deux fermetures font référence à la même fonction nommée. C'est également utile, mais beaucoup moins souvent.
Jessy