objective-cxcodememory-managementhierarchynscopying

Implementing NSCopying in Subclass of Subclass


I have a small class hierarchy that I'm having trouble implementing copyWithZone: for. I've read the NSCopying documentation, and I can't find the correct answer.

Take two classes: Shape and Square. Square is defined as:

@interface Square : Shape

No surprise there. Each class has one property, Shape has a "sides" int, and Square has a "width" int. The copyWithZone: methods are seen below:

Shape

- (id)copyWithZone:(NSZone *)zone {
    Shape *s = [[Shape alloc] init];
    s.sides = self.sides;
    return s;
}

Square

- (id)copyWithZone:(NSZone *)zone {
    Square *s = (Square *)[super copyWithZone:zone];
    s.width = self.width;
    return s;
}

Looking at the documentation, this seems to be the "right" way to do things.

It is not.

If you were to try to set/access the width property of a Square returned by the copyWithZone: method, it would fail with an error similar to the one below:

2010-12-17 11:55:35.441 Hierarchy[22617:a0f] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[Shape setWidth:]: unrecognized selector sent to instance 0x10010c970'

Calling [super copyWithZone:zone]; in the Square method actually returns a Shape. It's a miracle you're even allowed to set the width property in that method.

That having been said, how does one implement NSCopying for subclasses in a way that does not make them responsible for copying the variables of its superclass?


Solution

  • One of those things you realize right after asking...

    The implementation of copyWithZone: in the superclass (Shape) shouldn't be assuming it's a Shape. So instead of the wrong way, as I mentioned above:

    - (id)copyWithZone:(NSZone *)zone {
        Shape *s = [[Shape allocWithZone:zone] init];
        s.sides = self.sides;
        return s;
    }
    

    You should instead use:

    - (id)copyWithZone:(NSZone *)zone {
        Shape *s = [[[self class] allocWithZone:zone] init]; // <-- NOTE CHANGE
        s.sides = self.sides;
        return s;
    }