出題者簡介: 孫源(sunnyxx),目前就職於百度
整理者簡介:陳奕龍,目前就職於滴滴出行。
轉載者:豆電雨(starain)微信:doudianyu
若想令自己所寫的對象具有拷貝功能,則需實現 NSCopying 協議。如果自定義的對象分為可變版本與不可變版本,那么就要同時實現 NSCopying
與 NSMutableCopying
協議。
具體步驟:
- 需聲明該類遵從 NSCopying 協議
-
實現 NSCopying 協議。該協議只有一個方法:
- (id)copyWithZone:(NSZone *)zone;
注意:一提到讓自己的類用 copy 修飾符,我們總是想覆寫copy方法,其實真正需要實現的卻是 “copyWithZone” 方法。
以第一題的代碼為例:
// .h文件
// http://weibo.com/luohanchenyilong/ // https://github.com/ChenYilong // 修改完的代碼 typedef NS_ENUM(NSInteger, CYLSex) { CYLSexMan, CYLSexWoman }; @interface CYLUser : NSObject<NSCopying> @property (nonatomic, readonly, copy) NSString *name; @property (nonatomic, readonly, assign) NSUInteger age; @property (nonatomic, readonly, assign) CYLSex sex; - (instancetype)initWithName:(NSString *)name age:(NSUInteger)age sex:(CYLSex)sex; + (instancetype)userWithName:(NSString *)name age:(NSUInteger)age sex:(CYLSex)sex; @end
然后實現協議中規定的方法:
- (id)copyWithZone:(NSZone *)zone { CYLUser *copy = [[[self class] allocWithZone:zone] initWithName:_name age:_age sex:_sex]; return copy; }
但在實際的項目中,不可能這么簡單,遇到更復雜一點,比如類對象中的數據結構可能並未在初始化方法中設置好,需要另行設置。舉個例子,假如 CYLUser 中含有一個數組,與其他 CYLUser 對象建立或解除朋友關系的那些方法都需要操作這個數組。那么在這種情況下,你得把這個包含朋友對象的數組也一並拷貝過來。下面列出了實現此功能所需的全部代碼:
// .h文件
// http://weibo.com/luohanchenyilong/ // https://github.com/ChenYilong // 以第一題《風格糾錯題》里的代碼為例 typedef NS_ENUM(NSInteger, CYLSex) { CYLSexMan, CYLSexWoman }; @interface CYLUser : NSObject<NSCopying> @property (nonatomic, readonly, copy) NSString *name; @property (nonatomic, readonly, assign) NSUInteger age; @property (nonatomic, readonly, assign) CYLSex sex; - (instancetype)initWithName:(NSString *)name age:(NSUInteger)age sex:(CYLSex)sex; + (instancetype)userWithName:(NSString *)name age:(NSUInteger)age sex:(CYLSex)sex; - (void)addFriend:(CYLUser *)user; - (void)removeFriend:(CYLUser *)user; @end
// .m文件
// .m文件
// http://weibo.com/luohanchenyilong/ // https://github.com/ChenYilong // @implementation CYLUser { NSMutableSet *_friends; } - (void)setName:(NSString *)name { _name = [name copy]; } - (instancetype)initWithName:(NSString *)name age:(NSUInteger)age sex:(CYLSex)sex { if(self = [super init]) { _name = [name copy]; _age = age; _sex = sex; _friends = [[NSMutableSet alloc] init]; } return self; } - (void)addFriend:(CYLUser *)user { [_friends addObject:user]; } - (void)removeFriend:(CYLUser *)user { [_friends removeObject:user]; } - (id)copyWithZone:(NSZone *)zone { CYLUser *copy = [[[self class] allocWithZone:zone] initWithName:_name age:_age sex:_sex]; copy->_friends = [_friends mutableCopy]; return copy; } - (id)deepCopy { CYLUser *copy = [[[self class] allocWithZone:zone] initWithName:_name age:_age sex:_sex]; copy->_friends = [[NSMutableSet alloc] initWithSet:_friends copyItems:YES]; return copy; } @end
以上做法能滿足基本的需求,但是也有缺陷:
如果你所寫的對象需要深拷貝,那么可考慮新增一個專門執行深拷貝的方法。
【注:深淺拷貝的概念,在下文中有介紹,詳見下文的:用@property聲明的 NSString(或NSArray,NSDictionary)經常使用 copy 關鍵字,為什么?如果改用 strong 關鍵字,可能造成什么問題?】
在例子中,存放朋友對象的 set 是用 “copyWithZone:” 方法來拷貝的,這種淺拷貝方式不會逐個復制 set 中的元素。若需要深拷貝的話,則可像下面這樣,編寫一個專供深拷貝所用的方法:
- (id)deepCopy {
CYLUser *copy = [[[self class] allocWithZone:zone] initWithName:_name age:_age sex:_sex]; copy->_friends = [[NSMutableSet alloc] initWithSet:_friends copyItems:YES]; return copy; }
至於如何重寫帶 copy 關鍵字的 setter這個問題,
如果拋開本例來回答的話,如下:
- (void)setName:(NSString *)name { //[_name release]; _name = [name copy]; }
不過也有爭議,有人說“蘋果如果像下面這樣干,是不是效率會高一些?”
- (void)setName:(NSString *)name { if (_name != name) { //[_name release];//MRC _name = [name copy]; } }
這樣真得高效嗎?不見得!這種寫法“看上去很美、很合理”,但在實際開發中,它更像下圖里的做法:
克強總理這樣評價你的代碼風格:
我和總理的意見基本一致:
老百姓 copy 一下,咋就這么難?
你可能會說:
之所以在這里做if判斷
這個操作:是因為一個 if 可能避免一個耗時的copy,還是很划算的。 (在剛剛講的:《如何讓自己的類用 copy 修飾符?》里的那種復雜的copy,我們可以稱之為 “耗時的copy”,但是對 NSString 的 copy 還稱不上。)
但是你有沒有考慮過代價:
你每次調用
setX:
都會做 if 判斷,這會讓setX:
變慢,如果你在setX:
寫了一串復雜的if+elseif+elseif+...
判斷,將會更慢。
要回答“哪個效率會高一些?”這個問題,不能脫離實際開發,就算 copy 操作十分耗時,if 判斷也不見得一定會更快,除非你把一個“ @property他當前的值 ”賦給了他自己,代碼看起來就像:
[a setX:x1];
[a setX:x1]; //你確定你要這么干?與其在setter中判斷,為什么不把代碼寫好?
或者
[a setX:[a x]]; //隊友咆哮道:你在干嘛?!!
不要在 setter 里進行像
if(_obj != newObj)
這樣的判斷。(該觀點參考鏈接: How To Write Cocoa Object Setters: Principle 3: Only Optimize After You Measure )
什么情況會在 copy setter 里做 if 判斷? 例如,車速可能就有最高速的限制,車速也不可能出現負值,如果車子的最高速為300,則 setter 的方法就要改寫成這樣:
-(void)setSpeed:(int)_speed{ if(_speed < 0) speed = 0; if(_speed > 300) speed = 300; _speed = speed; }
回到這個題目,如果單單就上文的代碼而言,我們不需要也不能重寫 name 的 setter :由於是 name 是只讀屬性,所以編譯器不會為其創建對應的“設置方法”,用初始化方法設置好屬性值之后,就不能再改變了。( 在本例中,之所以還要聲明屬性的“內存管理語義”--copy,是因為:如果不寫 copy,該類的調用者就不知道初始化方法里會拷貝這些屬性,他們有可能會在調用初始化方法之前自行拷貝屬性值。這種操作多余而低效)。
那如何確保 name 被 copy?在初始化方法(initializer)中做:
- (instancetype)initWithName:(NSString *)name age:(NSUInteger)age sex:(CYLSex)sex { if(self = [super init]) { _name = [name copy]; _age = age; _sex = sex; _friends = [[NSMutableSet alloc] init]; } return self; }