I want to have an Object with immutable fields in Objective-C.
In C#, I would use Properties with private setters and a big constructor.
What would I use in Objective-C?
Using @property doesn't seem to allow me to declare the setter as private.
Using
initWithData: (NSString*) something createDate: (NSDate*) date userID: (long) uid
seems overly verbose if I have more than 4 properties to set.
Would I declare the getters in the .h file and the setters only in .m?
I need to use retain or copy on something and date (by the way: which of these two should I use?), so I need some code in the setter.
Or is there even something else like an immutable keyword?
You can have a public read-only property, and use a private read-write property to provide a setter for the property within your class if you really need one. However, you should consider whether it's even necessary.
As an example, consider the following declaration and definition of an immutable Person class:
// Person.h
#import <Foundation/Foundation.h>
@interface Person : NSObject {
@private
NSString *name_;
NSDate *dateOfBirth_;
}
@property (readonly, copy) NSString *name;
@property (readonly, copy) NSDate *dateOfBirth;
/*! Initializes a Person with copies of the given name and date of birth. */
- (id)initWithName:(NSString *)name dateOfBirth:(NSDate *)dateOfBirth;
@end
// Person.m
#import "Person.h"
@implementation Person
@synthesize name = name_;
@synthesize dateOfBirth = dateOfBirth_;
- (id)initWithName:(NSString *)name dateOfBirth:(NSDate *)dateOfBirth {
self = [super init];
if (self) {
name_ = [name copy];
dateOfBirth_ = [dateOfBirth copy];
}
return self;
}
- (void)dealloc {
[name_ release];
[dateOfBirth_ release];
[super dealloc];
}
@end
First, notice that I did not declare a class extension in Person.m
that redeclares the name
and dateOfBirth
properties as readwrite
. This is because the purpose of the class is to be immutable; there's no need to have setters if the instance variables are only ever going to be set at initialization time.
Also notice that I declared the instance variables with different names than the properties. This makes clear the distinction between properties as a programmatic interface to the class, and instance variables as an implementation detail of the class. I've seen far too many developers (especially those new to Mac OS X and iOS, including many coming from C#) conflate properties with the instance variables that may be used to implement them.
A third thing to notice is that I declared both of these properties as copy
even though they're read-only. There are two reasons. The first is that while direct instances of this class are immutable, there's nothing preventing the creation of a MutablePerson subclass. In fact, this might even be desirable! So the copy
specifies clearly what the expectations of the superclass are - that the values of the name
and dateOfBirth
properties themselves won't change. It also hints that -initWithName:dateOfBirth:
probably copies as well; its documentation comment should make that clear. Secondly, both NSString and NSDate are value classes; copies of immutable ones should be inexpensive, and you don't want to hang onto an instance of a mutable subclass that will change out from under your own class. (Now there's not actually any mutable subclass of NSDate, but that doesn't mean someone couldn't create their own...)
Finally, don't worry about whether your designated initializer is verbose. If an instance of your object is not valid unless it's in some particular state, then your designated initializer needs to put it in that state -- and it needs to take the appropriate parameters to do so.
There's one more thing: If you're creating an immutable value class like this, you should probably also implement your own -isEqual:
and -hash
methods for fast comparison, and probably conform to NSCopying as well. For example:
@interface Person (ImmutableValueClass) <NSCopying>
@end
@implementation Person (ImmutableValueClass)
- (NSUInteger)hash {
return [name_ hash];
}
- (BOOL)isEqual:(id)other {
Person *otherPerson = other;
// Using [super isEqual:] to allow easier reparenting
// -[NSObject isEqual:] is documented as just doing pointer comparison
return ([super isEqual:otherPerson]
|| ([object isKindOfClass:[self class]]
&& [self.name isEqual:otherPerson.name]
&& [self.dateOfBirth isEqual:otherPerson.dateOfBirth]));
}
- (id)copyWithZone:(NSZone *)zone {
return [self retain];
}
@end
I declared this in its own category so as to not repeat all of the code I previously showed as an example, but in real code I would probably put all of this in the main @interface
and @implementation
. Note that I didn't redeclare -hash
and -isEqual:
, I only defined them, because they're already declared by NSObject. And that because this is an immutable value class, I can implement -copyWithZone:
purely by retaining self
, I don't need to make a physical copy of the object because it should behave exactly the same.
If you're using Core Data, however, don't do this; Core Data implements object uniquing for you, so you must not have your own -hash
or -isEqual:
implementation. And for good measure you shouldn't really conform to NSCopying in Core Data NSManagedObject subclasses either; what it means to "copy" objects that are part of a Core Data object graph requires careful thought, and is generally more of a controller-level behavior.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With