Bagaimana Anda menguji fungsi dan closure untuk persamaan?


88

Buku tersebut mengatakan bahwa "fungsi dan penutup adalah tipe referensi". Jadi, bagaimana Anda mengetahui apakah referensinya sama? == dan === tidak berfungsi.

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

5
Sejauh yang saya tahu, Anda juga tidak dapat memeriksa kesetaraan metaclasses (misalnya, MyClass.self)
Jiaaro

Seharusnya tidak perlu membandingkan dua penutupan untuk identitas. Dapatkah Anda memberikan contoh di mana Anda akan melakukan ini? Mungkin ada solusi alternatif.
Bill

1
Penutupan multicast, ala C #. Mereka pasti lebih jelek di Swift, karena Anda tidak bisa membebani "operator" (T, U), tapi kita masih bisa membuatnya sendiri. Namun, tanpa bisa menghapus closure dari daftar pemanggilan dengan referensi, kita perlu membuat kelas pembungkus kita sendiri. Itu membosankan, dan seharusnya tidak perlu.
Jessy

2
Pertanyaan bagus, tetapi hal yang sama sekali terpisah: penggunaan diakritik åuntuk referensi asangat menarik. Apakah ada konvensi yang Anda jelajahi di sini? (Saya tidak tahu apakah saya benar-benar suka atau tidak; tetapi sepertinya itu bisa sangat kuat, terutama dalam pemrograman fungsional murni.)
Rob Napier

