Eşitlik için işlevleri ve kapanışları nasıl test ediyorsunuz?


88

Kitap, "işlevler ve kapanışların referans türler olduğunu" söylüyor. Peki, referansların eşit olup olmadığını nasıl anlarsınız? == ve === çalışmıyor.

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

5
Bildiğim kadarıyla söyleyebilirim, ayrıca metaclasses eşitliğini (örneğin kontrol edemez MyClass.self)
Jiaaro

Kimlik için iki kapanışı karşılaştırmak gerekli olmamalıdır. Bunu nerede yapacağınıza dair bir örnek verebilir misiniz? Alternatif bir çözüm olabilir.
Bill

1
Çok noktaya yayın kapanmaları, bir la C #. Swift'de mutlaka daha çirkinler, çünkü (T, U) "operatörünü" aşırı yükleyemezsiniz, ancak yine de onları kendimiz yaratabiliriz. Bir çağrı listesinden kapanışları referans olarak çıkaramadan kendi sarmalayıcı sınıfımızı oluşturmamız gerekir. Bu bir engeldir ve gerekli olmamalıdır.
Jessy

2
Harika bir soru, ama tamamen ayrı bir şey: åreferans aiçin bir aksan kullanman gerçekten ilginç. Burada keşfettiğiniz bir kongre var mı? (Gerçekten beğenip beğenmediğimi bilmiyorum; ama özellikle saf fonksiyonel programlamada çok güçlü olabilir gibi görünüyor.)
Rob Napier

2
@Bill Kapakları bir Dizide depoluyorum ve bunları bulmak ve kaldırmak için indexOf ({$ 0 == closure}) kullanamıyorum. Şimdi, kötü bir dil tasarımı olduğuna inandığım optimizasyon nedeniyle kodumu yeniden yapılandırmam gerekiyor.
Zack Morris

Yanıtlar:


72

Chris Lattner, geliştirici forumlarına şunları yazdı:

Bu, kasıtlı olarak desteklemek istemediğimiz bir özelliktir. Optimizasyona bağlı olarak işaretçi eşitliği işlevlerinin (birkaç tür kapanış içeren hızlı tip sistem anlamında) başarısız olmasına veya değişmesine neden olacak çeşitli şeyler vardır. İşlevlerde "===" tanımlandıysa, derleyicinin aynı yöntem gövdelerini birleştirmesine, thunk'ları paylaşmasına ve kapanışlarda belirli yakalama optimizasyonları gerçekleştirmesine izin verilmezdi. Dahası, bu türden bir eşitlik, bir fonksiyonun gerçek imzasını fonksiyon tipinin beklediğine ayarlayan yeniden soyutlama thunk'ları alabileceğiniz bazı jenerik bağlamlarda son derece şaşırtıcı olurdu.

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

Bu, eşitlik açısından kapanışları karşılaştırmaya bile çalışmamanız gerektiği anlamına gelir çünkü optimizasyonlar sonucu etkileyebilir.


19
Bu beni biraz ısırdı, bu biraz yıkıcıydı çünkü kapanışları bir Array'de depoluyordum ve şimdi bunları indexOf ({$ 0 == closure} ile kaldıramıyorum, bu yüzden yeniden düzenlemem gerekiyor. IMHO optimizasyonu dil tasarımını etkilememeli, bu yüzden, Matt'in cevabında artık kullanımdan kaldırılan @objc_block gibi hızlı bir düzeltme olmadan, Swift'in şu anda kapanışları düzgün bir şekilde saklayamayacağını ve geri alamayacağını iddia ediyorum. Bu yüzden, geri arama ağır kodunda Swift kullanımını savunmanın uygun olduğunu düşünmüyorum web geliştirmede karşılaşılan türden. İlk etapta Swift'e geçmemizin tek nedeni buydu ...
Zack Morris

