Как вы проверяете функции и замыкания на равенство?

88

В книге говорится, что «функции и замыкания являются ссылочными типами». Итак, как узнать, равны ли ссылки? == и === не работают.

func a() { }
let å = a
let b = å === å // Could not find an overload for === that accepts the supplied arguments
Джесси
источник
5
Насколько я могу судить, вы также не можете проверить равенство метаклассов (например, MyClass.self)
Jiaaro 09
Нет необходимости сравнивать два замыкания для идентификации. Вы можете привести пример, где бы вы это сделали? Может быть альтернативное решение.
Билл
1
Замыкания многоадресной рассылки, а-ля C #. Они обязательно более уродливы в Swift, потому что вы не можете перегрузить «оператор» (T, U), но мы все равно можем создать их сами. Однако, не имея возможности удалять замыкания из списка вызовов по ссылке, нам необходимо создать наш собственный класс-оболочку. Это затруднение, и в этом нет необходимости.
Jessy
2
Отличный вопрос, но совершенно отдельная вещь: ваше использование диакритики åдля ссылки aдействительно интересно. Есть ли здесь конвенция, которую вы изучаете? (Не знаю, нравится мне это или нет; но похоже, что это могло бы быть очень мощным, особенно в чисто функциональном программировании.)
Роб Напье
2
@Bill Я храню замыкания в массиве и не могу использовать indexOf ({$ 0 == closure}, чтобы найти и удалить их. Теперь мне нужно реструктурировать свой код из-за оптимизации, которую я считаю плохой языковой конструкцией.
Zack Morris

Ответы:

72

Крис Латтнер написал на форумах разработчиков:

Это функция, которую мы намеренно не хотим поддерживать. Есть множество вещей, которые могут привести к сбою или изменению равенства указателей функций (в смысле системы быстрого типа, который включает несколько видов замыканий) в зависимости от оптимизации. Если бы для функций было определено «===», компилятору не разрешалось бы объединять идентичные тела методов, совместно использовать преобразователи и выполнять определенные оптимизации захвата в замыканиях. Кроме того, равенство такого рода было бы чрезвычайно неожиданным в некоторых контекстах обобщения, где вы можете получить преобразователи реабстракции, которые корректируют фактическую сигнатуру функции в соответствии с ожидаемой типом функции.

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

Это означает, что вам не следует даже пытаться сравнивать замыкания на предмет равенства, поскольку оптимизация может повлиять на результат.

Дрюаг
источник
18
Это меня просто укусило, что было ужасно, потому что я хранил замыкания в массиве и теперь не могу удалить их с помощью indexOf ({$ 0 == closure}, поэтому мне нужно провести рефакторинг. Оптимизация IMHO не должна влиять на дизайн языка, поэтому без быстрого исправления, такого как теперь устаревший @objc_block в ответе Мэтта, я бы сказал, что Swift не может правильно хранить и извлекать замыкания в настоящее время.Поэтому я не думаю, что уместно защищать использование Swift в тяжелом коде обратного вызова вроде тех, что встречаются в веб-разработке. Именно поэтому мы в первую очередь перешли на Swift ...
Зак Моррис,
4
@ZackMorris Сохраните какой-то идентификатор с закрытием, чтобы вы могли удалить его позже. Если вы используете ссылочные типы, вы можете просто сохранить ссылку на объект, иначе вы можете придумать свою собственную систему идентификаторов. Вы даже можете создать тип с закрытием и уникальным идентификатором, который можно использовать вместо простого закрытия.
drewag
5
@drewag Да, есть обходные пути, но Зак прав. Это действительно отстой. Я понимаю, что хочу провести оптимизацию, но если в коде есть место, где разработчику нужно сравнить некоторые замыкания, то просто пусть компилятор не оптимизирует эти конкретные разделы. Или сделайте какую-нибудь дополнительную функцию компилятора, которая позволит ему создавать сигнатуры равенства, которые не ломаются из-за чертовых оптимизаций. Это Apple, о которой мы говорим здесь ... если они могут установить Xeon в iMac, они, безусловно, могут сделать закрытие сопоставимым. Дай мне передохнуть!
CommaToast
10

Я много искал. Кажется, нет способа сравнения указателей на функции. Лучшее решение, которое я получил, - это инкапсулировать функцию или закрытие в хешируемый объект. Подобно:

var handler:Handler = Handler(callback: { (message:String) in
            //handler body
}))
Tuncay
источник
2
Это, безусловно, лучший подход. Отстойно заворачивать и распаковывать крышки, но это лучше, чем недетерминированная, неподдерживаемая хрупкость.
8

@objc_blockСамый простой способ - обозначить тип блока как , и теперь вы можете преобразовать его в AnyObject, сравнимый с ===. Пример:

    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
матовый
источник
Эй, я пытаюсь, если unsafeBitCast (listener, AnyObject.self) === unsafeBitCast (f, AnyObject.self), но получаю фатальную ошибку: не может unsafeBitCast между типами разных размеров. Идея состоит в том, чтобы создать систему, основанную на событиях, но метод removeEventListener должен иметь возможность проверять указатели функций.
freezing_
2
Используйте @convention (block) вместо @objc_block в Swift 2.x. Отличный ответ!
Gabriel.Massana
6

Я тоже искал ответ. И наконец я его нашел.

Что вам нужно, так это фактический указатель на функцию и его контекст, скрытый в объекте функции.

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
}

