I'm wondering what the correct pattern for implementing Mutable vs Immutable data structures would be. I understand the concept and how it works, but how should I implement if using an underlying Cocoa data structure? I mean, if I use a NSSet
, for instance. Lets say I have the following:
// MyDataStructure.h
@interface MyDataStructure : NSObject
@property (nonatomic, strong, readonly) NSSet * mySet;
@end
// MyDataStructure.m
@interface MyDataStructure ()
@property (nonatomic, strong) NSMutableSet * myMutableSet;
@end
@implementation MyDataStructure
- (NSSet *)mySet
{
return [_myMutableSet copy];
}
@end
The only reason I'm using a mutable set as the underlying data structure,is so that the mutable version of this class can tamper with it. MyDataStructure
per se does not really need a mutable set. Therefore, assuming that I have implemented some initialisers to make this class useful, here's how MyMutableDataStructure
looks like:
// MyDataStructure.h (same .h as before)
@interface MyMutableDataStructure : MyDataStructure
- (void)addObject:(id)object;
@end
// MyDataStructure.m (same .m as before)
@implementation MyMutableDataStructure
- (void)addObject:(id)object
{
[self.myMutableSet addObject:object];
}
@end
By using this pattern the underlying data structure is always mutable, and its immutable version is just an immutable copy (or is it??).
This also begs another question that arises when implementing the NSCopying
protocol. Here's a sample implementation:
- (id)copyWithZone:(NSZone *)zone
{
MyDataStructure * copy = [MyDataStructure allocWithZone:zone];
copy->_myMutableSet = [_myMutableSet copyWithZone:zone];
return copy;
}
Doesn't copyWithZone:
return an immutable copy if that applies? So I'm basically assigning a NSSet
instead to a NSMutableSet
property, isn't that right?
Edit: While diving deeper into the issue I found some more issues surrounding this concern.
mySet
should be copy
instead of strong
.copyWithZone:
implementation isn't right either. I didn't mention it in the first post but that implementation relates to the Immutable version of the data structure (MyDataStructure
). As I've read, Immutable data structures don't actually create a copy, they just return themselves. That makes sense.copyWithZone:
in the Mutable version (MyMutableDataStructure
).To make things clear:
// MyDataStructure.h
@property (nonatomic, copy, readonly) NSSet * mySet;
And
// MyDataStructure.m
@implementation MyDataStructure
- (id)copyWithZone:(NSZone *)zone
{
// We don't really need a copy, it's Immutable
return self;
}
- (id)mutableCopyWithZone:(NSZone *)zone
{
// I also implement -mutableCopyWithZone:, in which case an actual (mutable) copy is returned
MyDataStructure * copy = [MyMutableDataStructure allocWithZone:zone];
copy-> _myMutableSet = [_myMutableSet mutableCopyWithZone:zone];
return copy;
}
@end
@implementation MyMutableDataStructure
- (id)copyWithZone:(NSZone *)zone
{
return [self mutableCopyWithZone:zone];
}
@end
It seems tricky at first, but I think I'm getting the hang of it. So the remaining questions are:
mySet
return a mutable or immutable instance?copy
signal when using the copy
property attribute?I appreciate your patience to read this far. Best.
Apple is the way
All across Apple's libraries the pattern that is used is, the mutable version of the class can be created through -mutableCopy
, or (let's say the class is called NSSomething
), then it can be created through the methods -initWithSomething:(NSSomething*)something
or +somethingWithSomething:(NSSomething*)something
. NSMutableSomething
always inherits from NSSomething
and so the constructor methods are the same. (i.e., +[NSArray arrayWithArray:]
and +[NSMutableArray arrayWithArray:]
return their respective instance types, also you pass in a mutable object to make an immutable copy, aka [NSArray arrayWithArray:someNSMutableArrayObject]
)
So this is how I would do it:
Interface
MyDataStructure.h
// MyDataStructure.h
@interface MyDataStructure : NSObject
@property (nonatomic, strong) NSSet * mySet;
+ (instancetype)dataStructureWithDataStructure:(MyDataStructure*)dataStructure;
- (instancetype)initWithDataStructure:(MyDataStructure*)dataStructure;
@end
MyMutableDataStructure.h
// MyMutableDataStructure.h
#import "MyDataStructure.h"
@interface MyMutableDataStructure : MyDataStructure
@property (nonatomic, strong) NSMutableSet * mySet; // Only needs to redefine this property. The instantiation methods will be borrowed from the immutable class.
@end;
Implementation
MyDataStructure.m
@implementation MyDataStructure
+ (instancetype)dataStructureWithDataStructure:(MyDataStructure *)dataStructure {
return [[self alloc] initWithDataStructure:dataStructure];
}
- (instancetype)initWithDataStructure:(MyDataStructure *)dataStructure {
self = [super init];
if (self) {
self.mySet = [NSSet setWithSet:dataStructure.mySet];
}
return self;
}
- (instancetype)mutableCopy {
return [MyMutableDataStructure dataStructureWithDataStructure:self];
}
@end
MyMutableDataStructure.m
@implementation MyMutableDataStructure
- (instancetype)initWithDataStructure:(MyDataStructure *)dataStructure {
self = [super init];
if (self) {
self.mySet = [NSMutableSet setWithSet:dataStructure.mySet];
}
return self;
}
@end
By using this pattern the underlying data structure is always mutable, and its immutable version is just an immutable copy (or is it??).
No. To cut down on memory footprint, immutable objects do not need the same overhead as their mutable counterparts.
copyWithZone
Just make sure you use [self class]
so MyMutableDataStructure
can inherit this method and return its own type, and also don't forget the call to -init
after +allocWithZone:
__typeof(self)
just declares the variable "copy
" as whatever type self
is, so it's completely inheritable by the mutable subclass.
- (id)copyWithZone:(NSZone *)zone
{
__typeof(self) copy = [[[self class] allocWithZone:zone] init]; // don't forget init!
copy.mySet = [self.mySet copyWithZone:zone];
return copy;
}
^ That method goes into the implementation of MyDataStructure
In your original implementation,
// We don't really need a copy, it's Immutable
While this may be true for your project, it's an abuse of the naming convention. A method that starts with -copy
should return a copy of the object.
Going a little off topic:
I want to touch on some things I saw in your original question...The first is about hiding a "mutable object" with an "immutable object" pointer reference. Maybe you need this functionality too, so here is a more robust way to do it and why.
MyClass.h (notice no ownership attributes - because it's only really an alias - aka we only need this for thy synthesis of setters and getters)
@property (nonatomic) NSSet *mySet;
MyClass.m
@interface MyClass () {
NSMutableSet *_myMutableSet;
}
@implementation MyClass
// returns a copy of the internal mutable set as an NSSet
- (NSSet*)mySet {
return [NSSet setWithSet:_myMutableSet];
}
// setter saves the internal mutable set as a copy of whatever set you hand it
- (NSSet*)setMySet:(NSSet*)mySet {
_myMutableSet = [NSMutableSet setWithSet:mySet];
}
I defined _myMutableSet
as an ivar, to protect the getters and setters further. In your original code you put @property (...) myMutableSet
in an interface extension in the .m file. This synthesizes getters and setters automatically, so even if the declaration is apparently "private", one could call [myDataStructure performSelector:@selector(setMutableSet:) withObject:someMutableSet];
and it will work despite the "Undeclared selector" compiler warning.
Also, in your original implementation of -mySet
:
- (NSSet *)mySet {
return [_myMutableSet copy];
}
This returns a copy of a pointer to _myMutableSet
, type-casted as an NSSet*
. Therefore if someone re-casted it as NSMutableSet*
they could alter the underlying mutable set.