Làm thế nào để bạn kiểm tra các hàm và các bao đóng cho bằng nhau?


88

Cuốn sách nói rằng "các hàm và các bao đóng là các kiểu tham chiếu". Vì vậy, làm thế nào để bạn tìm ra nếu các tài liệu tham khảo là bằng nhau? == và === không hoạt động.

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

5
Theo như tôi có thể nói, bạn cũng không thể kiểm tra sự bình đẳng của các MyClass.self
kính đeo

Không cần thiết phải so sánh hai đóng cửa để xác định danh tính. Bạn có thể cho một ví dụ về nơi bạn sẽ làm điều này? Có thể có một giải pháp thay thế.
Bill

1
Multicast đóng, a la C #. Chúng nhất thiết phải xấu hơn trong Swift, vì bạn không thể nạp chồng "toán tử" (T, U), nhưng chúng ta vẫn có thể tự tạo chúng. Tuy nhiên, không thể xóa các bao đóng khỏi danh sách gọi bằng tham chiếu, chúng ta cần tạo lớp trình bao bọc của riêng mình. Đó là một lực cản và không cần thiết.
Jessy

2
Câu hỏi hay, nhưng hoàn toàn riêng biệt: việc bạn sử dụng dấu phụ åđể tham chiếu athực sự rất thú vị. Có quy ước nào bạn đang khám phá ở đây không? (Tôi không biết nếu tôi thực sự thích nó hay không, nhưng nó có vẻ như nó có thể là rất mạnh mẽ, đặc biệt là trong lập trình chức năng thuần túy.)
Rob Napier

2
@Bill Tôi đang lưu trữ các bao đóng trong Mảng và không thể sử dụng indexOf ({$ 0 == bao đóng} để tìm và xóa chúng. Bây giờ tôi phải cấu trúc lại mã của mình do tối ưu hóa mà tôi cho là thiết kế ngôn ngữ kém.
Zack Morris

Câu trả lời:


72

Chris Lattner đã viết trên diễn đàn nhà phát triển:

Đây là một tính năng mà chúng tôi cố ý không muốn hỗ trợ. Có nhiều thứ sẽ khiến sự bình đẳng con trỏ của các chức năng (theo nghĩa hệ thống kiểu nhanh chóng, bao gồm một số loại đóng) không thành công hoặc thay đổi tùy thuộc vào tối ưu hóa. Nếu "===" được xác định trên các hàm, trình biên dịch sẽ không được phép hợp nhất các phần thân phương thức giống hệt nhau, chia sẻ phần thu thập và thực hiện một số tối ưu hóa nắm bắt nhất định trong các bao đóng. Hơn nữa, sự bình đẳng kiểu này sẽ cực kỳ đáng ngạc nhiên trong một số ngữ cảnh chung chung, nơi bạn có thể nhận được các thu hồi tái lập để điều chỉnh chữ ký thực tế của một hàm thành chữ ký mà loại hàm mong đợi.

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

Điều này có nghĩa là bạn thậm chí không nên cố gắng so sánh các lần đóng để có sự bình đẳng vì việc tối ưu hóa có thể ảnh hưởng đến kết quả.


18
Điều này khiến tôi khó chịu, điều này thật kinh khủng vì tôi đã lưu trữ các bao đóng trong Mảng và giờ không thể xóa chúng bằng indexOf ({$ 0 == closeure} nên tôi phải cấu trúc lại. Tối ưu hóa IMHO sẽ không ảnh hưởng đến thiết kế ngôn ngữ, vì vậy nếu không có một bản sửa lỗi nhanh chóng như @objc_block hiện đã không còn được sử dụng trong câu trả lời của matt, tôi sẽ tranh luận rằng Swift không thể lưu trữ và truy xuất đúng cách vào lúc này. Vì vậy, tôi không nghĩ rằng việc ủng hộ việc sử dụng Swift trong mã nặng gọi lại là phù hợp giống như các loại gặp phải trong phát triển web nào là toàn bộ lý do chúng tôi chuyển sang Swift ở nơi đầu tiên ....
Zack Morris

4
@ZackMorris Lưu trữ một số loại số nhận dạng có đóng cửa để bạn có thể xóa nó sau này. Nếu bạn đang sử dụng các loại tham chiếu, bạn chỉ có thể lưu trữ một tham chiếu tới đối tượng, nếu không, bạn có thể đưa ra hệ thống định danh của riêng mình. Bạn thậm chí có thể thiết kế một kiểu có một bao đóng và một số nhận dạng duy nhất mà bạn có thể sử dụng thay vì một bao đóng đơn thuần.
drawag

5
@cedag Vâng, có những cách giải quyết, nhưng Zack đã đúng. Điều này thực sự là thực sự khập khiễng. Tôi hiểu muốn có tối ưu hóa, nhưng nếu có chỗ nào đó trong mã mà nhà phát triển cần so sánh một số phần đóng, thì chỉ cần trình biên dịch không tối ưu hóa các phần cụ thể đó. Hoặc tạo một số loại chức năng bổ sung của trình biên dịch cho phép nó tạo ra các chữ ký bình đẳng mà không bị phá vỡ với các tối ưu hóa kỳ lạ. Đây là Apple mà chúng ta đang nói đến ở đây ... nếu họ có thể lắp Xeon vào iMac thì chắc chắn họ có thể đóng cửa tương đương. Hãy cho tôi một break!
CommaToast,

10

Tôi đã tìm kiếm rất nhiều. Dường như không có cách nào để so sánh con trỏ hàm. Giải pháp tốt nhất tôi nhận được là đóng gói hàm hoặc đóng trong một đối tượng có thể băm. Giống:

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

2
Đây là cách tiếp cận tốt nhất cho đến nay. Thật tệ khi phải quấn và mở các lớp đóng, nhưng nó tốt hơn là sự mỏng manh không xác định, không được hỗ trợ.

8

Cách đơn giản nhất là chỉ định loại khối là @objc_blockvà bây giờ bạn có thể truyền nó tới AnyObject có thể so sánh với ===. Thí dụ:

    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

Này, tôi đang thử nếu không an toànBitCast (người nghe, AnyObject.self) === secureBitCast (f, AnyObject.self) nhưng gặp lỗi nghiêm trọng: không thể secureBitCast giữa các loại kích thước khác nhau. Ý tưởng là xây dựng một hệ thống dựa trên sự kiện nhưng phương thức removeEventListener sẽ có thể kiểm tra các con trỏ hàm.
đóng băng_

2
Sử dụng @convention (khối) thay vì @objc_block trên Swift 2.x. Câu trả lời chính xác!
Gabriel.Massana

6

Tôi cũng đang tìm kiếm câu trả lời. Và cuối cùng tôi đã tìm thấy nó.

Những gì bạn cần là con trỏ hàm thực sự và ngữ cảnh của nó ẩn trong đối tượng hàm.

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
}

Và đây là bản 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)")

