第八条:理解“对象等同性”这一概念
- 根据等同性来比较对象是一个非常有用的功能。使用
==
操作符比较未必就得到正确的结果,因为该操作符比较的是两个指针本身,而不是其指向的对象。应该使用NSObject
协议中声明的isEqual:
方法来判断两个对象的等同性。 - 一般来说,两个类型不同的对象总是不相等的。
- 某些对象提供了自己的等同性判定的方法,如果知道两个对象属于同一个类,就可以使用这种方法。
NSString *foo = @"Badger 123";
NSString *bar = [NSString stringWithFormat:@"Badger %i", 123];
NSLog(@"%d", foo == bar); // false
NSLog(@"%d", [foo isEqual:bar]); // true
NSLog(@"%d", [foo isEqualToString:bar]); // true
从上述代码可以看出==
和等同性判断的差别。NSString
类实现了一个自己独有的等同性判断的方法,必须传递一个字符串对象作为参数。比isEqual:
方法速度快,而isEqual:
方法还要执行额外的步骤。因为它不知道接收参数的对象类型。
-
NSObject
协议中有两个方法用于等同性判断:- (Bool)isEqual:(id)object
-(NSUinteger)hash;
-
NSObject
类对这两个方法的默认实现是:当且仅当其指针值完全相等时,这两个对象才相等。(下面代码参考GNUstep)
- (BOOL) isEqual: (id)anObject
{
return (self == anObject);
}
- (NSUInteger) hash
{
/*
* malloc() must return pointers aligned to point to any data type
*/
#define MAXALIGN (__alignof__(_Complex long double))
static int shift = MAXALIGN==16 ? 4 : (MAXALIGN==8 ? 3 : 2);
/* We shift left to lose any zero bits produced by the
* alignment of the object in memory.
*/
return (NSUInteger)((uintptr_t)self >> shift);
}
-
如果想判断自定义对象的等同性,就要重写这两个方法。并且需要遵循一个约定:如果
isEqual
方法判断两个对象相等,那么其hash
方法也必须返回同一个值。但是hash
方法返回同一个值,isEqual
方法未必认为两个对象相等。 -
以
EOCPerson
类举例,我们约定,如果两个EOCPerson
的所有字段均相等,那么这两个对象相等。
// EOCPerson.h
@interface EOCPerson : NSObject
@property (nonatomic, copy) NSString *firstName;
@property (nonatomic, copy) NSString *lastName;
@property (nonatomic, assign) NSUInteger age;
@end
// EOCPerson.m
- (BOOL)isEqual:(id)object {
if (self == object) { // 同一个对象返回YES
return YES;
}
if ([self class] != [object class]) { // 不是同一个类型,返回NO
return NO;
}
EOCPerson *otherPerson = (EOCPerson *)(object);
if (![_firstName isEqualToString:otherPerson.firstName]) { // 名字不一样,返回NO
return NO;
}
if (![_lastName isEqualToString:otherPerson.lastName]) { // 姓不一样,返回NO
return NO;
}
if (_age != otherPerson.age) { // 年龄不一样,返回NO
return NO;
}
return YES;
}
- hash方法,根据等同性的约定:两个对象相等,那么他们的hash值必须相等,但是,hash值相同的对象不一定是相等的对象。
- 第1种实现方案:直接返回一个固定个的值
- (NSUInteger)hash {
return 1337;
}
如果这么写,在collection(代表Array、Dictionary、Set等数据结构)
中使用会产生性能问题,因为collection
在检索哈希表时,会用对象的哈希值作为索引。加入某个collection
是用set
实现的,那么set
可能会根据哈希值把对象分装到不同数组,在向set
添加对象的时候,要根据哈希值找到与之相关的数组,依次检查其中的各个元素,看数组中已有的对象是否将要添加的新对象相等。如果相等说明对象已经在set
里面了。如果对象都返回相同的哈希值,那么在set
中已有100000个对象的情况下,若是继续向其中添加新对象,最坏情况下,需要把100000个对象都遍历一遍。
- 第2中实现方案:以实例变量的值为基础,创建字符串,返回字符串的hash值。这种做法符合约定。但是每次都要用额外的开销去创建一个字符串对象。
- (NSUInteger)hash {
NSString *stringToHash = [NSString stringWithFormat:@"%@:%@:%i", _firstName, _lastName, _age];
return [stringToHash hash];
}
- 第3中方案:分别生成哈希值,最后进行位运算,返回哈希值。这种做法可以保持高效,还可以使生成的哈希值在一定范围内。
- (NSUInteger)hash {
NSUInteger firstNameHash = [_firstName hash];
NSUInteger lastNameHash = [_lastName hash];
NSUInteger ageHash =_age;
return firstNameHash ^ lastNameHash ^ ageHash;
}
特定类所具有的等同性判定方法
- 除了
NSString
类之外,NSArray
和NSDictionary
类也具有特殊的等同性判定方法。前者为isEqualToArray:
,后者为isEqualToDictionay:
,如果和其相比较的对象不是数组或字典,那么就会抛出异常。 - 如果经常需要判断某一种类型的等同性,可以自己创建一个等同性判定的方法,这样无需检测类型,提高速度,代码更加易读。
- 自己编写等同性判定方法,同样需要重写
isEqual:
方法,如果两者是同一类型,就调用我们自己的判定方法,否则调用父类的方法去判断。
- (BOOL)isEqualToPerson:(EOCPerson *)person {
if (self == person) {
return YES;
}
if (![_firstName isEqualToString:person.firstName]) {
return NO;
}
if (![_lastName isEqualToString:person.lastName]) {
return NO;
}
if (_age != person.age) {
return NO;
}
return YES;
}
- (BOOL)isEqual:(id)object {
if ([self class] == [object class]) {
return [self isEqualToPerson:(EOCPerson *)object];
} else {
return [super isEqual:object];
}
}
等同性判定的执行深度
- 创建等同性方法时,需要决定是根据整个对象来判断等同性,还是仅根据其中几个字段来判断。
NSArray
的检测方式为先看2个数组所含对象的个数是否相同,若相同,则在每个对应位置的2个对象身上来调用isEqual:
方法。如果对应位置的对象均相等,那么这2个数组就是相等的,这叫做深度等同性判定。有些时候,无需将所有数据逐个比较,只根据其中部分数据比较即可。 - 举例:假设
EOCPerson
的数据来源于数据库,那么其中可能包含一个属性,就是数据库中的主键,也是唯一标识。在这种情况下,只要是唯一标识是相同的,就表示肯定是同一个对象。这样就不需要比较EOCPerson
的每个属性是不是相同的了。
容器中可变类的等同性
- 在容器中放入可变对象的时候,一定要注意。把某个对象放入
collection
之后,就不应再改变其哈希值。collection
会把每个对象按照其哈希值分装到不同的箱子数组中。如果某个对象在放入箱子之后哈希值又变了。那么其所处的这个箱子对它来说就是错误的。要想解决这个问题,需要保证哈希值不是根据对象的可变部分计算出来的,或是保证放入collection
之后就不再改变对象内容了。 - 举个例子:用一个
NSMutableSet
和几个NSMutableArray
对象测试,首先把一个数组添加到set
中
NSMutableSet *set = [NSMutableSet set];
NSMutableArray *arrayA = @[@1, @2].mutableCopy;
[set addObject:arrayA];
NSLog(@"set = %@", set);
/* set = {(
(1,2)
)}
*/
-
set
里面有一个数组对象,数组中有两个对象。再向set
中添加一个数组,此数组与前一个数组所含的对象相同,顺序也相同。
NSMutableArray *arrayB = @[@1, @2].mutableCopy;
[set addObject:arrayB];
NSLog(@"set = %@", set);
/* set = {(
(1,2)
)}
*/
-
set
里面仍然只有一个对象,因为刚加入的数组与已有的数组相等的,所以set
不会变。此时,我们添加一个和set
已有数组不一样的数组。
NSMutableArray *arrayC = @[@1].mutableCopy;
[set addObject:arrayC];
NSLog(@"set = %@", set);
/*
set = {(
(1),
(1,2)
)}
*/
-
set
此时有两个数组。接下来,我们改变arrayC
的内容,让其和之前加入的数组相等。
[arrayC addObject:@2];
NSLog(@"set = %@", set);
/*
set = {(
(1,2),
(1,2)
)}
*/
- 从打印结果来看,
set
中有2个完全相同的数组。但是set
的语义是不允许重复出现的,因为我们修改了set
中的对象。如果我们此时拷贝set
NSSet *setB = [set copy];
NSLog(@"setB = %@", setB);
/*
setB = {(
(1,2)
)}
*/
- 复制过的
set
又变成了只有一个数组对象。这样的代码就很容易产生很难预料的结果和隐患。
网友评论