Multicast Block: How to generalize

168 views Asked by At

Goal

I have a class with various properties that can be used to plug in a block to receive certain events.

@interface SomeClass

@property (copy, nonatomic) void (^handler)(int arg1, int arg2);

@end

In the client code, I would like to dynamically add / remove handler blocks to this property, similar to a MulticastDelegate in C#.

self.logger = ^(int arg1, int arg2){
    NSLog(@"arg1 = %d, arg2 = %d", arg1, arg2);
};

void (^doSomething)(int, int) = ^(int arg1, int arg2){
    if (arg1 == 42) {
        // Do something.
    }
};

For example, I would like to plug in logger in -(id)init, but only use doSomething while a certain method is running. While doSomething is plugged in, logger should still run.

Current implementation

To maintain the blocks, I thought about using an NSMutableArray that stores copies of the blocks and broadcasts the event to all registered blocks (observer pattern).

- (id)init

self.handlerBlocks = [NSMutableArray array];
__weak typeof(self) weakSelf = self;
self.object.handler = ^(int x, int y){
    typeof(self) strongSelf = weakSelf;
    if (!strongSelf) {
        return;
    }
    for (void (^item)(int x, int y) in strongSelf.handlerBlocks) {
        item(x, y);
    }
};

[self.handlerBlocks addObject:[self.logger copy]];

- (void)someOtherMethod

void (^doSomething)(int, int) = [^(int arg1, int arg2){
    if (arg1 == 42) {
        // Do something.
    }
} copy];
[self.handlerBlocks addObject:doSomething copy];
// Do something.
[self.handlerBlocks removeObject:doSomething];

Open questions

Can the method be generalized to blocks with any argument count / types? So that I could use it like this:

MulticastBlock *b = [[MulticastBlock alloc] init];
self.object.handler = b;

[b addBlock:self.logger];

The problem here is that the type of self.object.handler is void (^)(int, int). Therefore, MulticastBlock would need to mimic a block, forwarding any invocations it receives to the array.

Could the techniques described here by used?

Maybe intercepting all invocations, copying them for every array element and assigning new invocation targets?

1

There are 1 answers

2
CRD On

From the link you gave to mikeash.com you'll see doing this in code is a challenge, and not something to include in production code. For similar reasons C# stuff works because it is provided by the runtime, you couldn't easily write it yourself in C#. Even parametric polymorphism won't help you here, that won't get you the block call with a varying number of arguments.

What you need is "parametric polymorphism" by string expansion... i.e. macros.

Here is a sample "MulticastBlock.h" file:

#define MULTICAST(name, typelist, arglist) \
\
@interface name : NSObject \
\
@property (readonly) void (^block)typelist; \
\
- (id) addBlock:(void (^)typelist)aBlock; \
\
- (void) removeBlock:(id)token; \
\
@end

MULTICAST(MulticastBlock, (int arg1, int arg2), (arg1, arg2))
MULTICAST(MulticastBlock2, (NSString *arg1, NSString *arg2), (arg1, arg2))

#undef MULTICAST

This defines a macro which expands out to an @interface, uses it twice, then removes the macro as its no longer needed.

The implementation follows your code and is done likewise with a macro - it uses the arglist macro argument for the call in the loop, I just include it here for consistency though it isn't used.

The only significant change I made to your code was using an NSMutableDictionary with a auto-generated key (just an increasing number) - the key is returned by addBlock: and accepted by removeBlock: and avoids any issues with blocks being copied (two blocks are only equal if they are the same block)

Not exactly what you'd like but it works.

Addendum

OK, it wasn't clear how to use this, here is my test code which should explain all:

MulticastBlock *multicast = MulticastBlock.new;

id tokenAdd = [multicast addBlock:^(int arg1, int arg2) {
   NSLog(@"%d + %d -> %d", arg1, arg2, arg1 + arg2);
}];

multicast.block(3, 4);

id tokenMul = [multicast addBlock:^(int arg1, int arg2) {
   NSLog(@"%d * %d -> %d", arg1, arg2, arg1 * arg2);
}];

multicast.block(4, 5);

[multicast removeBlock:tokenAdd];

multicast.block(5, 6);

[multicast removeBlock:tokenMul];

multicast.block(6, 7);

MulticastBlock2 *two = MulticastBlock2.new;

[two addBlock:^(NSString *arg1, NSString *arg2) {
   NSLog(@"%@ | %@", arg1, arg2);
}];

two.block(@"asda", @"tesco");