Search code examples
objective-ccocoacocoa-touchconventiondeclared-property

Safe mutation and usage of readonly property value in Objective-C APIs


Consider a C++ API like const T* foo(). This clearly documents the supported mutability and use of the API: OK, we'll let you look at T, but please don't change it. You can still mutate it, but you have to explicitly use const_cast to indicate your intention to not follow the API.

A good portion of Objective-C API's are comprised of property declarations. How is a user of an API supposed to interpret: @property (readonly) T foo ? (Assume T isn't an immutable type)

  • Since the setter isn't synthesized, clearly foo isn't mean to be replaced.
  • However, the getter still gives me a pointer to foo. Is it safe to mutate foo? (Clearly I can)

NOTE: I'm not asking about the language specs. I'm asking about what the conventional interpretation of an API like this is within the Objective-C community.


Solution

  • As matt said, the fact that you've got a pointer to the object does not mean that the object itself is mutable. Objective-C uses the behavior of the class, not the pointer, to enforce immutability. So in general you should be seeing read-only properties that return, e.g., NSString rather than NSMutableString.

    I took a look through Apple's iOS framework headers to verify this:

    grep -rn "@property.*readonly.*Mutable" /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk/*.h
    

    (The pattern for class names in Cocoa is to call the mutable "version" of the class $PREFIXMutable$CLASSNAME: NSString/NSMutableString, NSDictionary/NSMutableDictionary.)

    ./System/Library/Frameworks/AVFoundation.framework/Headers/AVComposition.h:133:@property (nonatomic, readonly) NSArray<AVMutableCompositionTrack *> *tracks; ./System/Library/Frameworks/CoreData.framework/Headers/NSManagedObjectContext.h:149:@property (nonatomic, readonly, strong) NSMutableDictionary *userInfo NS_AVAILABLE(10_7, 5_0); ./System/Library/Frameworks/Foundation.framework/Headers/NSAttributedString.h:54:@property (readonly, retain) NSMutableString *mutableString; ./System/Library/Frameworks/Foundation.framework/Headers/NSExpression.h:127:@property (readonly, copy) id (^expressionBlock)(id __nullable, NSArray *, NSMutableDictionary * __nullable) NS_AVAILABLE(10_6, 4_0); ./System/Library/Frameworks/Foundation.framework/Headers/NSThread.h:24:@property (readonly, retain) NSMutableDictionary *threadDictionary; ./System/Library/Frameworks/GameplayKit.framework/Headers/GKRuleSystem.h:54:@property (nonatomic, retain, readonly) NSMutableDictionary *state; ./System/Library/Frameworks/ModelIO.framework/Headers/MDLMesh.h:137:@property (nonatomic, readonly, retain) NSMutableArray *submeshes;

    Only seven results, and the one in NSExpression doesn't count because the "Mutable" that the search found is an argument to the Block that is actually the property's value.

    For the others, I think you'll find that the appropriate class reference doc tells you what you can and can't do with the values.

    For example, the documentation for threadDictionary has this to say:

    You can use the returned dictionary to store thread-specific data.[...]You may define your own keys for the dictionary.

    A mutable dictionary is returned precisely so that you can mutate it. The thread object doesn't let you set it, however, so that it can also store things there.

    The hit in NSAttributedString.h is actually in the NSMutableAttributedString class, and those docs note:

    The receiver tracks changes to this string and keeps its attribute mappings up to date.

    Since NSAttributedString is pretty explicitly* just an NSString packaged up with a bunch of attributes, the design of the class exposes the wrapped string directly; the mutable version follows suit.

    UIButton was mentioned in the comments, because there you have a read-only label whose own properties are modifiable. And there again, the docs are explicit:

    Although this property is read-only, its own properties are read/write. Use these properties primarily to configure the text of the button.

    and

    Do not use the label object to set the text color or the shadow color.

    In summary, there's no way in Objective-C at the language level to create or enforce mutability restrictions. As you've noted, a property marked readonly simply means there's no way for you to set the value to something else.** And there's no equivalent of const_casting the value to be mutable so that you can change it: you will end up with a new value that the vendor object knows nothing about.

    The Cocoa convention, then, is to secondarily enforce the property's status by using immutable classes. (In some cases you might even be getting an immutable copy of data that the class internally retains as mutable.) If the API gives you a mutable object, you can assume that you may mutate it, but the documentation should tell you exactly how you can use it.


    *Its class description says: "An NSAttributedString object manages character strings and associated sets of attributes (for example, font and kerning) that apply to individual characters or ranges of characters in the string."

    **There is KVC, but that's again at the framework level, and framework convention would indicate that you're asking for trouble doing that.