objective-cobjective-c-blocksobjective-c-runtime

Objective-C: Why two requests of [object copy] return the same result?


I would expect copy on an object to generate a new object. But it seems it will only generate a different one and may reuse it for another copy.

void (^block1)(void) = ^ {
    // ...
};
void (^block2)(void) = ^ {
    // ...
};
typeof(block1) block3 = [block1 copy];
typeof(block1) block4 = [block1 copy];
typeof(block2) block5 = [block2 copy];

XCTAssertNotEqual(block1, block2);
XCTAssertNotEqual(block2, block3);
XCTAssertNotEqual(block3, block4); // Fail, [block1 copy] returns the same result for two requests
XCTAssertNotEqual(block4, block5);

But why does it act like this?


Solution

  • -copy and -copyWithZone: are described by the NSCopying protocol, which says the following:

    The exact meaning of “copy” can vary from class to class, but a copy must be a functionally independent object with values identical to the original at the time the copy was made. A copy produced with NSCopying is implicitly retained by the sender, who is responsible for releasing it.

    The object returned by -copy/-copyWithZone: can be any object which is considered equal to the original object, while being functionally independent. Critically, this means that immutable objects are allowed to implement -copy by returning self, since they cannot be modified before or after the copy, which means that two instances with the same value would be indistinguishable (allowing them to skip making an actual copy, since it would be needless work).

    This is a pretty common optimization, and plenty of types implement it, including NSString (esp. with constant strings), certain collections like NSArray and NSDictionary, and others:

    void printEqual(id obj) {
        id copy = [obj copy];
        NSLog(@"%p == %p: %d", obj, copy, obj == copy);
    }
    
    printEqual(@"Hello, world!"); // => 0x1046d8048 == 0x1046d8048: 1
    printEqual(@[@1, @2, @3]); // => 0x1046dc078 == 0x1046dc078: 1
    printEqual(@{@"greeting": @"Hello, world!"}); // => 0x1046dc090 == 0x1046dc090: 1
    

    In general, you should not rely on -copy returning a different object from the original if the type is immutable, since it can include this optimization.

    (Immutable copies of mutable objects, and mutable copies in general, cannot behave this way, because mutations of either the original or the new object are not allowed to change one another; those copies are truly distinct.)


    In your specific case: the blocks you declared are immutable objects, and are allowed to return self from -copy, since they cannot be changed, and there's no reason to perform a full copy of the block.

    It's not just that block3 == block4, but that block1 == block3 == block4, since -copy is returning the original block:

    NSLog(@"%p == %p: %d", block1, block3, block1 == block3); // => 0x600001dc0b70 == 0x600001dc0b70: 1
    NSLog(@"%p == %p: %d", block1, block4, block1 == block4); // => 0x600001dc0b70 == 0x600001dc0b70: 1
    

    This is not true for all types of blocks; if you're curious, this SO answer about "global" blocks, "stack" blocks, and "malloc" blocks also describes copying behavior: https://stackoverflow.com/a/29160233/169394