views:

33

answers:

2

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?

+1  A: 

You can make something immutable with the @property keyword; simply make use readonly instead of copy or retain (copy and retain describe the desired behavior on assignment, and so they would not make sense for a property that is readonly). Note that this will result in a getter function being generated when you use @synthesize, but not a setter function. If your data is truly immutable, then it would not make sense to have a setter function, although you could easily create one in the same way that you would create any other method in Objective-C. As for retain vs. copy -- it depends on whether you want to share the object with the caller or whether you want to have a completely independent copy of it. Using retain is more efficient, but it also is a little bit subtle and scary in that your object might possibly be modified from somewhere else through this shared object. It really depends on the guarantees that your class makes and whether such an external modification is ok.

Michael Aaron Safyan
Thanks. As for the setter: I need a way to initialize the variable. So a big "initWithTenParameters" method? Thanks for the retain/copy clarification!
Michael Stum
You can still have a private setter, i.e. declare a private category (very common). Just make sure you don't change data unexpectedly on something that should be immutable.
Eiko
@Eiko, yes, you can have a private setter, but not using the same public property declaration. I was trying to get at that when I said that one could easily create a setter without using @synthesize, although your statement does a better job of articulating that.
Michael Aaron Safyan
A: 

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.

Chris Hanson