함수와 클로저가 같은지 어떻게 테스트합니까?


88

이 책은 "함수와 클로저는 참조 유형"이라고 말합니다. 그렇다면 참조가 동일한 지 어떻게 알 수 있습니까? == 및 === 작동하지 않습니다.

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

5
지금까지 내가 말할 수있는, 당신은 또한 메타 클래스의 평등 (예를 들어, 확인할 수 없습니다 MyClass.self)
Jiaaro

정체성을 위해 두 개의 클로저를 비교할 필요가 없습니다. 이 작업을 수행 할 곳의 예를 들어 줄 수 있습니까? 대체 솔루션이있을 수 있습니다.
Bill

1
멀티 캐스트 클로저, a la C #. (T, U) "연산자"를 오버로드 할 수 없기 때문에 Swift에서는 필연적으로 추악합니다.하지만 여전히 우리가 직접 만들 수 있습니다. 그러나 참조로 호출 목록에서 클로저를 제거 할 수는 없지만 자체 래퍼 클래스를 만들어야합니다. 그것은 드래그이며 필요하지 않습니다.
Jessy

2
좋은 질문이지만 완전히 분리 된 것입니다. å참조에 분음 부호를 사용하는 a것은 정말 흥미 롭습니다. 여기서 탐구하고있는 컨벤션이 있습니까? (I 실제로 같은 아닌지 모르겠어요,하지만 특히 순수 함수형 프로그래밍에서 매우 강력한 수처럼 보인다.)
롭 네이피어에게

