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


88

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

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

5
私の知る限り、メタクラスの同等性を確認することもできません(例MyClass.self
Jiaaro 2014年

アイデンティティについて2つのクロージャを比較する必要はありません。これを行う場所の例を挙げていただけますか?別の解決策があるかもしれません。
ビル

1
マルチキャストクロージャ、C#。(T、U)「演算子」をオーバーロードできないため、Swiftでは必然的に醜いですが、それでも自分で作成することはできます。ただし、参照によって呼び出しリストからクロージャを削除することはできませんが、独自のラッパークラスを作成する必要があります。それはドラッグであり、必要ではないはずです。
ジェシー2014年

2
すばらしい質問ですが、まったく別のことです。å参照に発音区別符号を使用することaは非常に興味深いことです。ここで検討しているコンベンションはありますか?(実際に気に入ったかどうかはわかりませんが、特に純粋な関数型プログラミングでは非常に強力な可能性があるようです。)
Rob Napier 2014年

2
私は、Array内のクロージャを格納しています@Billと見つけ、それらを除去するために、indexOfを({$ 0 ==閉鎖}を使用することはできません今、私が原因私が悪い言語設計であると信じる最適化に再構築に私のコードを持っています。。
ザックモリス

回答:


72

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

これは、意図的にサポートしたくない機能です。関数のポインターの同等性(いくつかの種類のクロージャーを含む迅速な型システムの意味で)が失敗したり、最適化に応じて変更されたりする原因はさまざまです。関数に「===」が定義されている場合、コンパイラーは、同一のメソッド本体をマージし、サンクを共有し、クロージャーで特定のキャプチャー最適化を実行することを許可されません。さらに、この種の同等性は、関数の実際のシグネチャを関数タイプが期待するものに調整する再抽象化サンクを取得できる一部のジェネリックコンテキストでは非常に驚くべきものです。

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

これは、最適化が結果に影響を与える可能性があるため、クロージャが等しいかどうかを比較しようとしてもいけないことを意味します。


19
クロージャを配列に格納していて、indexOf({$ 0 ==クロージャ}でクロージャを削除できないため、リファクタリングする必要があるため、これはちょっと壊滅的でした。IMHOの最適化は言語設計に影響を与えないはずです。したがって、マットの回答で非推奨になった@objc_blockのような簡単な修正がなければ、現時点ではSwiftはクロージャを適切に格納および取得できないと主張します。したがって、コールバックの重いコードでSwiftの使用を推奨することは適切ではないと思います。 。Web開発で遭遇種類、我々は最初の場所でスウィフトに切り替え、全理由だったような...
ザックモリス

4
@ZackMorris後で削除できるように、ある種の識別子をクロージャーとともに保存します。参照型を使用している場合は、オブジェクトへの参照を保存するだけで済みます。それ以外の場合は、独自の識別子システムを考え出すことができます。単純なクロージャの代わりに使用できるクロージャと一意の識別子を持つタイプを設計することもできます。
drewag 2016年

5
@drewagええ、回避策はありますが、ザックは正しいです。これは本当に本当に下手です。最適化が必要なことは理解していますが、開発者がいくつかのクロージャーを比較する必要があるコードのどこかにある場合は、コンパイラーにそれらの特定のセクションを最適化させないでください。または、コンパイラのある種の追加機能を作成して、異常な最適化で壊れない等価署名を作成できるようにします。これは私たちがここで話しているAppleです... XeonをiMacに収めることができれば、クロージャを同等にすることができます。ちょっと休憩して!
CommaToast 2017

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(listener、AnyObject.self)=== unsafeBitCast(f、AnyObject.self)の場合は試していますが、致命的なエラーが発生します。異なるサイズのタイプ間でunsafeBitCastを実行できません。アイデアはイベントベースのシステムを構築することですが、removeEventListenerメソッドは関数ポインターをチェックできる必要があります。
freezing_

2
Swift 2.xでは@objc_blockの代わりに@convention(block)を使用してください。素晴らしい答えです!
Gabriel.Massana 2016年

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を参照してください。

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


5
この方法は、コンパイラの最適化では信頼できませんdevforums.apple.com/message/1035180#1035180
drewag

8
これは、未定義の実装の詳細に基づくハックです。次にこれを使用すると、プログラムは未定義の結果を生成します。
eonil 2014年

8
これは、文書化されていないものと、実装の詳細が開示されていないことに依存していることに注意してください。これらは、将来、変更時にアプリをクラッシュさせる可能性があります。本番コードでの使用はお勧めしません。
Cristik 2016年

これは「クローバー」ですが、完全に機能しません。なぜこれが報奨金に報われたのか分かりません。より良い最適化をもたらすためにコンパイラーを解放して関数の等価性を自由破るという正確な目的のために、言語は意図的に関数の等価性を持っていません。
アレクサンダー

...そしてこれはまさにクリス・ラトナーが提唱しているアプローチです(一番上の答えを参照)。
pipacs

4

これは素晴らしい質問です。ChrisLattnerは意図的にこの機能をサポートしたくありませんが、多くの開発者と同様に、これが簡単な作業である他の言語からの私の気持ちを手放すことはできません。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はそうではありません。ObjBlockをSwfBlockにキャストすると、同じことが起こり、2つの異なる値になります。したがって、参照を保持するために、この種のキャストは避ける必要があります。

私はまだこの主題全体を理解していますが、私が望んでいたことの1つは@convention(block)、クラス/構造体メソッドで使用できることです。そこで、賛成票を投じたり、なぜそれが悪い考えであるかを説明する必要がある機能リクエストを提出しました。私はまた、このアプローチがすべて一緒に悪いかもしれないと感じます、もしそうなら、誰かが理由を説明できますか?


1
これがサポートされていない(そしてサポートされるべきではない)理由についてのChrisLatnerの推論を理解していないと思います。「私はまた、このアプローチがすべて一緒に悪いかもしれないと感じます、もしそうなら、誰かが理由を説明できますか?」最適化されたビルドでは、コンパイラーは、関数のポイントの同等性の概念を破る多くの方法でコードを自由にマングルすることができます。基本的な例として、ある関数の本体が別の関数と同じように起動する場合、コンパイラはマシンコードで2つをオーバーラップし、異なる出口点のみを保持する可能性があります。これにより重複が減少します
Alexander

1
基本的に、クロージャは匿名クラスのオブジェクトを開始する方法です(Javaの場合と同じですが、もっと明白です)。これらのクロージャオブジェクトはヒープに割り当てられ、クロージャによってキャプチャされたデータを格納します。これは、クロージャの関数に対する暗黙のパラメータのように機能します。クロージャオブジェクトは、明示的(func argsを介して)および暗黙的(キャプチャされたクロージャコンテキストを介して)引数を操作する関数への参照を保持します。関数本体は単一の一意のポイントとして共有できますが、囲まれた値のセットごとに1つのクロージャオブジェクトがあるため、クロージャオブジェクトのポインタ共有することはできません
アレクサンダー

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(1が参照閉鎖オブジェクトであるため、s1そしてfもう一方がどの参照閉鎖オブジェクトで、s2そしてf)。
アレクサンダー

これは素晴らしいです、ありがとう!はい、今までに私は何が起こっているのかについての写真を持っていました、そしてこれはすべてを見通しに入れます!👍
イアンBytchek

4

これが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

あなたがそれをジェネリックにしたならば、これははるかに良いでしょう。
アレクサンダー

2

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

私の知る限り、関数(例のように)とメタクラス(例)の同等性や同一性をチェックすることはできませんMyClass.self

しかし、これは単なるアイデアです。whereジェネリックスが型の同等性をチェックできるように見えることに気づかずにはいられません。それで、少なくとも身元を確認するために、それを活用できるかもしれませんか?


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、「register」関数によって返されたものを保存し、登録解除時に渡す必要があります。


0

私の解決策は、NSObjectを拡張するクラスに関数をラップすることでした

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

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

あなたがそれをするとき、それらをどのように比較しますか?ラッパーの配列からそれらの1つを削除したいとしますが、どのように行いますか?ありがとう。
リカルド

0

私はこの質問に6年遅れて答えていることを知っていますが、質問の背後にある動機を調べる価値があると思います。質問者はコメントしました:

ただし、参照によって呼び出しリストからクロージャを削除することはできませんが、独自のラッパークラスを作成する必要があります。それはドラッグであり、必要ではないはずです。

したがって、質問者は次のようなコールバックリストを維持したいと考えています。

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年には、Combineを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で許可されていないことは、2つのクロージャが同じ名前の関数を参照しているかどうかを確認することです。これも便利ですが、それほど頻繁ではありません。
ジェシー
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.