如何讓自己的類用 copy 修飾符?如何重寫帶 copy 關鍵字的 setter?


出題者簡介: 孫源(sunnyxx),目前就職於百度

整理者簡介:陳奕龍,目前就職於滴滴出行。

轉載者:豆電雨(starain)微信:doudianyu

 

 

若想令自己所寫的對象具有拷貝功能,則需實現 NSCopying 協議。如果自定義的對象分為可變版本與不可變版本,那么就要同時實現 NSCopying 與 NSMutableCopying 協議。

 

 

具體步驟:

  1. 需聲明該類遵從 NSCopying 協議
  2. 實現 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]; } }

這樣真得高效嗎?不見得!這種寫法“看上去很美、很合理”,但在實際開發中,它更像下圖里的做法:

enter image description here

克強總理這樣評價你的代碼風格:

enter image description here

我和總理的意見基本一致:

老百姓 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; } 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM