1. 程式人生 > >iOS 集合的深拷貝與淺拷貝

iOS 集合的深拷貝與淺拷貝

Copying Collections 概念

拷貝的方式有兩種:淺拷貝和深拷貝。 從字面意思理解,淺拷貝,只是拷貝了物件的指標,而不是拷貝物件本身。 深拷貝,是直接拷貝整個物件的記憶體到另一塊記憶體中。

如下圖所示:左側是淺拷貝,右側是深拷貝


圖示

從圖上也能簡單這樣認為:淺拷貝就是拷貝物件的指標,深拷貝就是拷貝物件本身。

shallow copy 淺拷貝

淺拷貝有很多中方法,當你進行淺拷貝時,會向原始的集合傳送retain訊息,這時引用計數就會 +1 ,同時指標就被拷貝到新的集合中去。

下面是一個實現淺拷貝的例子:

NSArray *shallowCopyArray = [someArray copyWithZone:nil
]; NSDictionary *shallowCopyDict = [[NSDictionary alloc] initWithDictionary:someDictionary copyItems:NO];

deep copy 深拷貝

集合的深拷貝有兩種方法。可以用 initWithArray:copyItems: 將第二個引數設定為YES即可深拷貝,如:

NSDictionary shallowCopyDict = [[NSDictionary alloc] initWithDictionary:someDictionary copyItems:YES];

如果你用這種方法深拷貝,集合裡的每個物件都會收到 copyWithZone: 訊息。如果集合裡的物件都遵循 NSCopying 協議,那麼物件就會被深拷貝到新的集合。如果物件沒有遵循 NSCopying 協議,而嘗試用這種方法進行深拷貝,會在執行時出錯。copyWithZone: 這種拷貝方式只能夠提供一層記憶體拷貝(one-level-deep copy),而非真正的深拷貝。

第二個方法是將集合進行歸檔(archive),然後解檔(unarchive),如:

NSArray *trueDeepCopyArray = [NSKeyedUnarchiver unarchiveObjectWithData:[NSKeyedArchiver archivedDataWithRootObject:oldArray]];

one-level-deep copy 集合的單層深拷貝

有人可能會問:如果在多層陣列中,對第一層進行內容拷貝,其它層進行指標拷貝,這種情況是屬於深拷貝,還是淺拷貝?對此,蘋果官網文件有這樣一句話描述:

This kind of copy is only capable of producing a one-level-deep copy.
If you only need a one-level-deep copy, you can explicitly call for one as in Listing 2.

從文中可以看出,蘋果認為這種拷貝不是真正的深拷貝,而是將其稱為單層深拷貝(one-level-deep copy)。因此,網上有人對淺拷貝、深拷貝、單層深拷貝做了概念區分。

  • 淺拷貝(shallow copy):在淺拷貝操作時,對於被拷貝物件的每一層都是指標拷貝。
  • 深拷貝(one-level-deep copy):在深拷貝操作時,對於被拷貝物件,至少有一層是深拷貝。
  • 完全拷貝(real-deep copy):在完全拷貝操作時,對於被拷貝物件的每一層都是物件拷貝

當然,這些都是概念性的東西,沒有必要糾結於此。只要知道進行拷貝操作時,被拷貝的是指標還是內容即可

系統物件的copy與mutableCopy方法

不管是集合類物件,還是非集合類物件,接收到copy和mutableCopy訊息時,都遵循以下準則:

  • copy返回imutable物件;所以,如果對copy返回值使用mutable物件介面就會crash;
  • mutableCopy返回mutable物件;

下面將針對非集合類物件和集合類物件的copy和mutableCopy方法進行具體的闡述

1、非集合類物件的copy與mutableCopy

系統非集合類物件指的是 NSString, NSNumber ... 之類的物件。下面先看個非集合類immutable物件拷貝的例子

NSString *string = @"origin";
NSString *stringCopy = [string copy];
NSMutableString *stringMCopy = [string mutableCopy];

通過檢視記憶體,可以看到 stringCopy 和 string 的地址是一樣,進行了指標拷貝;而 stringMCopy 的地址和 string 不一樣,進行了內容拷貝;

再看mutable物件拷貝例子

