Objective-C 筆記

深拷貝與淺拷貝

首先,只有遵守 NSCopying 協議的類才能發送 copy 消息。同理,遵守了 NSMutableCopying 協議的類才能發送 mutableCopy 消息。大部分 Foundation 中的類都遵守 NSCopying 協議,但是 NSObject 的子類,也就是我們自定義的類并未遵守 NSCopying 協議。

淺拷貝,又稱為指針拷貝,并不會分配新的內存空間,新的指針和原指針指向同一地址。深拷貝,又稱對象拷貝,會分配新的內存空間,新指針和原指針指向不同的內存地址,但是存儲的內容相同。

依照深拷貝淺拷貝的特性,淺拷貝多用于添加引用,達到操作新對象,則所有指向同步發生變化的目的;反之深拷貝是隔離原對象和新對象,各自操作互不干擾。

Foundation 中非容器對象的 Copy

copy    mutableCopy
NSString    淺拷貝 深拷貝
NSMutableString 深拷貝 深拷貝

由表格可見,除了不可變對象 NSString 的不可變副本是淺拷貝以外,其他均為深拷貝。由于對象本身為不可變對象,所以在 copy 不可變副本的時候才用了指針復制,無必要新分配空間做深拷貝。

Foundation 中容器對象的 Copy

copy    mutableCopy
NSArray 淺拷貝 深拷貝
NSMutableArray  深拷貝 深拷貝
Object in NSArray   淺拷貝 淺拷貝
Object in NSMutableArray    淺拷貝 淺拷貝

除了不可變對象 NSArray 的不可變副本為淺拷貝以外,其他容器對象均為深拷貝。需要指出的是,容器內的對象均為淺拷貝,這就意味著,新容器的內部的對象改變,原容器內部的對象會同步改變。

如果要實現容器和內部對象的深拷貝,需要遵循 NSCoding 協議,先將對象 archive 再 unarchive。

NSArray *array  = @[@1, @2];
NSData *data = [NSKeyedArchiver archivedDataWithRootObject:array];
NSArray *newArray = [NSUnarchiver unarchiveObjectWithData:data];

此時 newArray 無論是容器本身還是容器內部對象都和原來的 array 無關聯。

自定義對象的 Copy

自定義對象繼承自 NSObject,需要自己實現 NSCopying 協議下的 copyWithZone 方法。

Person.h

import <Foundation/Foundation.h>
@interface Person : NSObject<NSCopying>
- (Person *)personWithName:(NSString *)name age:(NSString *)age;
@end

Person.m

import "Person.h"

@interface Person ()
@property (nonatomic, strong) NSString *name;
@property (nonatomic, strong) NSString *age;
@end

@implementation Person

- (Person *)initWithName:(NSString *)name age:(NSString *)age {
  if (self = [super init]) {
      self.name  = name;
      self.age = age;
  }
  return self;
  }

- (Person *)personWithName:(NSString *)name age:(NSString *)age {
  return [[self alloc] initWithName:name age:age];
  }

- (Person *)copyWithZone:(NSZone *)zone {
  Person *person = [[Person allocWithZone:zone] init];
  person.name = [self.name copyWithZone:zone];
  person.age = [self.age copyWithZone:zone];
  return person;
  }
  @end

Hash/Equal

Equal

NSObject 類中的 equal 方法的判斷,是包括內存地址的。換句話說,NSObject 若想判斷兩個對象相等,那這兩個對象的地址必須相同。

但實際編碼中,我們常常設計一個對象,其各項屬性相同, 我們就認為他們 equal,要達到這個目的,我們就要重載 equal 方法。于是我們在上述的 Person 對象中重載如下方法:

Person.m

- (BOOL)isEqual:(Person *)other {
  BOOL isMyClass     = [other isKindOfClass:self.class];
  BOOL isEqualToName = [other.name isEqualToString:self.name];
  BOOL isEqualToAge  = [other.age isEqualToString:other.age];
  if (isMyClass && isEqualToName && isEqualToAge) {
  return YES;
  }
  return NO;
  }

main.m

# import <Foundation/Foundation.h>
# import "Person.h"

int main(int argc, const char *argv[]) {

@autoreleasepool {
    Person *person1 = [Person personWithName:@"Joe" age:@"32"];
    Person *person2 = [Person personWithName:@"Joe" age:@"32"];
    NSLog(@"isEqual-----%zd", [person1 isEqual:person2]);
}
return 0;
}

控制臺打印結果為

isEqual-----1
證明確實完成了屬性相同,就判斷兩個對象 equal 的目的。

Hash

任何 Objective-C 都有 hash 方法,該方法返回一個 NSUInteger,是該對象的 hashCode。

-(NSUInteger)hash {
   return (NSUInteger)self>>4;
}

