Reputation: 8760
I've been playing with blocks and encountered a weird behavior. This is the interface/implementation, which just holds a block with the ability to execute it:
@interface TestClass : NSObject {
#if NS_BLOCKS_AVAILABLE
void (^blk)(void);
#endif
}
- (id)initWithBlock:(void (^)(void))block;
- (void)exec;
@end
@implementation TestClass
#if NS_BLOCKS_AVAILABLE
- (id)initWithBlock:(void (^)(void))block {
if ((self = [super init])) {
blk = Block_copy(block);
}
return self;
}
- (void)exec {
if (blk) blk();
}
- (void)dealloc {
Block_release(blk);
[super dealloc];
}
#endif
@end
While a regular instantiation and passing a regular block works:
TestClass *test = [[TestClass alloc] initWithBlock:^{
NSLog(@"TestClass");
}];
[test exec];
[test release];
Using a block with reference to the object which is being created doesn't:
TestClass *test1 = [[TestClass alloc] initWithBlock:^{
NSLog(@"TestClass %@", test1);
}];
[test1 exec];
[test1 release];
Error is EXC_BAD_ACCESS, stack trace on Block_copy(block); Debugger on: 0x000023b2 <+0050> add $0x18,%esp
I kept playing around, and moved the allocation code above the initialization, it worked:
TestClass *test2 = [TestClass alloc];
test2 = [test2 initWithBlock:^{
NSLog(@"TestClass %@", test2);
}];
[test2 exec];
[test2 release];
And combining both snippets works too:
TestClass *test1 = [[TestClass alloc] initWithBlock:^{
NSLog(@"TestClass %@", test1);
}];
[test1 exec];
[test1 release];
TestClass *test2 = [TestClass alloc];
test2 = [test2 initWithBlock:^{
NSLog(@"TestClass %@", test2);
}];
[test2 exec];
[test2 release];
What's going on here?
Upvotes: 2
Views: 2030
Reputation: 31323
The problem is that when a block is created, it will copy (make a separate copy) of any non-__block
variables it captures. Since test1
is uninitialized at the time your block is created, you will be using an uninitialized pointer for test1
when you run the block.
The proper solution is to declare test1
with __block
. That way, state is shared between the block and the enclosing scope, and after test1
is assigned in the enclosing scope, the block can access the changed value:
__block TestClass *test1;
test1 = [[TestClass alloc] initWithBlock:^{
NSLog(@"TestClass %@", test1);
}];
[test1 exec];
[test1 release];
p.s. The 3rd example (doing alloc
before and then assigning the result of init
) is not reliable because in general an object's init
method is not guaranteed to return the object it is called on (init
is allowed to deallocate itself and return nil if it failed, for example).
Update: The above code is only for MRC, as __block
variables are not retained by the block.
But in ARC, the above code will cause a retain cycle, as __block
object pointer variables are by default retained by the block. In ARC, the correct code is:
TestClass *test1;
__block __weak TestClass *weakTest1;
weakTest1 = test1 = [[TestClass alloc] initWithBlock:^{
NSLog(@"TestClass %@", weakTest1);
}];
[test1 exec];
Upvotes: 1
Reputation:
In an assignment expression, the rvalue is evaluated before being assigned to the lvalue.
This means that in:
TestClass *test1 = [[TestClass alloc] initWithBlock:^{
NSLog(@"TestClass %@", test1);
}];
the following sequence of operations is performed. Edit: as pointed out by Jonathan Grynspan, there’s no defined order for steps 1 and 2 so it could be the case that step 2 is executed before step 1.
+alloc
to TestClass
test1
, which hasn’t been initialised yet. test1
contains an arbitrary memory address.-initWithBlock:
to the object created in step 1.test1
.Note that test1
points to a valid object only after step 4.
In:
TestClass *test2 = [TestClass alloc];
test2 = [test2 initWithBlock:^{
NSLog(@"TestClass %@", test2);
}];
[test2 exec];
[test2 release];
the sequence is:
+alloc
to TestClass
test2
, which now points to a TestClass
object.test2
, which points to the TestClass
object per step 2.-initWithBlock:
to test2
, which was correctly assigned in step 2.test2
.Upvotes: 5