2
@Bill Saya menyimpan closure dalam Array dan tidak dapat menggunakan indexOf ({$ 0 == closure} untuk menemukan dan menghapusnya. Sekarang saya harus menyusun ulang kode saya karena pengoptimalan yang saya yakini sebagai desain bahasa yang buruk.
Zack Morris

Jawaban:


72

Chris Lattner menulis di forum pengembang:

Ini adalah fitur yang sengaja tidak ingin kami dukung. Ada berbagai hal yang akan menyebabkan kesamaan fungsi penunjuk (dalam pengertian sistem tipe cepat, yang mencakup beberapa jenis penutupan) gagal atau berubah tergantung pada pengoptimalan. Jika "===" didefinisikan pada fungsi, kompilator tidak akan diizinkan untuk menggabungkan badan metode yang identik, berbagi thunks, dan melakukan optimasi penangkapan tertentu dalam penutupan. Lebih lanjut, persamaan semacam ini akan sangat mengejutkan dalam beberapa konteks umum, di mana Anda bisa mendapatkan reabstraction thunks yang menyesuaikan tanda tangan sebenarnya dari suatu fungsi dengan yang diharapkan jenis fungsi.

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

Ini berarti bahwa Anda tidak boleh mencoba membandingkan penutupan untuk kesetaraan karena pengoptimalan dapat memengaruhi hasil.


19
Ini baru saja menggigit saya, yang agak menghancurkan karena saya telah menyimpan closure di Array dan sekarang tidak bisa menghapusnya dengan indexOf ({$ 0 == closure} jadi saya harus refactor. Pengoptimalan IMHO seharusnya tidak memengaruhi desain bahasa, jadi tanpa perbaikan cepat seperti @objc_block yang sekarang sudah usang dalam jawaban matt, saya berpendapat bahwa Swift tidak dapat menyimpan dan mengambil penutupan dengan benar saat ini. Jadi menurut saya tidak pantas untuk menganjurkan penggunaan Swift dalam kode berat callback seperti yang ditemukan dalam pengembangan web. Itulah alasan utama kami beralih ke Swift sejak awal ...
Zack Morris

4
@ZackMorris Menyimpan semacam pengenal dengan penutup sehingga Anda dapat menghapusnya nanti. Jika Anda menggunakan tipe referensi, Anda dapat menyimpan referensi ke objek jika tidak, Anda dapat membuat sistem pengenal Anda sendiri. Anda bahkan bisa mendesain tipe yang memiliki closure dan identifier unik yang bisa Anda gunakan sebagai ganti closure biasa.
menggambar

5
@drewag Ya, ada beberapa solusi, tapi Zack benar. Ini benar-benar payah. Saya memahami keinginan untuk memiliki pengoptimalan, tetapi jika ada suatu tempat dalam kode yang pengembang perlu membandingkan beberapa penutupan, maka cukup minta kompilator tidak mengoptimalkan bagian-bagian tersebut. Atau buat semacam fungsi tambahan dari kompilator yang memungkinkannya membuat tanda tangan kesetaraan yang tidak rusak dengan pengoptimalan yang gagal. Ini adalah Apple yang sedang kita bicarakan di sini ... jika mereka dapat memasukkan Xeon ke dalam iMac maka mereka pasti dapat membuat penutupan yang sebanding. Beri aku istirahat!
CommaToast

10

Saya banyak mencari. Tampaknya tidak ada cara untuk membandingkan penunjuk fungsi. Solusi terbaik yang saya dapatkan adalah merangkum fungsi atau penutupan dalam objek hashable. Suka:

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

2
Sejauh ini, ini adalah pendekatan terbaik. Menyebalkan harus membungkus dan membuka penutup, tetapi itu lebih baik daripada kerapuhan yang nondeterministik dan tidak didukung.

8

Cara termudah adalah menetapkan tipe blok sebagai @objc_block, dan sekarang Anda dapat mentransmisikannya ke AnyObject yang sebanding dengan ===. Contoh:

    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

Hai, saya mencoba jika unsafeBitCast (listener, AnyObject.self) === unsafeBitCast (f, AnyObject.self) tetapi mendapatkan kesalahan fatal: tidak bisa unsafeBitCast antara jenis ukuran yang berbeda. Idenya adalah untuk membangun sistem berbasis peristiwa tetapi metode removeEventListener harus dapat memeriksa pointer fungsi.
pembekuan_

2
Gunakan @convention (block) daripada @objc_block di Swift 2.x. Jawaban yang bagus!
Gabriel. Massana

6

Saya sudah mencari jawabannya juga. Dan akhirnya saya menemukannya.

Yang Anda butuhkan adalah penunjuk fungsi aktual dan konteksnya yang tersembunyi di objek fungsi.

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
}

Dan inilah demonya:

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

Lihat URL di bawah untuk mengetahui mengapa dan bagaimana cara kerjanya:

Seperti yang Anda lihat, ini hanya mampu memeriksa identitas (hasil tes ke-2 false). Tapi itu seharusnya sudah cukup.


5
Metode ini tidak akan dapat diandalkan dengan pengoptimalan kompiler devforums.apple.com/message/1035180#1035180
drewag

8
Ini adalah peretasan berdasarkan detail implementasi yang tidak ditentukan. Kemudian menggunakan ini berarti program Anda akan menghasilkan hasil yang tidak ditentukan.
eonil

8
Perhatikan bahwa ini bergantung pada hal-hal yang tidak terdokumentasi, dan detail implementasi yang tidak diungkapkan, yang dapat membuat aplikasi Anda mogok di masa mendatang jika berubah. Tidak disarankan untuk digunakan dalam kode produksi.
Cristik

Ini adalah "semanggi", tapi sama sekali tidak bisa dijalankan. Saya tidak tahu mengapa ini diberi hadiah. Bahasa tersebut sengaja tidak memiliki persamaan fungsi, dengan tujuan yang tepat untuk membebaskan compiler untuk memutuskan persamaan fungsi secara bebas untuk menghasilkan pengoptimalan yang lebih baik.
Alexander

... dan inilah pendekatan yang didukung Chris Lattner (lihat jawaban atas).
pipacs

4

Ini adalah pertanyaan yang bagus dan sementara Chris Lattner dengan sengaja tidak ingin mendukung fitur ini, saya, seperti banyak pengembang, juga tidak dapat melepaskan perasaan saya yang berasal dari bahasa lain di mana ini adalah tugas yang sepele. Ada banyak unsafeBitCastcontoh, kebanyakan tidak menunjukkan gambaran lengkap, berikut ini lebih detailnya :

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

Bagian yang menarik adalah betapa cepatnya melemparkan SwfBlock ke ObjBlock, namun pada kenyataannya dua blok SwfBlock yang dicor akan selalu memiliki nilai yang berbeda, sementara ObjBlocks tidak. Saat kami mentransmisikan ObjBlock ke SwfBlock, hal yang sama terjadi pada mereka, keduanya menjadi dua nilai yang berbeda. Jadi, untuk mempertahankan referensi, pengecoran semacam ini harus dihindari.

Saya masih memahami keseluruhan subjek ini, tetapi satu hal yang saya inginkan adalah kemampuan untuk menggunakan @convention(block)metode kelas / struct, jadi saya mengajukan permintaan fitur yang memerlukan pemungutan suara atau menjelaskan mengapa itu ide yang buruk. Saya juga merasa pendekatan ini mungkin buruk secara keseluruhan, jika demikian, adakah yang bisa menjelaskan mengapa?


1
Saya tidak berpikir Anda memahami alasan Chris Latner tentang mengapa ini tidak (dan tidak seharusnya) didukung. "Saya juga merasa pendekatan ini mungkin buruk secara keseluruhan, jika demikian, adakah yang bisa menjelaskan mengapa?" Karena dalam build yang dioptimalkan, compiler bebas untuk mengacaukan kode dengan banyak cara yang mematahkan ide persamaan titik fungsi. Sebagai contoh dasar, jika isi satu fungsi dimulai dengan cara yang sama seperti fungsi lainnya, kompilator kemungkinan besar akan menimpa keduanya dalam kode mesin, hanya mempertahankan titik keluar yang berbeda. Ini mengurangi duplikasi
Alexander

1
Pada dasarnya, closure adalah cara memulai objek kelas anonim (seperti di Java, tapi ada yang lebih jelas). Objek closure ini dialokasikan heap, dan menyimpan data yang ditangkap oleh closure, yang bertindak seperti parameter implisit untuk fungsi closure. Objek closure memegang referensi ke sebuah fungsi yang beroperasi pada args eksplisit (melalui func args) dan implisit (melalui konteks closure yang ditangkap). Meskipun badan fungsi bisa dibagikan sebagai satu titik unik, penunjuk objek penutupan tidak bisa, karena ada satu objek penutup per kumpulan nilai yang diapit.
Alexander

1
Jadi ketika Anda memiliki Struct S { func f(_: Int) -> Bool }, Anda sebenarnya memiliki fungsi tipe S.fyang memiliki tipe (S) -> (Int) -> Bool. Fungsi ini dapat dibagikan. Ini hanya diparameterisasi oleh parameter eksplisitnya. Saat Anda menggunakannya sebagai metode instance (baik dengan mengikat selfparameter secara implisit dengan memanggil metode pada objek, misalnya S().f, atau dengan mengikatnya secara eksplisit, misalnya S.f(S())), Anda membuat objek penutupan baru. Objek ini menyimpan pointer ke S.f(yang bisa dibagi) , but also to your instance (self , the S () `).
Alexander

1
Objek penutup ini harus unik untuk setiap instance S. Jika persamaan closure pointer dimungkinkan, maka Anda akan terkejut menemukan bahwa s1.fitu bukan pointer yang sama s2.f(karena satu adalah objek closure yang mereferensikan s1dan f, dan yang lainnya adalah objek closure yang mereferensikan s2dan f).
Alexander

Ini luar biasa, terima kasih! Ya, sekarang saya memiliki gambaran tentang apa yang sedang terjadi dan ini menempatkan semuanya ke dalam sebuah perspektif! 👍
Ian Bytchek

4

Berikut adalah satu solusi yang mungkin (secara konseptual sama dengan jawaban 'tuncay'). Intinya adalah untuk menentukan kelas yang membungkus beberapa fungsionalitas (misalnya, Perintah):

Cepat:

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

Jawa:

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

Ini akan jauh lebih baik jika Anda membuatnya menjadi Generik.
Alexander

2

Nah sudah 2 hari dan tidak ada yang ikut campur dengan solusi, jadi saya akan mengubah komentar saya menjadi jawaban:

Sejauh yang saya tahu, Anda tidak dapat memeriksa persamaan atau identitas fungsi (seperti contoh Anda) dan metaclasses (misalnya, MyClass.self):

Tetapi - dan ini hanya sebuah ide - saya tidak bisa tidak memperhatikan bahwa whereklausa dalam obat generik tampaknya dapat memeriksa persamaan jenis. Jadi mungkin Anda bisa memanfaatkannya, setidaknya untuk memeriksa identitas?


2

Bukan solusi umum, tetapi jika seseorang mencoba menerapkan pola pendengar, saya akhirnya mengembalikan "id" dari fungsi tersebut selama pendaftaran sehingga saya dapat menggunakannya untuk membatalkan pendaftaran nanti (yang merupakan semacam solusi untuk pertanyaan asli untuk kasus "pendengar" karena biasanya unregistering dilakukan pemeriksaan fungsi untuk persamaan, yang setidaknya tidak "sepele" seperti jawaban lainnya).

Jadi seperti ini:

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

Sekarang Anda hanya perlu menyimpan yang keydikembalikan oleh fungsi "register" dan meneruskannya saat membatalkan pendaftaran.


0

Solusi saya adalah membungkus fungsi ke kelas yang memperluas NSObject

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

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

Ketika Anda melakukan itu, bagaimana membandingkannya? katakanlah Anda ingin menghapus salah satunya dari array pembungkus Anda, bagaimana Anda melakukannya? Terima kasih.
Ricardo

0

Saya tahu saya menjawab pertanyaan ini enam tahun terlambat, tetapi saya pikir ada baiknya melihat motivasi di balik pertanyaan itu. Penanya berkomentar:

Namun, tanpa bisa menghapus closure dari daftar pemanggilan dengan referensi, kita perlu membuat kelas pembungkus kita sendiri. Itu membosankan, dan seharusnya tidak perlu.

Jadi saya kira si penanya ingin mempertahankan daftar panggilan balik, seperti ini:

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

Tapi kita tidak bisa menulis removeCallbackseperti itu, karena ==tidak berfungsi untuk fungsi. (Tidak juga ===.)

Berikut cara lain untuk mengelola daftar panggilan balik Anda. Kembalikan objek pendaftaran dari addCallback, dan gunakan objek pendaftaran untuk menghapus callback. Di sini di tahun 2020, kita bisa menggunakan Combine AnyCancellablesebagai registrasi.

API yang direvisi terlihat seperti ini:

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

Sekarang, saat Anda menambahkan callback, Anda tidak perlu menyimpannya untuk diteruskan removeCallbacknanti. Tidak ada removeCallbackmetode. Sebagai gantinya, Anda menyimpan AnyCancellable, dan memanggil cancelmetodenya untuk menghapus callback. Lebih baik lagi, jika Anda menyimpan properti AnyCancellablein an instance, maka itu akan membatalkan dirinya sendiri secara otomatis saat instance dimusnahkan.


Alasan paling umum yang kami perlukan adalah mengelola banyak pelanggan untuk penerbit. Gabungkan solusinya tanpa semua ini. Apa yang diizinkan C #, dan Swift tidak, adalah mencari tahu apakah dua closure merujuk pada fungsi bernama yang sama. Itu juga berguna, tetapi lebih jarang.
Jessy
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.