Як ви перевіряєте функції та закриття на рівність?


88

У книзі сказано, що "функції та закриття є еталонними типами". Отже, як дізнатись, чи однакові посилання? == і === не працюють.

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

5
Наскільки я можу зрозуміти, ви також не можете перевірити рівність метакласів (наприклад, MyClass.self)
Джааро,

Не слід порівнювати два закриття для ідентичності. Чи можете ви навести приклад, де б ви це робили? Може бути альтернативне рішення.
Білл

1
Багатоадресне закриття, а-ля C #. Вони обов’язково потворніші у Swift, тому що ви не можете перевантажити (T, U) "оператор", але ми все одно можемо створити їх самі. Однак, не маючи можливості видалити закриття зі списку викликів за допомогою посилання, нам потрібно створити власний клас обгортки. Це тягне, і не повинно бути необхідним.
Джессі

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

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 (слухач, AnyObject.self) === unsafeBitCast (f, AnyObject.self), але отримую фатальну помилку: не вдається unsafeBitCast між типами різних розмірів. Ідея полягає в тому, щоб створити систему на основі подій, але метод removeEventListener повинен мати можливість перевіряти покажчики на функції.
заморожування_

2
Використовуйте @convention (block) замість @objc_block на Swift 2.x. Чудова відповідь!
Габріель, Массана,

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-адреси нижче, щоб дізнатися, чому і як це працює:

Як бачите, він здатний перевіряти лише особу (2-й тест дає false). Але це має бути досить добре.


5
Цей метод не буде надійним при оптимізації компілятора devforums.apple.com/message/1035180#1035180
drewag

8
Це хак на основі невизначених деталей реалізації. Тоді за допомогою цього засобу ваша програма дасть невизначений результат.
eonil

8
Зверніть увагу, що це спирається на недокументовані матеріали та нерозкриті деталі реалізації, які можуть призвести до аварійного завершення роботи вашого додатка в майбутньому, коли вони зміняться. Не рекомендується використовувати у виробничому коді.
Крістік

Це «конюшина», але абсолютно непрацездатна. Не знаю, чому це було нагороджено щедрістю. Мова навмисно не має рівності функцій, саме для того, щоб звільнити компілятор вільно порушувати рівність функцій для кращої оптимізації.
Олександр -

... і саме проти цього виступає Кріс Латтнер (див. верхню відповідь).
pipacs

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())), ви створюєте новий об'єкт закриття. Цей об'єкт зберігає вказівник на S.f(яким можна спільно користуватися) , but also to your instance (self , the S () `).
Олександр -

1
Цей об'єкт закриття повинен бути унікальним для кожного екземпляра S. Якщо покажчик закриття рівність було можливо, то ви будете здивовані, виявивши , що s1.fце не те ж саме , як покажчик s2.f(бо один є об'єктом закриття , які посилання s1і f, а інший об'єкт закриття , які посилання s2і f).
Олександр -

Це чудово, дякую! Так, я вже мав уявлення про те, що відбувається, і це ставить все на перспективу! 👍
Ян Битчек

4

Ось одне з можливих рішень (концептуально те саме, що і відповідь "tuncay"). Суть полягає у визначенні класу, який обгортає деяку функціональність (наприклад, Command):

Стрімкий:

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

Це було б набагато краще, якби ви зробили його загальним.
Олександр -

2

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

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

Але - і це лише ідея - я не можу не помітити, що whereпункт у загальних виробах, здається, може перевірити рівність типів. То, можливо, ви можете використати це, принаймні для перевірки особи?


2

Не загальне рішення, але якщо хтось намагається реалізувати шаблон слухача, я в підсумку повертаю "ідентифікатор" функції під час реєстрації, щоб я міг використати його для скасування реєстрації пізніше (це своєрідний спосіб вирішення вихідного питання для випадку "слухачів", як правило, незареєстрація зводиться до перевірки функцій на рівність, що, принаймні, не є "тривіальним", як за іншими відповідями).

Отож приблизно так:

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повернену функцією "register" і передати її під час скасування реєстрації.


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властивість екземпляра, тоді воно автоматично скасується, коли екземпляр буде знищений.


Найпоширенішою причиною, яка нам потрібна, є управління кількома передплатниками для видавців. Комбінат вирішує це без усього цього. Що дозволяє C #, а Swift - ні, це з'ясувати, чи два закриття посилаються на одну і ту ж названу функцію. Це теж корисно, але набагато рідше.
Джессі
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.