¿Cómo se prueba la igualdad de funciones y cierres?

88

El libro dice que "las funciones y los cierres son tipos de referencia". Entonces, ¿cómo saber si las referencias son iguales? == y === no funcionan.

func a() { }
let å = a
let b = å === å // Could not find an overload for === that accepts the supplied arguments
Jessy
fuente
5
Por lo que puedo decir, tampoco se puede verificar la igualdad de las metaclases (por ejemplo, MyClass.self)
Jiaaro
No debería ser necesario comparar dos cierres para identificar la identidad. ¿Puede dar un ejemplo de dónde haría esto? Podría haber una solución alternativa.
Bill
1
Cierres de multidifusión, a la C #. Son necesariamente más feos en Swift, porque no se puede sobrecargar el "operador" (T, U), pero aún podemos crearlos nosotros mismos. Sin embargo, sin poder eliminar los cierres de una lista de invocación por referencia, necesitamos crear nuestra propia clase contenedora. Eso es un lastre y no debería ser necesario.
Jessy
2
Gran pregunta, pero algo totalmente diferente: su uso de un diacrítico para åhacer referencia aes realmente interesante. ¿Hay alguna convención que estés explorando aquí? (No sé si realmente me gusta o no; pero parece que podría ser muy poderoso, especialmente en la programación funcional pura.)
Rob Napier
2
@Bill Estoy almacenando cierres en una matriz y no puedo usar indexOf ({$ 0 == cierre} para encontrarlos y eliminarlos. Ahora tengo que reestructurar mi código debido a la optimización que creo que es un diseño de lenguaje deficiente.
Zack Morris

Respuestas:

72

Chris Lattner escribió en los foros de desarrolladores:

Esta es una función que intencionalmente no queremos admitir. Hay una variedad de cosas que harán que la igualdad de funciones de puntero (en el sentido del sistema de tipo rápido, que incluye varios tipos de cierres) falle o cambie según la optimización. Si se definiera "===" en funciones, el compilador no podría fusionar cuerpos de métodos idénticos, compartir procesadores y realizar ciertas optimizaciones de captura en cierres. Además, la igualdad de este tipo sería extremadamente sorprendente en algunos contextos genéricos, donde puede obtener procesadores de restracción que ajustan la firma real de una función a la que espera el tipo de función.

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

Esto significa que ni siquiera debe intentar comparar los cierres por igualdad porque las optimizaciones pueden afectar el resultado.

dibujar
fuente
18
Esto solo me mordió, lo cual fue algo devastador porque había estado almacenando cierres en una matriz y ahora no puedo eliminarlos con indexOf ({$ 0 == cierre}, así que tengo que refactorizar. En mi humilde opinión, la optimización no debería influir en el diseño del lenguaje, así que sin una solución rápida como el ahora obsoleto @objc_block en la respuesta de Matt, diría que Swift no puede almacenar y recuperar correctamente los cierres en este momento. Por lo tanto, no creo que sea apropiado defender el uso de Swift en el código pesado de devolución de llamada como el que se encuentra en el desarrollo web. Esa fue la razón por la que cambiamos a Swift en primer lugar ...
Zack Morris
4
@ZackMorris Almacena algún tipo de identificador con el cierre para que puedas eliminarlo más tarde. Si está utilizando tipos de referencia, simplemente puede almacenar una referencia al objeto, de lo contrario, puede crear su propio sistema de identificación. Incluso podría diseñar un tipo que tenga un cierre y un identificador único que pueda usar en lugar de un cierre simple.
drewag
5
@drewag Sí, hay soluciones, pero Zack tiene razón. Esto es realmente una tontería. Entiendo que quiera tener optimizaciones, pero si hay algún lugar en el código que el desarrollador necesita comparar algunos cierres, entonces simplemente haga que el compilador no optimice esas secciones en particular. O haga algún tipo de función adicional del compilador que le permita crear firmas de igualdad que no rompan con las optimizaciones. Esto es Apple de lo que estamos hablando aquí ... si pueden colocar un Xeon en un iMac, entonces ciertamente pueden hacer que los cierres sean comparables. ¡Dáme un respiro!
CommaToast
10

Busqué mucho. Parece que no hay forma de comparar el puntero de función. La mejor solución que obtuve es encapsular la función o el cierre en un objeto hash. Me gusta:

var handler:Handler = Handler(callback: { (message:String) in
            //handler body
}))
tuncay
fuente
2
Este es, de lejos, el mejor enfoque. Apesta tener que envolver y desenvolver cierres, pero es mejor que la fragilidad no determinista y sin apoyo.
8

La forma más sencilla es designar el tipo de bloque como @objc_block, y ahora puede convertirlo en un AnyObject que sea comparable con ===. Ejemplo:

    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
fuente
Oye, estoy intentando si unsafeBitCast (oyente, AnyObject.self) === unsafeBitCast (f, AnyObject.self) pero obtengo un error fatal: no puedo unsafeBitCast entre tipos de diferentes tamaños. La idea es construir un sistema basado en eventos, pero el método removeEventListener debería poder verificar los punteros de función.
congelación_
2
Utilice @convention (bloque) en lugar de @objc_block en Swift 2.x. ¡Gran respuesta!
Gabriel.Massana
6

Yo también he estado buscando la respuesta. Y por fin lo encontré.

Lo que necesita es el puntero de función real y su contexto oculto en el objeto de función.

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
}