4
@ZackMorris Kapatma ile bir tür tanımlayıcı saklayın, böylece daha sonra kaldırabilirsiniz. Referans tiplerini kullanıyorsanız, sadece nesneye bir referans kaydedebilirsiniz, aksi takdirde kendi tanımlayıcı sisteminizi oluşturabilirsiniz. Düz bir kapak yerine kullanabileceğiniz bir kapama ve benzersiz bir tanımlayıcıya sahip bir tür bile tasarlayabilirsiniz.
2016

5
@drewag Evet, geçici çözümler var ama Zack haklı. Bu gerçekten çok kötü. Optimizasyonlara sahip olmayı istemeyi anlıyorum, ancak kodda geliştiricinin bazı kapanışları karşılaştırması gereken bir yer varsa, derleyicinin bu belirli bölümleri optimize etmemesini sağlayın. Veya derleyicinin acayip optimizasyonlardan kopmayan eşitlik imzaları oluşturmasını sağlayan bir tür ek işlev yapın. Burada bahsettiğimiz Apple ... Bir Xeon'u bir iMac'e sığdırabiliyorlarsa, kapanışları kesinlikle karşılaştırılabilir hale getirebilirler. Beni rahat bırak!
CommaToast

10

Çok aradım. İşlev gösterici karşılaştırmasının bir yolu yok gibi görünüyor. Elimdeki en iyi çözüm, işlevi veya kapanışı hashable bir nesnede kapsüllemek. Sevmek:

var handler:Handler = Handler(callback: { (message:String) in
            //handler body
}))

2
Bu, açık ara en iyi yaklaşımdır. Kapakları sarmak ve açmak zorunda kalmak berbattır, ancak kesin olmayan, desteklenmeyen kırılganlıktan daha iyidir.

8

En basit yol, blok tipini olarak belirlemektir @objc_blockve şimdi onu. İle karşılaştırılabilir bir AnyObject'e çevirebilirsiniz ===. Misal:

    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

Hey, eğer unsafeBitCast (dinleyici, AnyObject.self) === unsafeBitCast (f, AnyObject.self) ise deniyorum ama ölümcül bir hata alıyorum: farklı boyut türleri arasında güvenli olmayanBitCast olamaz. Buradaki fikir, olaya dayalı bir sistem oluşturmaktır, ancak removeEventListener yöntemi işlev işaretçilerini kontrol edebilmelidir.
freezing_

2
Swift 2.x'te @objc_block yerine @convention (blok) kullanın. Mükemmel cevap!
Gabriel.Massana

6

Ben de cevabı arıyordum. Ve sonunda buldum.

İhtiyacınız olan şey, gerçek işlev işaretçisi ve işlev nesnesinde gizli olan bağlamıdır.

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
}

Ve işte demo:

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

Neden ve nasıl çalıştığını öğrenmek için aşağıdaki URL'lere bakın:

Gördüğünüz gibi yalnızca kimliği kontrol edebiliyor (2. test sonuçları false). Ama bu yeterince iyi olmalı.


5
Bu yöntem, derleyici optimizasyonları devforums.apple.com/message/1035180#1035180
drewag

8
Bu, tanımlanmamış uygulama ayrıntılarına dayalı bir saldırıdır. O zaman bunu kullanmak, programınızın tanımsız bir sonuç üreteceği anlamına gelir.
eonil

8
Bunun, belgelenmemiş şeylere ve açıklanmayan uygulama ayrıntılarına dayandığını ve bunların gelecekte uygulamanızı kilitleyebileceğini unutmayın. Üretim kodunda kullanılması tavsiye edilmez.
Cristik

Bu "yonca" ama tamamen işe yaramaz. Bunun neden bir ödül olarak ödüllendirildiğini bilmiyorum. Derleyicinin daha iyi optimizasyonlar elde etmek için fonksiyon eşitliğini özgürce kırmasını sağlamak amacıyla, dilin kasıtlı olarak işlev eşitliği yoktur .
Alexander

... ve bu tam olarak Chris Lattner'ın savunduğu yaklaşımdır (üst cevaba bakınız).
pipacs

4

