深拷貝與淺拷貝
首先,只有遵守 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,效率很差。