Xem các URL bên dưới để biết lý do và cách hoạt động:

Như bạn thấy, nó chỉ có khả năng kiểm tra danh tính (thử nghiệm thứ 2 cho kết quả false). Nhưng điều đó phải đủ tốt.


5
Phương pháp này sẽ không đáng tin cậy với tối ưu hóa trình biên dịch devforums.apple.com/message/1035180#1035180
drawag

8
Đây là một cuộc tấn công dựa trên các chi tiết triển khai không xác định. Sau đó, sử dụng điều này có nghĩa là chương trình của bạn sẽ tạo ra một kết quả không xác định.
eonil

8
Lưu ý rằng điều này dựa trên nội dung không có giấy tờ và chi tiết triển khai không được tiết lộ, có thể làm hỏng ứng dụng của bạn trong tương lai nếu chúng thay đổi. Không khuyến khích sử dụng trong mã sản xuất.
Cristik

Đây là "cỏ ba lá", nhưng hoàn toàn không thể làm được. Tôi không biết tại sao điều này lại được thưởng một khoản tiền thưởng. Ngôn ngữ cố ý không có bình đẳng hàm, vì mục đích chính xác là giải phóng trình biên dịch để phá vỡ bình đẳng hàm một cách tự do để mang lại tối ưu hóa tốt hơn.
Alexander - Phục hồi Monica

... và đây chính xác là cách tiếp cận mà Chris Lattner ủng hộ chống lại (xem câu trả lời trên cùng).
pipacs

4

