web-dev-qa-db-ja.com

関数とクロージャーの等価性をどのようにテストしますか?

本は「関数とクロージャは参照型である」と言っています。 それでは、参照が等しいかどうかをどのように確認しますか? ==および===は機能しません。

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

Catterwaulsがこれに対処する方法は次のとおりです。

MultiClosures&Equatable Closures

テスト

82
Jessy

Chris Lattnerは開発者フォーラムで次のように書いています。

これは、意図的にサポートしたくない機能です。関数のポインター等価性(Swift型システムの意味で、いくつかの種類のクロージャーを含む)が失敗または最適化に応じて変更する原因となるさまざまなことがあります。 "===関数で定義されているため、コンパイラは同一のメソッドボディをマージし、サンクを共有し、クロージャで特定のキャプチャ最適化を実行できません。関数の実際のシグネチャを、関数タイプが期待するものに調整します。

https://devforums.Apple.com/message/1035180#103518

これは、最適化が結果に影響を与える可能性があるため、等価性について閉包を比較することすらしてはならないことを意味します。

64
drewag

最も簡単な方法は、ブロックタイプを@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
8
matt

私も答えを探していました。そして、ついに見つけました。

必要なのは、実際の関数ポインターと、関数オブジェクトに隠されたそのコンテキストです。

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を参照して、その理由と仕組みを確認してください。

ご覧のとおり、IDのみをチェックできます(2回目のテストではfalseが得られます)。しかし、それで十分でしょう。

6
dankogai

よく検索しました。関数ポインタを比較する方法はないようです。私が得た最善の解決策は、関数またはクロージャをハッシュ可能なオブジェクトにカプセル化することです。お気に入り:

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

考えられる解決策の1つを示します(概念的には 'tuncay'回答と同じです)。ポイントは、いくつかの機能をラップするクラスを定義することです(例:コマンド):

迅速:

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
4
baso

これは素晴らしい質問です。クリスラトナーはこの機能を意図的にサポートしたくありませんが、多くの開発者と同様に、これが些細な作業である他の言語からの私の気持ちを手放すこともできません。 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にキャストすることですが、実際には2つのキャストされたSwfBlockブロックは常に異なる値になりますが、ObjBlocksはそうではありません。これらは2つの異なる値になるため、参照を保持するには、この種のキャストを避ける必要があります。

私はまだこの主題全体を理解していますが、クラス/構造体のメソッドで@convention(block)を使用できることを残したいので、 feature request 投票するか、それが悪い考えである理由を説明する必要があります。また、このアプローチが一緒に悪いかもしれないという感覚があります。もしそうなら、誰が理由を説明できますか?

4
Ian Bytchek

さて、2日が経ちましたが、誰も解決策に夢中になっていないので、コメントを回答に変更します。

私の知る限り、関数(例など)とメタクラス(例:MyClass.self):

しかし、これは単なるアイデアです。 genericsのwhere が型の等価性をチェックできるように見えることに気づかずにはいられません。それで、少なくともアイデンティティをチェックするために、あなたはそれを活用することができますか?

2
Jiaaro

一般的な解決策ではありませんが、リスナーパターンを実装しようとすると、登録中に関数の「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);
    }
}

これで、「register」関数によって返されたkeyを保存し、登録解除時に渡すだけで済みます。

0
vir us