Bu harika bir soru ve Chris Lattner kasıtlı olarak bu özelliği desteklemek istemese de, ben de birçok geliştirici gibi, bunun önemsiz bir görev olduğu diğer dillerden gelen hislerimi bırakamıyorum. Pek çok unsafeBitCastörnek var, çoğu resmin tamamını göstermiyor, işte daha ayrıntılı bir örnek :

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

İlginç olan, SwfBlock'u ObjBlock'a ne kadar hızlı bir şekilde attığıdır, ancak gerçekte iki SwfBlock bloğu her zaman farklı değerler olurken, ObjBlocks olmayacaktır. ObjBlock'u SwfBlock'a attığımızda, onlara da aynı şey oluyor, iki farklı değer haline geliyorlar. Bu nedenle referansı korumak için bu tür dökümlerden kaçınılmalıdır.

Hala tüm konuyu anlıyorum, ancak dilediğim bir şey @convention(block)sınıf / yapı yöntemlerinde kullanma yeteneği , bu yüzden oylama gerektiren veya bunun neden kötü bir fikir olduğunu açıklayan bir özellik talebinde bulundum . Ayrıca bu yaklaşımın hep birlikte kötü olabileceğine dair bir his var, eğer öyleyse, kimse nedenini açıklayabilir mi?


1
Chris Latner'ın bunun neden desteklenmediği (ve desteklenmemesi gerektiği) konusundaki mantığını anladığınızı sanmıyorum. "Ayrıca bu yaklaşımın hep birlikte kötü olabileceğine dair bir fikrim var, eğer öyleyse, kimse nedenini açıklayabilir mi?" Çünkü optimize edilmiş bir yapıda, derleyici, fonksiyonların nokta eşitliği fikrini bozan birçok şekilde kodu karıştırmakta özgürdür. Temel bir örnek için, bir işlevin gövdesi başka bir işlevle aynı şekilde başlarsa, derleyici muhtemelen makine kodundaki ikisini üst üste bindirir, yalnızca farklı çıkış noktalarını korur. Bu, çoğaltmayı azaltır
Alexander

