How to implement properly mutableCopyWithZone and copyWithZone
Asked Answered
W

1

7

I read others few topics about it, but still I'm lost.

I want to create 2 kind of objects, one immutable with only "readonly" properties, and one mutable with only "readwrite" properties.

Lets call them EXCar and EXMutableCar.

EXCar is subclass of NSObject, and EXMutableCar is Subclass of EXCar.

ExCar will have in its interface

@property (nonatomic, strong, readonly) NSString *name;

EXMutableCar will have in its interface

@property (nonatomic, strong) NSString *name;

So I "open" properties of EXCar when I use its subclasse EXMutableCar. And then it's mutable. The problem is to copy properly between them.

I implemented mutableCopyWithZone in EXCar :

- (id)mutableCopyWithZone:(NSZone *)zone {
    EXMutableCar *mutableCopy = [[EXMutableCar allocWithZone:zone] init];
    mutableCopy.name = _name;

    return mutableCopy;
}

First question, is it the good way to do it ? (I want swallow copy)

The problem is with copyWithZone. Since the properties of EXCar are readonly I cannot create neither in EXCar, neither in EXMutableCar a new instance of EXCar and fill its properties like this :

- (id)copyWithZone:(NSZone *)zone {
    EXCar *copy = [[EXCar allocWithZone:zone] init];
    copy.name = _name; // This can't work...

    return copy;
}

And I don't really want to do an "init" method with 15 properties to pass in (for sure, EXCar is an example, real classes are full of many properties). And normally they are initiated from JSON message from server, so they don't need a complicate init method.

Second question is so, how to do a copyWithZone that keep my class immutable ?

Thanks for your help :)

Wapiti answered 12/8, 2013 at 16:37 Comment(0)
S
9

Code:

// EXCar.h
#import <Foundation/Foundation.h>

@interface EXCar : NSObject <NSCopying, NSMutableCopying>

@property (nonatomic, strong, readonly) NSString* name;

@end

// EXCar.m
#import "EXCar.h"
#import "EXMutableCar.h"

@implementation EXCar

- (id)copyWithZone:(NSZone *)zone {
  EXCar* car = [[[self class] allocWithZone:zone] init];
  car->_name = [_name copyWithZone:zone];
  return car;
}

- (id)mutableCopyWithZone:(NSZone *)zone {
  EXMutableCar* mutableCar = [[EXMutableCar allocWithZone:zone] init];
  mutableCar.name = [_name mutableCopyWithZone:zone];
  return mutableCar;
}

@end

// EXMutableCar.h
#import "EXCar.h"

@interface EXMutableCar : EXCar

@property (nonatomic, strong) NSString* name;

@end

// EXMutableCar.m
#import "EXMutableCar.h"

@implementation EXMutableCar

@synthesize name = _mutableName;

- (id)copyWithZone:(NSZone *)zone {
  EXMutableCar* car = [super copyWithZone:zone];
  car->_mutableName = [_mutableName copyWithZone:zone];
  return car;
}

- (id)mutableCopyWithZone:(NSZone *)zone {
  EXMutableCar* car = [super mutableCopyWithZone:zone];
  car->_mutableName = [_mutableName mutableCopyWithZone:zone];
  return car;
}

Explanation:

  • EXCar interface implements both "copying" protocols;
  • The subclass EXMutableCar overrides the same property, making it readwrite.

First thing in EXMutableCar implementation: manually @synthesize name because Xcode gives us a warning, since we have the same property (but with different access specifier) in our superclass.

Note we could have given the same name to our instance variable, like _name, but it is important to understand that we declare in subclass a different variable, since _name from the superclass is inaccessible for us.

Next, Apple documentation states:

If a subclass inherits NSCopying from its superclass and declares additional instance variables, the subclass has to override copyWithZone: to properly handle its own instance variables, invoking the superclass’s implementation first.

same for NSMutableCopying:

If a subclass inherits NSMutableCopying from its superclass and declares additional instance variables, the subclass has to override mutableCopyWithZone: to properly handle its own instance variables, invoking the superclass’s implementation first.

We do declare additional instance variables, so we override these methods in our subclass as well.

Result:

EXCar* car = [[EXCar alloc]init]; // car.name is (null)
EXCar* carCopy = [car copy]; // we can do this
EXMutableCar* mutableCar = [car mutableCopy]; // and this
mutableCar.name = @"BMW";
car = [mutableCar copy]; // car.name is now @"BMW"
EXMutableCar* anotherMutableCar = [car mutableCopy]; //anotherMutableCar.name is @"BMW"
Shiver answered 7/8, 2014 at 15:57 Comment(1)
Copy on mutable subclass must return immutable version, e.g. EXCar, not EXMutableCar. This is especially important if classes implements custom equality when hash could be dependent on some mutable value (in mutable subclass), that's why, for example, NSDictionary copies its keys, it expects immutable objects back with immutable hash.Sanborne

© 2022 - 2024 — McMap. All rights reserved.