Đây là một câu hỏi tuyệt vời và trong khi Chris Lattner cố tình không muốn hỗ trợ tính năng này, tôi cũng như nhiều nhà phát triển, cũng không thể bỏ qua cảm giác của mình đến từ các ngôn ngữ khác, nơi đây là một nhiệm vụ tầm thường. Có rất nhiều unsafeBitCaství dụ, hầu hết trong số chúng không hiển thị hình ảnh đầy đủ, đây là một ví dụ chi tiết hơn :

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

Phần thú vị là cách nhanh chóng chuyển SwfBlock sang ObjBlock, nhưng trong thực tế, hai khối SwfBlock được truyền sẽ luôn có giá trị khác nhau, trong khi ObjBlocks thì không. Khi chúng ta chuyển ObjBlock sang SwfBlock, điều tương tự cũng xảy ra với chúng, chúng trở thành hai giá trị khác nhau. Vì vậy, để bảo vệ tài liệu tham khảo, nên tránh kiểu ép kiểu này.

Tôi vẫn đang hiểu toàn bộ chủ đề này, nhưng một điều tôi mong ước là khả năng sử dụng @convention(block)trên các phương thức class / struct, vì vậy tôi đã gửi một yêu cầu tính năng cần bỏ phiếu hoặc giải thích lý do tại sao đó là một ý tưởng tồi. Tôi cũng có cảm giác rằng cách tiếp cận này có thể xấu tất cả cùng nhau, nếu vậy, bất cứ ai có thể giải thích tại sao?


1
Tôi không nghĩ bạn hiểu lý do của Chris Latner về lý do tại sao điều này không được (và không nên) ủng hộ. "Tôi cũng có cảm giác rằng cách tiếp cận này có thể xấu tất cả cùng nhau, nếu vậy, có ai có thể giải thích tại sao không?" Bởi vì trong một bản dựng được tối ưu hóa, trình biên dịch có thể tự do xử lý mã theo nhiều cách phá vỡ ý tưởng về sự bình đẳng điểm của các hàm. Đối với một ví dụ cơ bản, nếu phần thân của một chức năng bắt đầu giống như cách một chức năng khác thực hiện, trình biên dịch có khả năng chồng chéo hai chức năng trong mã máy, chỉ giữ lại các điểm thoát khác nhau. Điều này làm giảm sự trùng lặp
Alexander - Phục hồi Monica

1
Về cơ bản, bao đóng là cách khởi tạo đối tượng của các lớp ẩn danh (giống như trong Java, nhưng rõ ràng hơn). Các đối tượng bao đóng này được cấp phát heap và lưu trữ dữ liệu do bao đóng thu được, hoạt động giống như tham số ngầm định đối với hàm của bao đóng. Đối tượng đóng giữ một tham chiếu đến một hàm hoạt động dựa trên args rõ ràng (thông qua func args) và ẩn (thông qua ngữ cảnh đóng đã được nắm bắt). Mặc dù thân hàm có thể được chia sẻ dưới dạng một điểm duy nhất, nhưng con trỏ của đối tượng đóng không thể được, bởi vì có một đối tượng đóng trên mỗi tập các giá trị kèm theo.
Alexander - Phục hồi Monica