2
@Bill 클로저를 Array에 저장하고 있는데 indexOf ({$ 0 == closure}를 찾아서 제거하기 위해 사용할 수 없습니다. 이제는 언어 디자인이 좋지 않다고 생각하는 최적화로 인해 코드를 재구성해야합니다.
잭 모리스

답변:


72

Chris Lattner는 개발자 포럼에 다음과 같이 썼습니다.

이것은 우리가 의도적으로 지원하고 싶지 않은 기능입니다. 최적화에 따라 포인터가 동일한 함수 (여러 종류의 클로저를 포함하는 신속한 유형 시스템 의미에서)가 실패하거나 변경되도록하는 다양한 요소가 있습니다. "==="가 함수에 정의 된 경우 컴파일러는 동일한 메서드 본문을 병합하고, 썽크를 공유하고, 클로저에서 특정 캡처 최적화를 수행 할 수 없습니다. 또한 이러한 종류의 동등성은 일부 제네릭 컨텍스트에서 매우 놀랍습니다. 여기서 함수의 실제 서명을 함수 유형이 예상하는 것으로 조정하는 재 추출 썽크를 얻을 수 있습니다.

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

이는 최적화가 결과에 영향을 미칠 수 있기 때문에 클로저를 동등하게 비교하려고 시도하지 않아야 함을 의미합니다.


18
이건 저를 깨 물었습니다. 저는 클로저를 Array에 저장하고 있었고 이제 indexOf ({$ 0 == closure}로 클로저를 제거 할 수 없기 때문에 리팩토링해야합니다. IMHO 최적화는 언어 디자인에 영향을주지 않아야합니다. 그래서 Matt의 대답에서 현재 사용되지 않는 @objc_block과 같은 빠른 수정 없이는 Swift가 현재 클로저를 제대로 저장하고 검색 할 수 없다고 주장합니다. 그래서 콜백 무거운 코드에서 Swift 사용을 옹호하는 것은 적절하지 않다고 생각합니다. . 우리가 처음에 신속한 전환 전체 이유였다 웹 개발에서 발생하는 종류 ... 등
잭 모리스

4
@ZackMorris 클로저와 함께 일종의 식별자를 저장하여 나중에 제거 할 수 있습니다. 참조 유형을 사용하는 경우 객체에 대한 참조를 저장할 수 있습니다. 그렇지 않으면 고유 한 식별자 시스템을 만들 수 있습니다. 일반 클로저 대신 사용할 수있는 클로저와 고유 식별자가있는 유형을 디자인 할 수도 있습니다.
drewag

5
@drewag 예, 해결 방법이 있지만 Zack이 옳습니다. 이것은 정말 절름발이입니다. 최적화를 원한다는 것은 이해하지만 개발자가 일부 클로저를 비교해야하는 코드가 있다면 컴파일러가 특정 섹션을 최적화하지 않도록하면됩니다. 또는 이상한 최적화로 깨지지 않는 평등 시그니처를 생성 할 수 있도록 컴파일러의 추가 기능을 만드십시오. 이것은 우리가 여기서 이야기하고있는 애플입니다. 제온을 iMac에 맞출 수 있다면 확실히 비슷한 클로저를 만들 수 있습니다. 나에게 휴식을 줘!
CommaToast

10

나는 많이 검색했다. 함수 포인터 비교 방법이없는 것 같습니다. 내가 얻은 최고의 솔루션은 해시 가능한 객체에 함수 또는 클로저를 캡슐화하는 것입니다. 처럼:

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

2
이것은 지금까지 최선의 접근 방식입니다. 클로저를 감싸고 풀어야하는 것은 짜증나지만 비결정적이고 지원되지 않는 취약성보다는 낫습니다.

8

가장 간단한 방법은 블록 유형을로 지정하는 것입니다. @objc_block이제 .NET과 비슷한 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

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을 참조하십시오.

보시다시피 신원 만 확인할 수 있습니다 (두 번째 테스트에서는 false). 하지만 그것만으로도 충분합니다.


5
이 방법은 컴파일러 최적화와 신뢰할 수없는 것입니다은 devforums.apple.com/message/1035180#1035180
drewag

8
이것은 정의되지 않은 구현 세부 정보를 기반으로 한 해킹입니다. 그런 다음 이것을 사용하면 프로그램이 정의되지 않은 결과를 생성합니다.
eonil

8
이는 문서화되지 않은 항목과 공개되지 않은 구현 세부 사항에 의존하며, 향후 변경시 앱이 중단 될 수 있습니다. 프로덕션 코드에는 사용하지 않는 것이 좋습니다.
Cristik

이것은 "클로버"이지만 완전히 작동하지 않습니다. 왜 이것이 현상금을 받았는지 모르겠습니다. 더 나은 최적화를 얻기 위해 컴파일러가 함수 동등성을 자유롭게 깨뜨리는 정확한 목적을 위해 언어는 의도적으로 함수 동등성을 갖지 않습니다 .
알렉산더 - 분석 재개 모니카

... 그리고 이것은 Chris Lattner가 반대하는 접근 방식입니다 (상위 답변 참조).
pipacs

4

이것은 좋은 질문이며 Chris Lattner는 의도적으로이 기능을 지원하고 싶지 않지만 많은 개발자와 마찬가지로 사소한 작업 인 다른 언어에서 오는 내 감정을 놓을 수 없습니다. 많은 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

흥미로운 부분은 SwfBlock을 ObjBlock으로 신속하게 캐스팅하는 방법이지만 실제로 두 개의 캐스팅 된 SwfBlock 블록은 항상 다른 값이지만 ObjBlocks는 그렇지 않습니다. ObjBlock을 SwfBlock으로 캐스트하면 똑같은 일이 발생하고 두 개의 다른 값이됩니다. 따라서 참조를 보존하려면 이러한 종류의 캐스팅을 피해야합니다.

나는 여전히이 전체 주제를 이해하고 있지만, 내가 바라는 한 가지는 @convention(block)클래스 / 구조체 메서드에서 사용할 수 있는 기능 이므로 up-voting이 필요한 기능 요청 을 제출 하거나 왜 나쁜 생각인지 설명했습니다. 나는 또한이 접근 방식이 모두 나쁠 수 있음을 느낍니다. 그렇다면 누구든지 이유를 설명 할 수 있습니까?


1
나는 이것이 왜 지원되지 않는지 (그리고 지원되어서는 안되는) 이유에 대한 Chris Latner의 추론을 이해하지 못한다고 생각합니다. "저는 또한이 접근 방식이 모두 나쁘다는 느낌을받습니다. 그렇다면 누구든지 이유를 설명 할 수 있습니까?" 최적화 된 빌드에서 컴파일러는 함수의 점 동일성 개념을 깨뜨리는 여러 방법으로 코드를 자유롭게 조작 할 수 있기 때문입니다. 기본 예를 들어, 한 함수의 본문이 다른 함수와 동일한 방식으로 시작하면 컴파일러는 기계 코드에서 두 가지를 겹치게되고 다른 종료 지점 만 유지합니다. 이것은 중복 감소
알렉산더 - 분석 재개 모니카

1
기본적으로 클로저는 익명 클래스의 객체를 시작하는 방법입니다 (Java에서와 같지만 더 분명합니다). 이러한 클로저 객체는 힙 할당되며 클로저에 의해 캡처 된 데이터를 저장하며 클로저의 기능에 대한 암시 적 매개 변수처럼 작동합니다. 클로저 객체는 명시 적 (func args를 통해) 및 암시 적 (캡처 된 클로저 컨텍스트를 통해) 인수에 대해 작동하는 함수에 대한 참조를 보유합니다. 함수 본문은 단일 고유 지점으로 공유 할 수 있지만, 닫힌 값 세트당 하나의 클로저 객체가 있기 때문에 클로저 객체의 포인터 공유 할 수 없습니다 .
알렉산더 - 분석 재개 모니카

1
그래서 당신이 가지고있을 때 Struct S { func f(_: Int) -> Bool }, 당신은 실제로 type 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, 다른 어떤 참조 폐쇄 개체입니다 s2f).
알렉산더 - 분석 재개 모니카

훌륭합니다, 감사합니다! 예, 지금까지 무슨 일이 일어나고 있는지 사진을 찍었고 모든 것을 원근감있게 보여줍니다! 👍
Ian Bytchek

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

자바:

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

Generic으로 만들면 훨씬 나을 것입니다.
알렉산더 - 분석 재개 모니카

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

그렇게 할 때 어떻게 비교합니까? 래퍼 배열에서 그중 하나를 제거하고 싶다고 가정 해 보겠습니다. 어떻게 수행합니까? 감사.
Ricardo

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인스턴스 속성에 를 저장 하면 인스턴스가 소멸 될 때 자동으로 취소된다는 것입니다.


이것이 필요한 가장 일반적인 이유는 게시자를 위해 여러 구독자를 관리하기위한 것입니다. Combine은이 모든 문제를 해결합니다. C #이 허용하고 Swift는 허용하지 않는 것은 두 개의 클로저가 동일한 이름의 함수를 참조하는지 확인하는 것입니다. 그것은 또한 유용하지만 훨씬 덜 자주 사용됩니다.
Jessy
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.