上述是 Cocotron 的 hashCode 的計算方式,簡單通過移位實現。右移4位,左邊補0。因為對象大多存于堆中,地址相差4位應該很正常,所以不同對象可能會有相同的 hashCode。當對象存入集合(NSSet, NSDictionary)中,他們的 hashCode 會作為 key 來決定放入哪個集合中。

存儲表

hashCode    subCollection
code1       value1,value2,value3,value4
code2       value5,value6
code3       value7
code4       value8,value9,value10

集合的內部是一個 hash 表,由于不同對象的 hashCode 可能相同,所以同一個 hashCode 對象的將會是一個 subCollection 的集合。如果要刪除或者比較集合內元素,它首先根據 hashCode 找到子集合,然后跟子集合的每個元素比較。

集合內部的查找策略是,先比較 hashCode,如果 hashCode 不同,則直接判定兩個對象不同;如果 hashCode 相同,則落到同一個 subCollection 中,再調用 equal 方法具體判斷對象是否相同。所以,如果兩個對象相同,則 hashCode 一定相同;反之,hashCode 相同的兩個對象,并不一定是相同的對象。

如果所有對象的 hashCode 都相同,那么每次比較都會調用 equal 方法,整個查詢效率會變得很低。

集合中自定義對象的存取

本節中集合對象選定為 NSDictionary。Hash 這一節中,我們得知了集合內部實際是一個 HashTable。那自定義對象,按照新邏輯重載 equal 方法之后,在集合中的存取應該如何?

參考 Cocotron 源碼,NSDictionary 使用 NSMapTable 實現的。

@interface NSMapTable : NSObject {

   NSMapTableKeyCallBacks   *keyCallBacks;
   NSMapTableValueCallBacks *valueCallBacks;
   NSUInteger               count;
   NSUInteger               nBuckets;
   struct _NSMapNode        * *buckets;

}
上面是NSMtabtable真正的描述,可以看出來NSMapTable是一個哈希+鏈表的數據結構,因此在 NSMapTable *

中插入或者刪除一對對象時:

  • 為對key進行hash得到bucket的位置
  • 遍歷該bucket后面沖突的value,通過鏈表連接起來。

由于一對鍵值存入字典中之后,key 是不能隨意改變的,這樣會造成 value 的丟失。所以一個自定義對象作為 key 存入 NSDictionary,必定要深拷貝。正是為了實現這一目的,則 key 必須遵守 NSCopying 協議。

main.m

# import <Foundation/Foundation.h>
# import "Person.h"

int main(int argc, const char *argv[]) {

@autoreleasepool {
    Person *person1 = [Person personWithName:@"Joe" age:@"32"];
    Person *person2 = [Person personWithName:@"Joe" age:@"32"];
    Person *person3 = [Person personWithName:@"Joe" age:@"33"];
    NSMutableDictionary *dict = [[NSMutableDictionary alloc] init];
    [dict setObject:@"1" forKey:person1];
    [dict setObject:@"2" forKey:person2];
    [dict setObject:@"3" forKey:person3];
    NSLog(@"person1----%@", [dict objectForKey:person1]);
    NSLog(@"person2----%@", [dict objectForKey:person2]);
    NSLog(@"person3----%@", [dict objectForKey:person3]);
    NSLog(@"dict count: %ld", dict.count);
}
return 0;

}
由于我們重載了 equal 方法,person1 和 person2 應該是相同對象,理論上 dict 的 count 應該是 2。

事實上打印結果是隨機的,dict 內部可能會有2或3組鍵值對。Person 實例化對象取出的值也是不盡相同。這是因為,在對象存入 key 時,每次都要進行 hash/equal 驗證,如果為相同對象,則不增加鍵值對數量,直接覆蓋之前 key 的 value。我們重載了 equal 方法,但是 person1 和 person2 的 hashCode 是不同的,則他們直接會被判定為不同的對象,person2 直接作為新的 key 存入了 dict。

在取 key 的時候,依舊要執行 hash/equal ,由于存入 dict 中的副本是深拷貝,那副本的 hashCode 和原對象也是不同的,會判定要查找的對象在 key 中不存在,造成了能存不能查的情況。

這就是我們為什么重載了 equal 就必須還要重載 hash 的根本原因。

重載 hash 要保證,其 hash 算法只跟成員變量相關,即 name 和 age;同時要保證其深拷貝副本的 hashCode 與 原對象相同。

Person.m

- (NSUInteger)hash {
  return [self.name hash] ^ [self.age hash];
  }  

切記不能全部返回相同的 hashCode,這樣會每次都調用 equal,效率很差。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,698評論 6 539
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,202評論 3 426
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,742評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,580評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,297評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,688評論 1 327
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,693評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,875評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,438評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,183評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,384評論 1 372
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,931評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,612評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,022評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,297評論 1 292
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,093評論 3 397
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,330評論 2 377

推薦閱讀更多精彩內容