Como você testa funções e fechamentos para igualdade?

88

O livro diz que "funções e fechamentos são tipos de referência". Então, como você descobre se as referências são iguais? == e === não funcionam.

func a() { }
let å = a
let b = å === å // Could not find an overload for === that accepts the supplied arguments
Jessy
fonte
5
Pelo que eu posso dizer, você também não pode verificar a igualdade das metaclasses (por exemplo, MyClass.self)
Jiaaro
Não deveria ser necessário comparar dois fechamentos para identidade. Você pode dar um exemplo de onde você faria isso? Pode haver uma solução alternativa.
Bill
1
Fechamentos de multicast, a la C #. Eles são necessariamente mais feios no Swift, porque você não pode sobrecarregar o "operador" (T, U), mas ainda podemos criá-los. Sem poder remover fechamentos de uma lista de invocação por referência, no entanto, precisamos criar nossa própria classe de wrapper. Isso é uma chatice e não deveria ser necessário.
Jessy
2
Ótima pergunta, mas uma coisa totalmente separada: o uso de um diacrítico åpara referência aé realmente interessante. Existe uma convenção que você está explorando aqui? (Não sei se gosto ou não; mas parece que pode ser muito poderoso, especialmente em programação funcional pura.)
Rob Napier
2
@Bill Estou armazenando encerramentos em um Array e não posso usar indexOf ({$ 0 == closure} para localizá-los e removê-los. Agora, preciso reestruturar meu código devido à otimização, que acredito ser um projeto de linguagem ruim.
Zack Morris

Respostas:

72

Chris Lattner escreveu nos fóruns de desenvolvedores:

Esse é um recurso que intencionalmente não queremos oferecer suporte. Há uma variedade de coisas que farão com que a igualdade de funções do ponteiro (no sentido do sistema de tipo rápido, que inclui vários tipos de fechamentos) falhe ou mude dependendo da otimização. Se "===" fosse definido nas funções, o compilador não teria permissão para mesclar corpos de métodos idênticos, compartilhar thunks e realizar certas otimizações de captura em encerramentos. Além disso, a igualdade desse tipo seria extremamente surpreendente em alguns contextos genéricos, onde você pode obter thunks de reabstração que ajustam a assinatura real de uma função para aquela que o tipo de função espera.

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

Isso significa que você não deve nem tentar comparar fechamentos de igualdade, porque otimizações podem afetar o resultado.

Drewag
fonte
18
Isso me incomodou, o que foi meio devastador porque eu estava armazenando fechamentos em um Array e agora não consigo removê-los com indexOf ({$ 0 == closure}, então tenho que refatorar. A otimização IMHO não deve influenciar o design da linguagem, então, sem uma solução rápida como o agora obsoleto @objc_block na resposta de matt, eu diria que o Swift não pode armazenar e recuperar fechamentos corretamente no momento. Portanto, não acho apropriado defender o uso de Swift em código pesado de callback como o tipo encontrado no desenvolvimento da Web. Essa foi a razão pela qual mudamos para o Swift em primeiro lugar ...
Zack Morris
4
@ZackMorris Armazene algum tipo de identificador com o fechamento para que possa removê-lo mais tarde. Se estiver usando tipos de referência, você pode apenas armazenar uma referência ao objeto, caso contrário, poderá criar seu próprio sistema de identificadores. Você pode até projetar um tipo que tenha um fechamento e um identificador exclusivo que você pode usar em vez de um fechamento simples.
desenhou em
5
@drewag Sim, existem soluções alternativas, mas Zack está certo. Isso é realmente muito idiota. Eu entendo que quero ter otimizações, mas se houver algum lugar no código que o desenvolvedor precise comparar alguns fechamentos, então simplesmente faça com que o compilador não otimize essas seções específicas. Ou crie algum tipo de função adicional do compilador que o habilite a criar assinaturas de igualdade que não quebrem com otimizações absurdas. É da Apple que estamos falando aqui ... se eles podem encaixar um Xeon em um iMac, então certamente podem fazer tampas comparáveis. Me dá um tempo!
CommaToast
10

Eu procurei muito. Parece não haver nenhuma maneira de comparação de ponteiro de função. A melhor solução que encontrei é encapsular a função ou o encerramento em um objeto hashble. Gostar:

var handler:Handler = Handler(callback: { (message:String) in
            //handler body
}))
tuncay
fonte
2
Esta é, de longe, a melhor abordagem. É chato ter que embrulhar e desembrulhar fechos, mas é melhor do que fragilidade não-determinística e sem suporte.
8

A maneira mais simples é designar o tipo de bloco como @objc_block, e agora você pode lançá-lo em um AnyObject comparável com ===. Exemplo:

    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
mate
fonte
Ei, estou tentando se unsafeBitCast (listener, AnyObject.self) === unsafeBitCast (f, AnyObject.self), mas obtenho um erro fatal: não é possível unsafeBitCast entre tipos de tamanhos diferentes. A ideia é construir um sistema baseado em eventos, mas o método removeEventListener deve ser capaz de verificar os ponteiros de função.
congelando_
2
Use @convention (block) em vez de @objc_block no Swift 2.x. Ótima resposta!
Gabriel.Massana de
6

Também estou procurando a resposta. E eu finalmente encontrei.

O que você precisa é do ponteiro de função real e seu contexto oculto no objeto de função.

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
}