NSMutableString *string = [NSMutableString stringWithString: @"origin"];
//copy
NSString *stringCopy = [string copy];
NSMutableString *mStringCopy = [string copy];
NSMutableString *stringMCopy = [string mutableCopy];
//change value
[mStringCopy appendString:@"mm"]; //crash
[string appendString:@" origion!"];
[stringMCopy appendString:@"!!"];

執行以上程式碼,會在第7行crash,原因就是 copy 返回的物件是 immutable 物件。註釋第7行後再執行,檢視記憶體,發現 string、stringCopy、mStringCopy、stringMCopy 四個物件的記憶體地址都不一樣,說明此時都是做內容拷貝。

綜上兩個例子,我們可以得出結論:

在非集合類物件中:對immutable物件進行copy操作,是指標拷貝,mutableCopy操作時內容拷貝;對mutable物件進行copy和mutableCopy都是內容拷貝。用程式碼簡單表示如下:

[immutableObject copy] // 淺拷貝
[immutableObject mutableCopy] //深拷貝
[mutableObject copy] //深拷貝
[mutableObject mutableCopy] //深拷貝

2、集合類物件的copy與mutableCopy

集合類物件是指NSArray、NSDictionary、NSSet ... 之類的物件。下面先看集合類immutable物件使用copy和mutableCopy的一個例子:

NSArray *array = @[@[@"a", @"b"], @[@"c", @"d"];
NSArray *copyArray = [array copy];
NSMutableArray *mCopyArray = [array mutableCopy];

檢視內容,可以看到copyArray和array的地址是一樣的,而mCopyArray和array的地址是不同的。說明copy操作進行了指標拷貝,mutableCopy進行了內容拷貝。但需要強調的是:此處的內容拷貝,僅僅是拷貝array這個物件,array集合內部的元素仍然是指標拷貝。這和上面的非集合immutable物件的拷貝還是挺相似的,那麼mutable物件的拷貝會不會類似呢?我們繼續往下,看mutable物件拷貝的例子:

NSMutableArray *array = [NSMutableArray arrayWithObjects:[NSMutableString stringWithString:@"a"],@"b",@"c",nil];
NSArray *copyArray = [array copy];
NSMutableArray *mCopyArray = [array mutableCopy];

檢視記憶體,如我們所料,copyArray、mCopyArray和array的記憶體地址都不一樣,說明copyArray、mCopyArray都對array進行了內容拷貝。同樣,我們可以得出結論:

在集合類物件中,對immutable物件進行copy,是指標拷貝,mutableCopy是內容拷貝;對mutable物件進行copy和mutableCopy都是內容拷貝。但是:集合物件的內容拷貝僅限於物件本身,物件元素仍然是指標拷貝。用程式碼簡單表示如下:

[immutableObject copy] // 淺拷貝
[immutableObject mutableCopy] //單層深拷貝
[mutableObject copy] //單層深拷貝
[mutableObject mutableCopy] //單層深拷貝

這個程式碼結論和非集合類的非常相似。

這時候,是不是有人要問了,如果要對集合物件拷貝元素怎麼辦?有這疑問的同學不妨回頭看看集合的深拷貝。

好了,深拷貝與淺拷貝就講到這裡。

最後說個題外的東西,在蒐集資料的過程中,發現一個有可能犯錯的點

NSString *str = @"string";
str = @"newString";

上面這段程式碼,在執行第二行程式碼後,記憶體地址發生了變化。乍一看,有點意外。按照 C 語言的經驗,初始化一個字串之後,字串的首地址就被確定下來,不管之後如何修改字串內容,這個地址都不會改變。但此處第二行並不是對 str 指向的記憶體地址重新賦值,因為賦值操作符左邊的 str 是一個指標,也就是說此處修改的是記憶體地址。

所以第二行應該這樣理解:將@"newStirng"當做一個新的物件,將這段物件的記憶體地址賦值給str。

我有如下的兩個方法檢視記憶體地址

  • p str 會列印物件本身的記憶體地址和物件內容
    (lldb) p str
    (NSString *) $0 = 0x000000010c913680 @"a"
  • po &str 則列印的是引用物件的指標所在的地址
    (lldb) po &str
    0x00007fff532fb6c0

參考文件