А вот и демонстрация:

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

См. Приведенные ниже URL-адреса, чтобы узнать, почему и как это работает:

Как видите, он способен проверять только личность (второй тест дает результат false). Но этого должно быть достаточно.

данкогай
источник
5
Этот метод не будет надежным при оптимизации компилятора devforums.apple.com/message/1035180#1035180
drewag
8
Это взлом, основанный на неопределенных деталях реализации. Тогда использование этого означает, что ваша программа выдаст неопределенный результат.
eonil 09
8
Обратите внимание, что это зависит от недокументированного материала и нераскрытых деталей реализации, которые могут привести к сбою вашего приложения в будущем, если они изменятся. Не рекомендуется использовать в производственном коде.
Cristik
Это «клевер», но совершенно неработоспособный. Я не знаю, почему это было награждено наградой. В языке намеренно не предусмотрено равенство функций, чтобы дать компилятору возможность свободно нарушать равенство функций , чтобы обеспечить лучшую оптимизацию.
Александр - Восстановить Монику
... и это именно тот подход, против которого выступает Крис Латтнер (см. верхний ответ).
pipacs 06
4

Это отличный вопрос, и хотя Крис Латтнер намеренно не хочет поддерживать эту функцию, я, как и многие разработчики, также не могу отпустить свои чувства, исходящие от других языков, где это тривиальная задача. unsafeBitCastПримеров много , большинство из них не показывают полной картины, вот более подробный :

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

Интересно то, как swift свободно переводит SwfBlock в ObjBlock, но на самом деле два приведенных блока SwfBlock всегда будут иметь разные значения, а ObjBlocks - нет. Когда мы приводим ObjBlock к SwfBlock, с ними происходит то же самое, они становятся двумя разными значениями. Таким образом, чтобы сохранить ссылку, следует избегать такого преобразования.

Я все еще разбираюсь в этой теме, но я оставил одну вещь, которую я хотел, - это возможность использовать @convention(block)методы класса / структуры, поэтому я отправил запрос функции, который требует голосования или объяснения, почему это плохая идея. Я также чувствую, что этот подход может быть плохим в целом, если да, может ли кто-нибудь объяснить, почему?