E aqui está a demonstração:

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

Veja os URLs abaixo para descobrir por que e como funciona:

Como você pode ver, ele é capaz de verificar apenas a identidade (o segundo teste produz false). Mas isso deve ser bom o suficiente.

dankogai
fonte
5
Este método não será confiável com otimizações de compilador devforums.apple.com/message/1035180#1035180
drewag
8
Este é um hack baseado em detalhes de implementação indefinidos. Então, usar isso significa que seu programa produzirá um resultado indefinido.
eonil
8
Observe que isso depende de coisas não documentadas e detalhes de implementação não divulgados, que podem travar seu aplicativo no futuro, caso sejam alterados. Não recomendado para uso em código de produção.
Cristik
Isso é "trevo", mas completamente impraticável. Não sei por que isso foi recompensado com uma recompensa. A linguagem intencionalmente não tem igualdade de funções, com o objetivo exato de liberar o compilador para quebrar a igualdade de funções livremente a fim de produzir melhores otimizações.
Alexander - Reintegrar Monica
... e esta é exatamente a abordagem que Chris Lattner defende (veja a primeira resposta).
pipacs
4

Esta é uma ótima pergunta e, embora Chris Lattner intencionalmente não queira oferecer suporte a esse recurso, eu, como muitos desenvolvedores, também não posso deixar de lado meus sentimentos vindos de outras linguagens onde esta é uma tarefa trivial. Existem muitos unsafeBitCastexemplos, a maioria deles não mostra a imagem completa, aqui está um mais detalhado :

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

A parte interessante é como o Swift transmite livremente SwfBlock para ObjBlock, mas na realidade dois blocos SwfBlock fundidos sempre terão valores diferentes, enquanto ObjBlocks não. Quando lançamos ObjBlock para SwfBlock, a mesma coisa acontece com eles, eles se tornam dois valores diferentes. Portanto, para preservar a referência, esse tipo de fundição deve ser evitado.

Ainda estou compreendendo todo esse assunto, mas uma coisa que deixei de desejar é a capacidade de usar @convention(block)métodos de classe / estrutura, então preenchi uma solicitação de recurso que precisa ser votada ou explicada por que é uma má ideia. Também tenho a sensação de que essa abordagem pode ser ruim, se for o caso, alguém pode explicar por quê?