1
Vì vậy, khi bạn có Struct S { func f(_: Int) -> Bool }, bạn thực sự có một hàm kiểu S.fcó kiểu (S) -> (Int) -> Bool. Chức năng này có thể được chia sẻ. Nó chỉ được tham số hóa bởi các tham số rõ ràng của nó. Bởi khi bạn sử dụng nó như một phương thức thể hiện (hoặc bằng cách ràng buộc ngầm selftham số bằng cách gọi phương thức trên một đối tượng, ví dụ S().f, hoặc bằng cách ràng buộc nó một cách rõ ràng, chẳng hạn S.f(S())), bạn tạo một đối tượng đóng mới. Đối tượng này lưu trữ một con trỏ tới S.f(có thể được chia sẻ) , but also to your instance (self , the S () `).
Alexander - Phục hồi Monica

1
Đối tượng đóng này phải là duy nhất cho mỗi phiên bản S. Nếu bình đẳng con trỏ đóng là có thể, thì bạn sẽ ngạc nhiên khi phát hiện ra rằng s1.fkhông phải là con trỏ giống như s2.f(bởi vì một là đối tượng bao đóng tham chiếu s1f, còn đối tượng kia là đối tượng bao đóng tham chiếu s2f).
Alexander - Phục hồi Monica

Điều này là tuyệt vời, cảm ơn bạn! Vâng, bây giờ tôi đã có một bức tranh về những gì đang xảy ra và điều này đặt mọi thứ vào một viễn cảnh! 👍
Ian Bytchek

4

Đây là một giải pháp khả thi (về mặt khái niệm giống như câu trả lời 'tuncay'). Vấn đề là xác định một lớp bao bọc một số chức năng (ví dụ: Command):

Nhanh:

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

Điều này sẽ tốt hơn nhiều nếu bạn làm cho nó Chung.
Alexander - Phục hồi Monica

2

Chà, đã 2 ngày rồi và chưa có ai giải quyết được vấn đề, vì vậy tôi sẽ thay đổi nhận xét của mình thành một câu trả lời:

Theo như tôi có thể nói, bạn không thể kiểm tra tính bình đẳng hoặc nhận dạng của các hàm (như ví dụ của bạn) và kính đo (ví dụ MyClass.self:):

Nhưng - và đây chỉ là một ý tưởng - tôi không thể không nhận thấy rằng wheremệnh đề trong generic dường như có thể kiểm tra sự bình đẳng của các loại. Vì vậy, có thể bạn có thể tận dụng điều đó, ít nhất là để kiểm tra danh tính?


2

Không phải là một giải pháp chung, nhưng nếu một người đang cố gắng triển khai một mẫu trình xử lý, thì tôi đã trả về một "id" của hàm trong quá trình đăng ký để tôi có thể sử dụng nó để hủy đăng ký sau này (đây là một cách giải quyết cho câu hỏi ban đầu đối với trường hợp "người nghe" như thường thì việc hủy đăng ký đi xuống để kiểm tra các hàm xem có bình đẳng không, điều này ít nhất là không "tầm thường" như các câu trả lời khác).

Vì vậy, một cái gì đó như thế này:

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

Bây giờ bạn chỉ cần lưu trữ keytrả về bởi chức năng "đăng ký" và chuyển nó khi hủy đăng ký.


0

Giải pháp của tôi là bọc các hàm thành lớp mở rộng NSObject

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

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

Khi bạn làm điều đó, làm thế nào để so sánh chúng? giả sử bạn muốn xóa một trong số chúng khỏi một mảng trình bao bọc của mình, làm cách nào để làm điều đó? Cảm ơn.
Ricardo

0

Tôi biết tôi đang trả lời câu hỏi này muộn sáu năm, nhưng tôi nghĩ điều đó đáng để xem xét động lực đằng sau câu hỏi. Người hỏi nhận xét:

Tuy nhiên, không thể xóa các bao đóng khỏi danh sách gọi bằng tham chiếu, chúng ta cần tạo lớp trình bao bọc của riêng mình. Đó là một lực cản và không cần thiết.

Vì vậy, tôi đoán người hỏi muốn duy trì một danh sách gọi lại, như thế này:

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

Nhưng chúng ta không thể viết removeCallbacktheo cách đó, bởi vì ==nó không hoạt động cho các hàm. (Cũng không ===.)

Đây là một cách khác để quản lý danh sách gọi lại của bạn. Trả về một đối tượng đăng ký từ addCallbackvà sử dụng đối tượng đăng ký để xóa lệnh gọi lại. Vào năm 2020, chúng ta có thể sử dụng Combine's AnyCancellableđể đăng ký.

API sửa đổi trông giống như sau:

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

Bây giờ, khi bạn thêm một cuộc gọi lại, bạn không cần phải giữ nó lại để chuyển đến removeCallbacksau. Không có removeCallbackphương pháp nào . Thay vào đó, bạn lưu AnyCancellablevà gọi cancelphương thức của nó để loại bỏ lệnh gọi lại. Thậm chí tốt hơn, nếu bạn lưu trữ thuộc tính AnyCancellabletrong một cá thể, thì nó sẽ tự động hủy khi cá thể bị hủy.


Lý do phổ biến nhất mà chúng tôi cần là quản lý nhiều người đăng ký cho các nhà xuất bản. Kết hợp giải quyết mà không có tất cả những điều này. Điều mà C # cho phép và Swift không cho phép là tìm hiểu xem hai hàm đóng tham chiếu đến cùng một hàm được đặt tên. Điều đó cũng hữu ích, nhưng ít thường xuyên hơn nhiều.
Jessy
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.