天天看點

iPhone開發之Deep Copy和Shallow Copy的差別

首先,從copy開始說,簡而言之,copy的目的就是生成一個新的執行個體,然後把其成員都按原執行個體指派。對于非指針型的成員,比如BOOL, int, float,這樣的指派可以直接進行。但是對于指針型的資料,比如Objc中用到的對象,就有Deep Copy和Shallow Copy的差別——這個和在C++中的基本上是一樣的:是生成新的成員對象,或是指向同一成員對象。

了解了這點以後,再看看Copy在Objetive-C中的實作方式。如果要調用一個對象的copy方法,這個對象必須遵循NSCopying的協定。這個協定中規定了一個方法:- (id)copyWithZone:(NSZone *)zone;我們就是通過實作這個方法給對象提供拷貝的功能。對于很多現有類,如NSString,NSDictionary,。。。這個方法已經實作。假設我們現在自定義了一個類,需要為這個類提供拷貝的功能,就需要自己來動手寫CopyWithZone的方法:示例如下:

這個是自定義的類:

@interface Product : NSObject <NSCopying>

{

    NSString *productName;

    float price;

    id delegate;

}

@end

然後我們需要在Product類中實作NSCopying中的copyWithZone方法:

- (id)copyWithZone:(NSZone *)zone

{

    Product *copy = [[[self class] allocWithZone: zone]

            initWithProductName:[self productName]

            price:[self price]]; //注意這裡,我們使用了class的allocWithZone的方法建立了一個拷貝,這裡假定Product類中有一個initWithProductName: price:的初始化方法。那麼這樣調用後就得到了一個Product的副本,而且name和price都已經設定好了

    [copy setDelegate:[self delegate]]; //這裡再設定delegate

    return copy; //傳回副本

}

那麼這樣,如果我們有一個product的執行個體, 假設為product1,然後調用Product *product2 = [product1 copy];

就會使用我們上面寫的copyWithZone的方法建立一個product1的副本,然後指派給product2。

這裡再以上面方法中的成員delegate為例,解釋一下deep copy和shallow copy:

在copyWithZone方法中,我們得到了一個新的product執行個體,但是delegate是個對象,是以在副本中,我們可以選擇建立一個新的delegate對象(deep copy),或是指向同一個delegate(shallow copy)。這個就取決于Product類中的setDelegate:方法了。你可以選擇在setDelegate的時候,copy,也可以讓它們都指向同一個對象(但是需要retain,原因可以自己思考一下),當然,簡單assign在某種情況下也是可以的。

假設在Product類中有setDelegate:方法,或是有delegate的property:

- (void)setDelegate: (id)aDelegate

{

       [delegate release];

       delegate = [delegate copy];

}

這樣就是一個深拷貝了,因為使用了delegate的copy方法得到了一個delegate的副本。至于如何得到delegate的副本,就要看delegate的copyWithZone方法的實作了,不在這個層面的考慮中。也就是說,copy總是一中“遞歸”的形式,從上到下,我們可以一層一層的考慮。

- (void)setDelegate: (id)aDelegate

{

     [delegate release];

     delegate = [aDelegate retain];

}

這樣操作後,delegate和aDelegate為同一對象,但是為了記憶體管理方面的要求,我們調用了retain來将reference count加了一。當然,如果不需要了,還可以直接指派(assign):

- (void)setDelegate: (id)aDelegate

{

    delegate = aDelegate

}

你可以把這個例子自己實作一下,然後用log打一打記憶體,這個結構就很明了了。

然後再說一下可變副本(mutable copy)和不可變副本(immutable copy):

可變和不可變的概念,我們之前通過NSDictionary和NSMutableDictionary的差別了解過。

一般來說,如果我們的某個類需要差別對待這兩個功能——同時提供建立可變副本和不可變副本的話,一般在NSCopying協定規定的方法copyWithZone中傳回不可變副本;而在NSMutableCopying的mutableCopyWithZone方法中傳回可變副本。然後調用對象的copy和mutableCopy方法來得到副本。

舉個例子:

NSDictionary類已經遵循了NSCopying和NSMutableCopy的協定,也就是說我們可以調用它的copy和mutableCopy來得到不可變和可變的副本,程式如下:

    NSDictionary *testDict = [[NSDictionary alloc]initWithObjectsAndKeys:@"hello", @"test",nil];

    NSDictionary *destDict = [testDict copy];

    NSLog(@"test Dict:%p,retain Count: %d/ndest Dict:%p, retain Count: %d",testDict,[testDict retainCount],destDict,[destDict retainCount]);

這個在我機器上的運作結果為:

test Dict:0x11f220, retain Count: 2

dest Dict:0x11f220,retain Count: 2

看起來,兩個dict指向了同一片記憶體區域,但是retainCount加了1。這點需要了解一下,因為我們使用NSCopying方法要傳回一個不可變對象。而且原來的testDict也是不可變的,那麼這裡的“副本”也就沒多大意義了(這就如同使用字元串常量時,系統會為我們優化,聲明了多個字元串,但是都是常量,且内容相等,那麼系統就隻為我們申請一塊空間,這個道理是一樣的)。既然都不可變,那麼指向同一個空間就可以了。這裡的copy和retain沒什麼差別。

我們使用copyWithZone的方法傳回immutable的對象,而不管原來的是可變的或是不可變的。我們再看一下如下代碼:

    NSMutableDictionary *testDict = [[NSMutableDictionary alloc]initWithObjectsAndKeys:@"hello", @"test",nil];

    NSMutableDictionary *destDict = [testDict copy];

    NSLog(@"test Dict:%p,retain count:%d/ndest Dict:%p,retain count:%d",testDict,[testDict retainCount],destDict,[destDict retainCount]);

    [destDict setObject:@"what" forKey:@"test2"];

NSMutableDictionary是可變的,該代碼在我機器上運作的結果為:

test Dict:0x20dcc0,retain count:1

dest Dict:0x209120,retain count:1

*** -[NSCFDictionary setObject:forKey:]: mutating method sent to immutable object

可以看到因為我們調用了可變對象的copy方法,這個不像之前的例子中一樣,隻是retain了一下。這裡的test dict和dest Dict已經是兩個對象了,但是,copyWithZone的方法傳回的是不可變的對象,是以之後的setObject: forKey:方法會出現錯誤。

下面這樣改一下就OK了。

    NSMutableDictionary *testDict = [[NSMutableDictionary alloc]initWithObjectsAndKeys:@"hello", @"test",nil];

    NSMutableDictionary *destDict = [testDict mutableCopy];

    NSLog(@"test Dict:%p,retain count:%d/ndest Dict:%p,retain count:%d",testDict,[testDict retainCount],destDict,[destDict retainCount]);

    [destDict setObject:@"what" forKey:@"test2"];

    NSLog(@"destDict:%@",destDict);

運作結果為:

test Dict:0x123550,retain count:1

dest Dict:0x10a460,retain count:1

destDict:{

    test = hello;

    test2 = what;

因為我們使用了mutableCopy來得到了一個可變副本。

Note:對于系統提供的所有既支援NSCopying,又支援NSMutableCopying的類。

copy方法,得到的是不可變對象,不管以前的是可變還是不可變。

mutableCopy方法,得到的是可變對象,不管以前的是可變還是不可變。

繼續閱讀