Y aquí está la demostración:

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

Consulte las URL a continuación para averiguar por qué y cómo funciona:

Como puede ver, solo es capaz de verificar la identidad (la segunda prueba da como resultado false). Pero eso debería ser suficiente.

dankogai
fuente
5
Este método no será confiable con las optimizaciones del compilador devforums.apple.com/message/1035180#1035180
drewag
8
Este es un truco basado en detalles de implementación no definidos. Luego, usar esto significa que su programa producirá un resultado indefinido.
eonil
8
Tenga en cuenta que esto depende de cosas indocumentadas y detalles de implementación no revelados, que pueden bloquear su aplicación en el futuro si cambian. No se recomienda su uso en código de producción.
Cristik
Esto es "trébol", pero completamente inviable. No sé por qué esto fue recompensado con una recompensa. El lenguaje intencionalmente no tiene igualdad de funciones, con el propósito exacto de liberar al compilador para que rompa la igualdad de funciones libremente para producir mejores optimizaciones.
Alexander - Reincorpora a Monica
... y este es exactamente el enfoque que defiende Chris Lattner (consulte la respuesta principal).
pipacs
4

Esta es una gran pregunta y aunque Chris Lattner intencionalmente no quiere admitir esta característica, yo, como muchos desarrolladores, tampoco puedo dejar de lado mis sentimientos provenientes de otros lenguajes donde esta es una tarea trivial. Hay muchos unsafeBitCastejemplos, la mayoría de ellos no muestran la imagen completa, aquí hay uno más detallado :

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 interesante es cómo Swift lanza libremente SwfBlock a ObjBlock, pero en realidad dos bloques SwfBlock emitidos siempre tendrán valores diferentes, mientras que ObjBlocks no. Cuando lanzamos ObjBlock a SwfBlock, les sucede lo mismo, se convierten en dos valores diferentes. Entonces, para preservar la referencia, este tipo de casting debe evitarse.

Todavía estoy comprendiendo todo este tema, pero una cosa que dejé deseando es la capacidad de usar @convention(block)en métodos de clase / estructura, así que presenté una solicitud de función que necesita una votación positiva o explicar por qué es una mala idea. También tengo la sensación de que este enfoque podría ser malo en conjunto, si es así, ¿alguien puede explicar por qué?

