iOS之物件複製

蘇小妖發表於2017-03-11

前言

  NSObject類提供了copymutableCopy方法,通過這兩個方法即可複製已有物件的副本,本文將會詳細介紹關於物件複製的內容。

系統物件的copy與mutableCopy

  copy方法用於複製物件的副本。通常來說,copy方法總是返回物件的不可修改的副本,即使物件本身是可修改的。例如,程式呼叫NSMutableString的copy方法,將會返回不可修改的字串物件,
  mutableCopy方法用於複製物件的可變副本。通常來說,mutableCopy方法總是返回物件可修改的副本,即使被複制的物件本身是不可修改的,呼叫mutableCopy方法複製出來的副本也是可修改的。例如,程式呼叫NSString的mutableCopy方法,將會返回一個NSMutableString物件。
  下圖詳細闡述了NSString、NSMutableString、NSArray、NSMutableArray、NSDictionary、NSMutableDictionary分別呼叫copy與mutableCopy方法後的結果:

iOS之物件複製
系統物件複製

深複製與淺複製

  物件拷貝有兩種方式:淺複製深複製。顧名思義,淺複製,並不拷貝物件本身,僅僅是拷貝指向物件的指標;深複製是直接拷貝整個物件內容到另一塊記憶體中。再簡單些說:淺複製就是指標拷貝;深複製就是內容拷貝
  如果在多層陣列中,對第一層進行內容拷貝,其它層進行指標拷貝,這種情況是屬於深複製,還是淺複製?對此,蘋果官網文件有這樣一句話描述:

This kind of copy is only capable of producing a one-level-deep copy. If you only need a one-level-deep copy...
If you need a true deep copy, such as when you have an array of arrays...複製程式碼

  從文中可以看出,蘋果認為這種複製不是真正的深複製,而是將其稱為單層深複製(one-level-deep copy)。因此,有人對淺複製、完全深複製、單層深複製做了概念區分。當然,這些都是概念性的東西,沒有必要糾結於此。只要知道進行拷貝操作時,被拷貝的是指標還是內容即可。
  一般來說,完全深複製的實現難度大很多,尤其是當該物件包含大量的指標型別的例項變數時,如果某些例項變數裡再次包含指標型別的例項變數,那麼實現完全深複製會更加複雜。
  上面圖中的深複製(單層或者完全)就是因為集合物件中可能會包含指標型別的例項變數,從而導致深複製不完全。

自定義物件的複製

  使用copy和mutableCopy複製物件的副本使用起來確實方便,那麼我們自定義的類是否可呼叫copy與mutableCopy方法來複制副本呢?
  我們先定義一個SXYPerson類,程式碼如下:

@interface SXYPerson : NSObject

@property (nonatomic, assign) NSInteger age;

@property (nonatomic, copy) NSString *name;

@end複製程式碼

  然後嘗試呼叫SXYPerson的copy方法來複制一個副本:

SXYPerson *person1 = [[SXYPerson alloc] init];//建立一個SXYPerson物件
person1.age = 20;
person1.name = @"蘇小妖";
SXYPerson *person2 = [person1 copy];//複製副本複製程式碼

  執行程式,將會發生崩潰,並輸出以下錯誤資訊:

[SXYPerson copyWithZone:]: unrecognized selector sent to instance 0x608000030920複製程式碼

  上面的提示:SXYPerson找不到copyWithZone:方法。
  我們將複製副本的程式碼換成如下:

 SXYPerson *person2 = [person1 mutableCopy];//複製副本複製程式碼

  再次執行程式,程式同樣崩潰了,並輸出去以下錯誤資訊:

[SXYPerson mutableCopyWithZone:]: unrecognized selector sent to instance 0x600000221120複製程式碼

  上面的提示:SXYPerson找不到mutableCopyWithZone:方法。
  大家可能會覺得疑惑,程式只是呼叫了copy和mutableCopy方法,為什麼會提示找不到copyWithZone:與mutableCopyWithZone:方法呢?其實當程式呼叫物件的copy方法來複制自身時,底層需要呼叫copyWithZone:方法來完成實際的複製工作,copy返回實際上就是copyWithZone:方法的返回值;mutableCopy與mutableCopyWithZone:方法也是同樣的道理。
  那麼怎麼做才能讓自定義的物件進行copy與mutableCopy呢?需要做以下事情:

1.讓類實現NSCopying/NSMutableCopying協議。
2.讓類實現copyWithZone:/mutableCopyWithZone:方法複製程式碼

  所以讓我們的SXYPerson類能夠複製自身,我們需要讓SXYPerson實現NSCopying協議;然後實現copyWithZone:方法:

@interface SXYPerson : NSObject<NSCopying>

@property (nonatomic, assign) NSInteger age;

@property (nonatomic, copy) NSString *name;

@end複製程式碼
#import "SXYPerson.h"

@implementation SXYPerson

- (id)copyWithZone:(NSZone *)zone {

    SXYPerson *person = [[[self class] allocWithZone:zone] init];
    person.age = self.age;
    person.name = self.name;

    return person;

}

@end複製程式碼

  執行之後發現我們實現了物件的複製:

iOS之物件複製
自定義物件複製

  同時需要注意的是如果物件中有其他指標型別的例項變數,且只是簡單的賦值操作:person.obj2 = self.obj2,其中obj2是另一個自定義類,如果我們修改obj2中的屬性,我們會發現複製後的person物件中obj2物件中的屬性值也變了,因為對於這個物件並沒有進行copy操作,這樣的複製操作不是完全的複製,如果要實現完全的複製,需要將obj2對應的類也要實現copy,然後這樣賦值:person.obj2 = [self.obj2 copy]。如果物件很多或者層級很多,實現起來還是很麻煩的。如果需要實現完全複製同樣還有另有一種方法,那就是歸檔:

SXYPerson *person2 = [NSKeyedUnarchiver unarchiveObjectWithData:[NSKeyedArchiver archivedDataWithRootObject:person1]];複製程式碼

  這樣我們就實現了自定義物件的複製,需要指出的是如果重寫copyWithZone:方法時,其父類已經實現NSCopying協議,並重寫過了copyWithZone:方法,那麼子類重寫copyWithZone:方法應先呼叫父類的copy方法複製從父類繼承得到的成員變數,然後對子類中定義的成員變數進行賦值:

- (id)copyWithZone:(NSZone *)zone {

        id obj = [super copyWithZone:zone];
        //對子類定義的成員變數賦值
        ...
        return obj;

}複製程式碼

  關於mutableCopy的實現與copy的實現類似,只是實現的是NSMutableCopying協議與mutableCopyWithZone:方法。對於自定義的物件,在我看來並沒有什麼可變不可變的概念,因此實現mutableCopy其實是沒有什麼意義的,在此就不詳細介紹了。

定義屬性的copy指示符

  如下段程式碼,我們在定義屬性的時候使用了copy指示符:

#import <Foundation/Foundation.h>

@interface SXYPerson : NSObject<NSCopying>

@property (nonatomic, copy) NSMutableString *name;

@end複製程式碼

  使用如下程式碼來進行測試:

SXYPerson *person1 = [[SXYPerson alloc] init];//建立一個SXYPerson物件
person1.name = [NSMutableString stringWithString:@"蘇小妖"];
[person1.name appendString:@"123"];複製程式碼

   執行程式會崩潰,並且提示以下資訊:

*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'Attempt to mutate immutable object with appendString:'複製程式碼

  這段錯誤提示不允許修改person的name屬性,這是因為程式定義name屬性時使用了copy指示符,該指示符置頂呼叫setName:方法時(通過點語法賦值時,實際上是呼叫對應的setter方法),程式實際上會使用引數的副本對name實際變數複製。也就是說,setName:方法的程式碼如下:

- (void)setName:(NSMutableString *)name {

    _name = [name copy];

}複製程式碼

  copy方法預設是複製該物件的不可變副本,雖然程式傳入的NSMutableString,但程式呼叫該引數的copy方法得到的是不可變副本。因此,程式賦給SXYPerson物件的name例項變數的值依然是不可變字串。
  注意:定義合成getter、setter方法時並沒有提供mutableCopy指示符。因此即使定義例項變數時使用了可變型別,但只要使用copy指示符,例項變數實際得到的值總是不可變物件。

總結

  對於物件的深複製的概念沒有必要那麼糾結,只要我們理解了複製的本質,並且運用到我們的業務場景,選擇我們想要的複製方式就可以。最主要的還是理解本質並且學會使用

  希望對您有幫助,如果文章中有問題,歡迎評論留言~,謝謝支援~歡迎關注,我會在空餘時間更新技術文章~

相關文章