Ian Bytchek
fonte
1
Não acho que você entenda o raciocínio de Chris Latner sobre por que isso não é (e não deveria ser) suportado. "Também tenho a sensação de que essa abordagem pode ser ruim no geral. Se for, alguém pode explicar por quê?" Porque em uma construção otimizada, o compilador é livre para destruir o código de várias maneiras que quebram a ideia de igualdade de pontos de funções. Para um exemplo básico, se o corpo de uma função iniciar da mesma maneira que outra função, o compilador provavelmente irá sobrepor os dois no código de máquina, mantendo apenas pontos de saída diferentes. Isso reduz a duplicação
Alexander - Reintegrar Monica
1
Fundamentalmente, fechamentos são maneiras de iniciar objetos de classes anônimas (assim como em Java, mas é mais óbvio). Esses objetos de encerramento são alocados no heap e armazenam os dados capturados pelo encerramento, que atuam como parâmetros implícitos para a função do encerramento. O objeto de fechamento contém uma referência a uma função que opera sobre os argumentos explícitos (via args func) e implícitos (via contexto de fechamento capturado). Embora o corpo da função possa ser compartilhado como um único ponto único, o ponteiro do objeto de fechamento não pode ser, porque há um objeto de fechamento por conjunto de valores incluídos.
Alexander - Reintegrar Monica de
1
Então, quando você tem Struct S { func f(_: Int) -> Bool }, você na verdade tem uma função de tipo S.fque tem tipo (S) -> (Int) -> Bool. Esta função pode ser compartilhada. Ele é parametrizado exclusivamente por seus parâmetros explícitos. Quando você o usa como um método de instância (ou vinculando implicitamente o selfparâmetro chamando o método em um objeto, por exemplo S().f, ou vinculando-o explicitamente, por exemplo S.f(S())), você cria um novo objeto de encerramento. Este objeto armazena um ponteiro para S.f(que pode ser compartilhado) , but also to your instance (self , the S () `).
Alexander - Reintegrar Monica de
1
Este objeto de fechamento deve ser único por instância de S. Se a igualdade do ponteiro de fechamento fosse possível, você ficaria surpreso ao descobrir que s1.fnão é o mesmo ponteiro que s2.f(porque um é um objeto de fechamento que faz referência a s1e f, e o outro é um objeto de fechamento que faz referência a s2e f).
Alexander - Reintegrar Monica
Isso é brilhante, obrigado! Sim, agora eu tinha uma imagem do que está acontecendo e isso coloca tudo em uma perspectiva! 👍
Ian Bytchek
4

Aqui está uma solução possível (conceitualmente o mesmo que a resposta 'tuncay'). O objetivo é definir uma classe que envolva alguma funcionalidade (por exemplo, Command):

Rápido:

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
fonte
Seria muito melhor se você o tornasse genérico.
Alexander - Reintegrar Monica de
2

Bem, já se passaram 2 dias e ninguém sugeriu uma solução, então vou mudar meu comentário para uma resposta:

Pelo que eu posso dizer, você não pode verificar a igualdade ou identidade de funções (como seu exemplo) e metaclasses (por exemplo, MyClass.self):

Mas - e isso é apenas uma ideia - não posso deixar de notar que a wherecláusula nos genéricos parece ser capaz de verificar a igualdade de tipos. Então, talvez você possa aproveitar isso, pelo menos para verificar a identidade?

Jiaaro
fonte
2

Não é uma solução geral, mas se alguém está tentando implementar um padrão de ouvinte, acabei retornando um "id" da função durante o registro para que possa usá-lo para cancelar o registro mais tarde (que é uma espécie de solução alternativa para a questão original para o caso de "ouvintes", já que geralmente cancelar o registro se resume a verificar a igualdade das funções, o que pelo menos não é "trivial", conforme outras respostas).

Então, algo assim:

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

Agora você só precisa armazenar o keyretornado pela função "registrar" e passá-lo ao cancelar o registro.

vírus
fonte
0

Minha solução foi encapsular funções em classes que estendem NSObject

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

    init(_ function: @escaping (Type) -> Void) {
        value = function
    }
}
Renetik
fonte
Quando você fizer isso, como compará-los? digamos que você deseja remover um deles de uma matriz de seus invólucros, como fazer isso? Obrigado.
Ricardo
0

Sei que estou respondendo a essa pergunta com seis anos de atraso, mas acho que vale a pena examinar a motivação por trás da pergunta. O questionador comentou:

Sem ser capaz de remover fechamentos de uma lista de invocação por referência, no entanto, precisamos criar nossa própria classe de wrapper. Isso é uma chatice e não deveria ser necessário.

Então, acho que o questionador deseja manter uma lista de retorno de chamada, como esta:

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

Mas não podemos escrever removeCallbackassim, porque ==não funciona para funções. (Nem=== .)

Esta é uma maneira diferente de gerenciar sua lista de retorno de chamada. Retorne um objeto de registro de addCallbacke use o objeto de registro para remover o retorno de chamada. Aqui em 2020, podemos usar o Combine AnyCancellablecomo registro.

A API revisada tem a seguinte aparência:

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

Agora, quando você adiciona um retorno de chamada, você não precisa ficar com ele para passar para removeCallbackmais tarde. Não existe removeCallbackmétodo. Em vez disso, você salva o AnyCancellablee chama seu cancelmétodo para remover o retorno de chamada. Melhor ainda, se você armazenar o AnyCancellableem uma propriedade de instância, ele se cancelará automaticamente quando a instância for destruída.

rob mayoff
fonte
O motivo mais comum de precisarmos disso é gerenciar vários assinantes para editores. Combine resolve isso sem tudo isso. O que o C # permite, e o Swift não, é descobrir se dois encerramentos fazem referência à mesma função nomeada. Isso também é útil, mas com muito menos frequência.
Jessy