Ian Bytchek
fuente
1
No creo que entiendas el razonamiento de Chris Latner sobre por qué esto no es (y no debería) apoyarse. "También tengo la sensación de que este enfoque podría ser malo en conjunto, si es así, ¿alguien puede explicar por qué?" Porque en una compilación optimizada, el compilador es libre de alterar el código de muchas formas que rompen la idea de igualdad de puntos de funciones. Para un ejemplo básico, si el cuerpo de una función comienza de la misma manera que lo hace otra función, es probable que el compilador se superponga a los dos en el código de máquina, solo manteniendo diferentes puntos de salida. Esto reduce la duplicación
Alexander - Reincorpora a Monica
1
Básicamente, los cierres son formas de iniciar objetos de clases anónimas (como en Java, pero es más obvio). Estos objetos de cierres se asignan en montón y almacenan los datos capturados por el cierre, que actúan como parámetros implícitos de la función del cierre. El objeto de cierre contiene una referencia a una función que opera sobre los argumentos explícitos (a través de func args) e implícitos (a través del contexto de cierre capturado). Si bien el cuerpo de la función se puede compartir como un único punto único, el puntero del objeto de cierre no puede serlo, porque hay un objeto de cierre por conjunto de valores incluidos.
Alexander - Reincorpora a Monica
1
Entonces, cuando lo tiene Struct S { func f(_: Int) -> Bool }, en realidad tiene una función de tipo S.fque tiene tipo (S) -> (Int) -> Bool. Esta función se puede compartir. Está parametrizado únicamente por sus parámetros explícitos. Cuando lo usa como un método de instancia (ya sea vinculando implícitamente el selfparámetro llamando al método en un objeto, por ejemplo S().f, o vinculándolo explícitamente, por ejemplo S.f(S())), crea un nuevo objeto de cierre. Este objeto almacena un puntero S.f(que se puede compartir) , but also to your instance (self , the S () `).
Alexander - Reincorpora a Monica
1
Este objeto de cierre debe ser único por instancia de S. Si la igualdad de puntero de cierre fuera posible, entonces se sorprendería al descubrir que s1.fno es el mismo puntero que s2.f(porque uno es un objeto de cierre que hace referencia a s1y f, y el otro es un objeto de cierre que hace referencia a s2y f).
Alexander - Reincorpora a Monica
¡Esto es genial, gracias! Sí, a estas alturas ya tenía una idea de lo que estaba pasando y esto pone todo en perspectiva. 👍
Ian Bytchek
4

Aquí hay una posible solución (conceptualmente la misma que la respuesta 'tuncay'). El punto es definir una clase que envuelva alguna funcionalidad (por ejemplo, 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
fuente
Esto sería mucho mejor si lo hiciera genérico.
Alexander - Reincorpora a Monica
2

Bueno, han pasado 2 días y nadie ha intervenido con una solución, así que cambiaré mi comentario por una respuesta:

Por lo que puedo decir, no puede verificar la igualdad o identidad de funciones (como su ejemplo) y metaclases (por ejemplo, MyClass.self):

Pero, y esto es solo una idea, no puedo evitar notar que la wherecláusula en los genéricos parece poder verificar la igualdad de tipos. Entonces, ¿tal vez pueda aprovechar eso, al menos para verificar la identidad?

Jiaaro
fuente
2

No es una solución general, pero si uno está tratando de implementar un patrón de escucha, terminé devolviendo un "id" de la función durante el registro para poder usarlo para cancelar el registro más tarde (que es una especie de solución a la pregunta original para el caso de los "oyentes", ya que generalmente la cancelación del registro se reduce a verificar la igualdad de las funciones, que al menos no es "trivial" según otras respuestas).

Entonces algo como esto:

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

Ahora solo necesita almacenar lo keydevuelto por la función "registrar" y pasarlo al cancelar el registro.

virus
fuente
0

Mi solución fue ajustar funciones a la clase que extiende NSObject

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

    init(_ function: @escaping (Type) -> Void) {
        value = function
    }
}
Renetik
fuente
Cuando haces eso, ¿cómo los comparas? digamos que desea eliminar uno de ellos de una matriz de sus envoltorios, ¿cómo lo hace? Gracias.
Ricardo
0

Sé que estoy respondiendo esta pregunta con seis años de retraso, pero creo que vale la pena analizar la motivación detrás de la pregunta. El interrogador comentó:

Sin embargo, sin poder eliminar los cierres de una lista de invocación por referencia, necesitamos crear nuestra propia clase contenedora. Eso es un lastre y no debería ser necesario.

Así que supongo que el interrogador quiere mantener una lista de devolución de llamada, 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 })
    }
}

Pero no podemos escribir de removeCallbackesa manera, porque ==no funciona para funciones. (Tampoco lo hace=== ).

Esta es una forma diferente de administrar su lista de devolución de llamada. Devuelve un objeto de registro de addCallbacky usa el objeto de registro para eliminar la devolución de llamada. Aquí en 2020, podemos usar el CombineAnyCancellable como registro.

La API revisada se ve así:

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

Ahora, cuando agrega una devolución de llamada, no es necesario que la guarde para pasarla removeCallbackmás tarde. No existe ningún removeCallbackmétodo. En su lugar, guarda AnyCancellabley llama a su cancelmétodo para eliminar la devolución de llamada. Aún mejor, si almacena la AnyCancellablepropiedad en una instancia, entonces se cancelará automáticamente cuando se destruya la instancia.

Rob Mayoff
fuente
La razón más común por la que necesitamos esto es para administrar varios suscriptores para los editores. Combinar resuelve eso sin todo esto. Lo que C # permite, y Swift no, es averiguar si dos cierres hacen referencia a la misma función nombrada. Eso también es útil, pero con mucha menos frecuencia.
Jessy