Immutable Object in Objective-C: Big init method?

后端 未结 2 1237
予麋鹿
予麋鹿 2021-02-10 20:07

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 Obj

相关标签:
2条回答
  • 2021-02-10 20:58

    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.

    0 讨论(0)
  • 2021-02-10 21:03

    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.

    0 讨论(0)
提交回复
热议问题