Objective-C API 中只读 属性 值的安全突变和使用

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

考虑像 const T* foo() 这样的 C++ API。这清楚地记录了 API 支持的可变性和使用:好的,我们会让你看看 T,但请不要更改它。你 可以 仍然改变它,但是你必须明确地使用 const_cast 来表明你不打算遵循 API.

Objective-C API 的很大一部分由 属性 声明组成。 API 的用户应该如何解释 : @property (readonly) T foo ? (假设 T 不是不可变类型)

注意:我不是在询问语言规范。我想问的是 Objective-C 社区对 API 的传统解释是什么。

matt said 一样,您拥有指向 object 的指针这一事实并不意味着 object 本身是可变的。 Objective-C 使用 class 的行为,而不是指针,来强制实现不变性。所以一般来说,您应该看到 read-only 属性 return,例如 NSString 而不是 NSMutableString.

我查看了 Apple 的 iOS 框架 headers 以验证这一点:

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

(Cocoa 中 class 名称的模式是调用 class $PREFIXMutable$CLASSNAME 的可变 "version":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;

只有七个结果,NSExpression 中的那个不算数,因为搜索找到的 "Mutable" 是 Block 的参数,实际上是 属性 的值。

对于其他人,我想您会发现相应的 class 参考文档会告诉您可以使用这些值做什么和不能做什么。

例如,documentation for threadDictionary 是这样说的:

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

可变字典 return 精确编辑,因此您 可以 改变它。然而,线程 object 不允许您设置它,因此它也可以在那里存储东西。

NSAttributedString.h中的命中实际上是在NSMutableAttributedStringclass中,而those docs note

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

由于 NSAttributedString 非常明确*只是一个 NSString 打包了一堆属性,class 的设计直接暴露了包装的字符串;可变版本紧随其后。

UIButton 在评论中被提及,因为那里有一个 read-only 标签,其自身的属性是可修改的。还有,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.

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

总而言之,Objective-C 无法在语言级别创建或实施可变性限制。正如您所指出的,标记为 readonly 的 属性 只是意味着您无法将值设置为其他值。** 并且没有等价于 const_cast 的值可变的,这样你就可以改变它:你最终会得到一个供应商object一无所知的新值。

然后,Cocoa 约定是通过使用不可变的 classes 来二次强制执行 属性 的状态。 (在某些情况下,您甚至可能获得 class 内部保留为可变的数据的不可变副本。)如果 API 给您一个可变的 object,您可以假设您可能改变它,但是文档应该告诉你如何使用它


*其 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."

**有 KVC,但这又是在框架级别,框架约定表明您这样做是自找麻烦。