0

Given this piece of code in ObjC that is used by an app, I want to convert it to Swift:

typedef void (^VoidBlock)(void);

@interface Worker : NSObject

- (void)add:(VoidBlock)completion;
- (void)remove:(VoidBlock)completion;
- (void)execute;

@end


@implementation Worker {
   NSMutableArray<VoidBlock> *completions;
}

- (void)add:(VoidBlock)completion {
   [completions addObject:completion];
}

- (void)remove:(VoidBlock)completion {
   [completions removeObject:completion];
}

- (void)execute {
    // ...
    [completions enumerateObjectsUsingBlock:^(VoidBlock  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
       obj();
    }];
}
@end

I have problems when implementing remove. I tried several things, but nothing. Basically, I cannot compare closures in Swift because evidently closures don't conform to Equatable protocol, so, it cannot be found in the array. Perhaps the memory address can be used for this, I don't know if there is a problem between objc pointer and swift references. In theory, this code could be used from objc or swift. Another idea I had was to use something like:

let firstIndex = hardWorkers.firstIndex(where:{ 
    $0 === hardWorkerClosure
})

Note the identity operator, but that does not work. A friend told me that perhaps I had to use a container for the closure/block. I remember in Objc we have NSValue to wrap a primitive type inside a class, so, we can save them inside NSMutableArray, but I don't remember anything to save blocks. Do you have any idea or suggestion about how to solve this problem? Thanks a lot.

Ricardo
  • 2,831
  • 4
  • 29
  • 42
  • 1
    Can you show the parts of the Swift code that you successfully converted from Objective-C as well? – Sweeper May 16 '20 at 14:25
  • 1
    Closures aren't comparable, and don't have a defined identity, in order to give the compiler more room to play, to optimize and merge closure implementations. If you need to associate identity with a closure, you'll need to do that yourself: https://stackoverflow.com/a/45284442/3141234 – Alexander May 16 '20 at 14:29
  • 3
    Does this answer your question? [In Swift 3, what is a way to compare two closures?](https://stackoverflow.com/questions/45270371/in-swift-3-what-is-a-way-to-compare-two-closures) – rebusB May 16 '20 at 14:32
  • Sweeper, that's the only code I have. It was a question in an interview. So, there is room for assumptions, I guess. – Ricardo May 17 '20 at 11:01
  • rebusB, in fact, my idea was to pass an identifier with the closure, but in theory, we should not modify the code that is already using this class. I mea, the consumer does not care if the Worker is implemented in objc or swift. Any other suggestion? – Ricardo May 17 '20 at 11:04
  • @Alexander-ReinstateMonica when you say closures are not comparable, are you talking exclusively about swift? Does that mean we can compare blocks using objc (through the memory address) but this cannot be done in Swift? Thanks a lot. – Ricardo May 17 '20 at 11:05
  • 1
    @Ricardo In ObjC, blocks are universally handled by their address. You pass around pointers to them to ARC them (if you're using manual memory management), copy them, etc. In Swift, they intentionally sought out to *not* expose closure identity into "userspace" Swift code. Motivation is here: https://stackoverflow.com/a/25694072/3141234 – Alexander May 17 '20 at 13:00
  • 1
    Swift closures also aren't just one pointer: https://forums.swift.org/t/is-it-possible-to-get-the-retain-count-of-a-closure-in-swift/36551/2?u=alexanderm – Alexander May 17 '20 at 13:06

1 Answers1

1

Closures don't have identity in Swift, which means that you cannot use === to find out if two closures point to the same code. If you really need this, one solution would be to build an abstraction over the closure:

typealias VoidClosure = () -> Void

class VoidClosureRef {
    let body: VoidClosure

    init(_ body: @escaping VoidClosure) {
        self.body = body
    }
}

You can then circulate instances of VoidClosureRef:

class Worker {
    private var completions: [VoidClosureRef] = []

    func add(_ completion: VoidClosureRef) { 
        completions.append(completion)
    }
    
    func remove(_ completion: VoidClosureRef) { 
        completions.removeAll { $0 === completion }
    }
}

However, a better design here would be for the add method to return an identifier that can later be used to remove the closure.

typealias VoidClosure = () -> Void

class Worker {
    private var completions: [UUID: VoidClosure] = [:]

    func add(_ completion: @escaping VoidClosure) -> UUID {
        let uuid = UUID()
        completions[uuid] = completion
        return uuid
    }
    
    func remove(_ uuid: UUID) {
        completions.removeValue(forKey: uuid)
    }
}
Cristik
  • 30,989
  • 25
  • 91
  • 127