Home > other >  NSArray as API property, NSMutableArray as implementation
NSArray as API property, NSMutableArray as implementation

Time:11-13

I have a class with the following property exposed in the .h file:

@property (readonly, nonnull)  NSArray<String *> * routeContext;

As you can see this is a NSArray which is not mutable. In the implementation though I want to be able to work with this array as a mutable one (NSMutableArray) so it will be easy to add, remove objects from it. What is the best approach to do it?

I was thinking about holder a NSMutableArray in the m file which backs the read only NSArray but it seems kinda dirty to me, is there any other suggestions? The reason I don't want to set the property to NSMutableArray although its readonly is that readonly doesn't really make sense with NSMutableArray.

Thanks.

CodePudding user response:

I would add a read-write NSMutableArray property to a class extension in the .m file. Then implement the read-only property as a method that returns a copy of the mutable property.

In the .m file:

@interface SomeClass()

@property (nonatomic, strong) NSMutableArray<NSString *> *myRouteContext;

@end

@implementation SomeClass

- (NSArray<NSString *> *)routeContext {
    return [myRouteContext copy];
}

@end

In all of your implementation code you use myRouteContext. Only clients of the class use routeContext for the read-only version.

I wouldn't call this "dirty" in any way. There's still only one backing instance variable implicitly created by the myRouteContext property. No instance variables are created for the read-only routeContext property. The @property for that one is just syntactic sugar for the routeContext method you implement.

CodePudding user response:

In addition to the approach suggested by HangarRash i would consider two other options:

  1. Extending routeContext property itself in the class extension:
@interface TDWObject ()

@property (copy, nonatomic, nonnull) NSMutableArray<NSString *> *routeContext;

@end
  1. Just introducing ivar in the class extension for the property manually (and accessing it directly in the implementation):
@interface TDWObject () {
    NSMutableArray<NSString *> *_routeContext;
}

@end

Personally I would prefer the manual ivar due to the following reasons:

  • It doesn't introduce any redundant methods that clang would synthesise otherwise (you neither need extra getter, nor setter for a NSMutableArray *)
  • It's the most performant (accessing ivar directly).

I would also recommend to alter the property attributes as follows:

// The header file
@interface TDWObject : NSObject

@property (copy, readonly, nonatomic, nonnull) NSArray<NSString *> *routeContext;

@end

Here a couple of clarifications regarding the properties choice:

  • copy storage - technically for a readonly property storage attrbitue should not make much difference, because it predominantly denotes setter semantic (if we don't count that the value for this property can also be passed as a constructor argument). However, in our case the getter is custom and returns a copy of the internal object (instead of just reference to it). If you look through Cocoa/Cocoa Touch API, they often use copy attribute when they want to explicitly say that you deal with copies of internal data structure and any changes made to the instance obtained from the property wouldn't be tracked by the owning object. (e.g. -[NSCharacterSet invertedSet]), thus it's an important part of the interface description. Why the copy is needed at all? because otherwise the client code can easily exploit the mutability of the original data, and manage its content itself.
  • nonatomic atomicity - first, I don't know if there is a reason to make the property atomic, and you commonly use nonatomic properties by default (because they don't have burden of synchronisation, which slows down access/read performance). Second - clang would not be able to pair a synthesized setter with a user defined getter (if you choose to use properties approaches instead of ivar). Last, but not least - since getter is user-defined, you will have to manage the synchronisation yourself, so it doesn't come "for free".

Finally, the implementation part would look like this:

@implementation TDWObject

#pragma mark Lifecycle

- (instancetype)init {
    if (self = [super init]) {
        _routeContext = [NSMutableArray array];
    }
    return self;
}

#pragma mark Actions

- (NSArray<NSString *> *)routeContext {
    return [_routeContext copy];
}

- (void)addFoo {
    [_routeContext addObject:@"Foo"];
}

@end
  • Related