Ян Бытчек
источник
1
Я не думаю, что вы понимаете рассуждения Криса Латнера относительно того, почему это не (и не должно поддерживаться). «Я также чувствую, что этот подход может быть плохим в целом, если да, то может ли кто-нибудь объяснить, почему?» Потому что в оптимизированной сборке компилятор может изменять код разными способами, которые нарушают идею точечного равенства функций. В качестве базового примера, если тело одной функции запускается так же, как и другая функция, компилятор, скорее всего, перекрывает их в машинном коде, сохраняя только разные точки выхода. Это уменьшает дублирование
Александр - Восстановите Монику
1
По сути, замыкания - это способы инициирования объектов анонимных классов (прямо как в Java, но там это более очевидно). Эти закрывающие объекты выделяются в куче и хранят данные, захваченные замыканием, которые действуют как неявные параметры функции замыкания. Объект закрытия содержит ссылку на функцию, которая работает с явными (через аргументы функции) и неявными (через захваченный контекст закрытия) аргументами. Хотя тело функции может использоваться как единственная уникальная точка, указатель на закрывающий объект быть не может, потому что на каждый набор вложенных значений приходится один закрывающий объект.
Александр - Восстановить Монику
1
Итак, когда у вас есть Struct S { func f(_: Int) -> Bool }, у вас действительно есть функция типа, у S.fкоторой есть тип (S) -> (Int) -> Bool. Этой функцией можно поделиться. Он параметризуется исключительно своими явными параметрами. Когда вы используете его как метод экземпляра (либо путем неявной привязки selfпараметра, вызывая метод к объекту, например S().f, либо путем явной привязки, например S.f(S())), вы создаете новый объект закрытия. Этот объект хранит указатель на self S () ` S.f(который может использоваться совместно ). , but also to your instance (, the
Александр - Восстановить Монику
1
Этот закрывающий объект должен быть уникальным для каждого экземпляра S. Если бы было возможно равенство указателя закрытия, вы были бы удивлены, обнаружив, что s1.fэто не тот же указатель, что и s2.f(потому что один является объектом закрытия, который ссылается на s1и f, а другой является объектом закрытия, который ссылается на s2и f).
Александр - Восстановить Монику
Это великолепно, спасибо! Да, к настоящему времени у меня была картина того, что происходит, и теперь все можно увидеть в перспективе! 👍
Ian Bytchek 03
4

Вот одно из возможных решений (концептуально то же, что и ответ tuncay). Дело в том, чтобы определить класс, который объединяет некоторые функции (например, 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

Ява:

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
источник
Было бы намного лучше, если бы вы сделали его универсальным.
Александр - Восстановить Монику
2

Что ж, прошло 2 дня, и никто не предложил решение, поэтому я заменю свой комментарий на ответ:

Насколько я могу судить, вы не можете проверить равенство или идентичность функций (например, в вашем примере) и метаклассов (например, MyClass.self):

Но - и это всего лишь идея - я не могу не заметить, что whereпредложение в дженериках, похоже, может проверять равенство типов. Так, может быть, вы сможете использовать это, по крайней мере, для проверки личности?

Jiaaro
источник
2

Не общее решение, но если кто-то пытается реализовать шаблон слушателя, я закончил тем, что вернул «id» функции во время регистрации, поэтому я могу использовать его для отмены регистрации позже (что является своего рода обходным путем к исходному вопросу для случая "слушателей", поскольку обычно отмена регистрации сводится к проверке функций на равенство, что, по крайней мере, не является "тривиальным" согласно другим ответам).

Так что примерно так:

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

Теперь вам просто нужно сохранить keyвозвращаемый функцией "регистр" и передать его при отмене регистрации.

вирус
источник
0

Мое решение заключалось в переносе функций в класс, расширяющий NSObject.

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

    init(_ function: @escaping (Type) -> Void) {
        value = function
    }
}
Ренетик
источник
Когда вы это сделаете, как их сравнить? скажем, вы хотите удалить один из них из массива ваших оберток, как вы это делаете? Спасибо.
Рикардо
0

Я знаю, что отвечаю на этот вопрос с опозданием на шесть лет, но я думаю, что стоит посмотреть на мотивацию, стоящую за вопросом. Спрашивающий прокомментировал:

Однако, не имея возможности удалять замыкания из списка вызовов по ссылке, нам необходимо создать наш собственный класс-оболочку. Это затруднение, и в этом нет необходимости.

Итак, я предполагаю, что спрашивающий хочет вести список обратных вызовов, например:

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

Но мы не можем так писать removeCallback, потому что== что не работает для функций. (Также=== .)

Вот другой способ управлять списком обратных вызовов. Верните объект регистрации из addCallbackи используйте объект регистрации для удаления обратного вызова. Здесь, в 2020 году, мы можем использоватьAnyCancellable качестве регистрации.

Обновленный API выглядит так:

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

Теперь, когда вы добавляете обратный вызов, вам не нужно держать его, чтобы перейти к нему removeCallbackпозже. Нет никакого removeCallbackметода. Вместо этого вы сохраняете AnyCancellableи вызываете его cancelметод для удаления обратного вызова. Еще лучше, если вы сохраните AnyCancellableв свойстве экземпляра, тогда оно автоматически отменится, когда экземпляр будет уничтожен.

Роб Мэйофф
источник
Наиболее частая причина, по которой нам это нужно, - это управление несколькими подписчиками для издателей. Combine решает эту проблему без всего этого. Что C # позволяет, а Swift нет, так это выяснить, ссылаются ли два замыкания на одну и ту же именованную функцию. Это тоже полезно, но гораздо реже.
Джесси,