1
Temelde, kapatmalar anonim sınıfların nesnelerini başlatmanın yollarıdır (tıpkı Java'da olduğu gibi, ancak daha barizdir). Bu kapanış nesneleri yığın tahsis edilir ve kapanış tarafından yakalanan verileri depolar, bu da kapanışın işlevine örtük bir parametre gibi davranır. Kapanış nesnesi, açık (func argümanları aracılığıyla) ve örtük (yakalanan kapanış bağlamı yoluyla) argümanlar üzerinde çalışan bir fonksiyona bir referans tutar. İşlev gövdesi tek bir benzersiz nokta olarak paylaşılabilirken, kapanış nesnesinin işaretçisi olamaz , çünkü her kapalı değer kümesi için bir kapatma nesnesi vardır.
Alexander

1
Yani, sahip olduğunuzda Struct S { func f(_: Int) -> Bool }, aslında türü S.folan bir tür işleviniz olur (S) -> (Int) -> Bool. Bu fonksiyon olabilir paylaşılabilir. Yalnızca açık parametreleri ile parametreleştirilir. Bunu bir örnek yöntem olarak kullandığınızda ( selfyöntemi bir nesnede çağırarak parametreyi örtük olarak bağlayarak S().fveya örneğin onu açıkça bağlayarak S.f(S())), yeni bir kapanış nesnesi oluşturursunuz. Bu nesne S.f(paylaşılabilir) , but also to your instance (self , the S () `) ' ye bir işaretçi saklar .
Alexander

1
Bu kapatma nesnesi, örneği başına benzersiz olmalıdır S. Kapatma işaretçi eşitlik mümkün olsaydı, o zaman o keşfetmek için sürpriz olmayacağını s1.faynı işaretçi değildir s2.f(biri referanslar bir kapatma nesnesi olduğundan s1ve fve diğer hangi referanslar bir kapatma nesnesidir s2ve f).
Alexander

Bu harika, teşekkürler! Evet, şimdiye kadar neler olup bittiğine dair bir fotoğrafım vardı ve bu her şeyi bir perspektife oturtuyor! 👍
Ian Bytchek

4

İşte olası bir çözüm (kavramsal olarak 'tuncay' cevabı ile aynı). Önemli olan, bazı işlevleri saran bir sınıf tanımlamaktır (örneğin, Komut):

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

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

Genel yaparsan bu çok daha iyi olurdu.
Alexander

2

2 gün oldu ve kimse bir çözüm bulamadı, bu yüzden yorumumu bir cevap olarak değiştireceğim:

Anladığım kadarıyla, işlevlerin (örneğiniz gibi) ve meta sınıfların (örneğin) eşitliğini veya kimliğini kontrol edemezsiniz MyClass.self:

Ama - ve bu sadece bir fikir - yardım edemem ama wherejenerikteki cümlenin türlerin eşitliğini kontrol edebildiği görülüyor. Yani belki bundan yararlanabilirsiniz, en azından kimliği kontrol etmek için?


2

Genel bir çözüm değil, ancak bir dinleyici kalıbı uygulamaya çalışıyorsa, kayıt sırasında işlevin bir "id" sini döndürdüm, böylece daha sonra kaydını silmek için kullanabilirim (bu, orijinal soru için bir tür geçici çözümdür) "dinleyiciler" için durum, genellikle kayıtsızlaştırma, eşitlik için işlevlerin kontrol edilmesine bağlıdır, bu, en azından diğer yanıtlara göre "önemsiz" değildir).

Yani bunun gibi bir şey:

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

Şimdi sadece key"register" işlevi tarafından döndürülenleri saklamanız ve kaydı sildiğinizde bunu geçmeniz yeterlidir .


0

Çözümüm, işlevleri NSObject'i genişleten sınıfa sarmaktı

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

    init(_ function: @escaping (Type) -> Void) {
        value = function
    }
}

Bunu yaptığınızda onları nasıl karşılaştırırsınız? diyelim ki bunlardan birini bir dizi sarmalayıcıdan çıkarmak istiyorsunuz, bunu nasıl yaparsınız? Teşekkürler.
Ricardo

0

Bu soruyu altı yıl geç cevapladığımı biliyorum, ama bence sorunun arkasındaki motivasyona bakmaya değer. Soru soran şu yorumda bulundu:

Bir çağrı listesinden kapanışları referans olarak çıkaramadan kendi sarmalayıcı sınıfımızı oluşturmamız gerekir. Bu bir engeldir ve gerekli olmamalıdır.

Sanırım soruyu soran kişi aşağıdaki gibi bir geri arama listesi tutmak istiyor:

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

Ancak removeCallbackbu şekilde yazamayız çünkü ==fonksiyonlar için çalışmaz. (İkisi de yapmıyor ===.)

Geri arama listenizi yönetmenin farklı bir yolu. İçinden bir kayıt nesnesi addCallbackdöndürün ve geri aramayı kaldırmak için kayıt nesnesini kullanın. 2020 yılında Combine'ı AnyCancellablekayıt olarak kullanabiliriz .

Revize edilmiş API şuna benzer:

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

Şimdi, bir geri arama eklediğinizde, removeCallbackdaha sonra iletmek için etrafta tutmanıza gerek yoktur . removeCallbackYöntem yok . Bunun yerine, kaydeder ve geri AnyCancellablearamayı cancelkaldırmak için yöntemini çağırırsınız . Daha da iyisi, AnyCancellablebir örnek özelliğini depolarsanız , örnek yok edildiğinde otomatik olarak kendini iptal edecektir.


Buna ihtiyacımız olan en yaygın neden, yayıncılar için birden çok aboneyi yönetmektir. Combine tüm bunlar olmadan bunu çözer. C # 'nın izin verdiği ve Swift'in izin vermediği şey, iki kapamanın aynı isimli işleve başvurup başvurmadığını bulmaktır. Bu da faydalıdır, ancak çok daha az sıklıkla.
Jessy
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.