Objective-C中“对象等同性”的判定方法

Objective-C中“对象等同性”的判定方法

根据“等同性”来比较对象是一个非常有用的功能。不过通过==操作符比较出来的结果未必是我们想要的,因为该操作比较的是两个指针本身,而不是其所指的对象。应该使用NSObject协议中声明的“isEqual:”方法来判定两个对象的等同性。一般来说,两个类型不同的对象总是不相等的。某些对象对象提供了特殊的“等同性判定方法”,如果已经知道两个受测对象都属于同一个类,那么就可以使用这种方法。以下述代码为例:

NSString *foo = @"Badger 123";
NSString *bar = [NSString stringWithFormat:@"Badger %i",123];
BOOL equalA = (foo == bar);// NO
BOOL equalB = [foo isEqual:bar];//YES
BOOL equalC = [foo isEqualToString:bar];//YES

可以看到==与等同性判定方法之间的差别。NSString类实现了一个自己所独有的等同性判定方法,名为“isEqualToString:”。传递给该方法的对象必须是NSString,否则结果为未定义。调用该方法比调用isEqual:方法快,后者还要执行额外的步骤,因为要判断受测对象的类型。
NSObject协议中有两个用于判定等同性的关键方法:

- (BOOL)isEqual:(id)object; - (NSUInteger)hash;

NSObject类对这两个方法的默认实现是:当且仅当“指针指”完全相等时,这两个对象才相等。若想在自定义的对象中正确覆写这些方法,就必须先理解其中的约定。如果“isEqual:”方法判定两个对象相等,那么其hash方法必须返回同一个值。但是,如果两个对象的hash方法返回同一个值,那么“isEqual:”方法未必会认为两者相等。
比如有下面这个类:

@interface EOCPerson : NSObject
@property (nonatomic, copy) NSString *firstName;
@property (nonatomic, copy) NSString *lastName;
@property (nonatomic, assign) NSUInteger age;
@end

一般认为,如果两个EOCPerson的所有字段均相等,那么这两个对象就相等。于是“isEqual:”方法就可以写成:

- (BOOL)isEqual:(id)object {
    if (self == object) return YES;// 1.判断两个对象的指针是否相同
    if ([self class] != [object class]) return NO;//2.判断两个对象所属的类
    EOCPerson *otherPerson = (EOCPerson *)object;
    if (![_lastName isEqualToString:otherPerson.lastName]) return NO;
    if (_age != otherPerson.age) return NO;//3.判断对象的属性值
    return YES;
}
  1. 首先,直接判断两个指针是否相等。若相等,则其均指向同一个对象,所以受测的对象也必定相等。
  2. 比较两个类所属的类。若不属于同一个类,则两个对象不相等。不过有时我们可能认为:一个父类实例可以与其子类实例相等。在继承体系中判断等同性时,经常遭遇到此类问题。所以实现“isEqual:”方法时要考虑到这种情况。
  3. 检测每个属性是否相等。只要其中有不相等的属性,就判定这两个对象不相等,否则两个对象相等。
  4. 实现hash方法。根据等同性约定:若两个对象相等,则其哈希码也相等,但是两个哈希码相等的对象却未必相等。这是能否正确覆写“isEqual:”方法的关键所在。
    哈希方法的实现:
    返回同一的哈希码是可行的,但是却是不可取的。若返回相同的哈希码,那么在集合中使用这种对象将产生性能问题。在集合中检索对象时,会用到对象的哈希码做索引,这样检索效率就降低了
    如下推荐一种比较好的做法。
- (NSUInteger)hash {
    NSUInteger firstNameHash = [_firstName hash];
    NSUInteger lastNameHash = [_lastName hash];
    NSUInteger ageHash = _age;
    return firstNameHash ^ lastNameHash ^ ageHash;
}

这种做法能够保证生成哈希码的效率。又能使生成的哈希码至少位于一定范围之内,减少碰撞的次数。

特定类所具有的等同性判定方法
系统中一些类的等同性判定方法:
NSString,等同性判定方法:isEqualToString:
NSArray, 等同性判定方法:isEqualToArray:
NSDictionary:等同性判定方法:isEqualToDictionary:
这些方法如果如果受测对象类型不正确,将会抛出异常。
在编写自定类的判定方法时,也应一并覆写“isEqual:”方法。常见的实现方式为:如果受测的对象与接受消息的对象都属于同一个类,那么就调用自己编写的判定方法,否则就交由父类来判定。如下代码实现:

- (BOOL)isEqualToPerson:(EOCPerson *)otherPerson {
    if (self == otherPerson) return YES;
    if (![_firstName isEqualToString:otherPerson.firstName]) {
        return NO;
    }
    if (![_lastName isEqualToString:otherPerson.lastName]) {
        return NO;
    }
    if (_age != otherPerson.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为例,NSArray检测方式为先看两个数组所含对象个数是否相等,若相等,则在每个对应位置的两个对象身上调用其“isEqual:”方法。如果对应位置上的对象均相等,那么这两个数组就相等。不过有时候,无须所有所有的数据都逐个比较。只根据部分数据即可判定,如对象中含有唯一标识符。

容器中可变类的等同性
在容器中放入可变类对象的时候,如果把某个对象放入集合中之后,就不应该在改变其哈希码了。集合会根据各个对象的哈希码来分装数据在不同的位置,如果某个对象在放入数组之后,哈希码又变了,那么其现在所在数组对这个对象来说就是错误的。更具体的介绍请参考书《Effective Objective-C 2.0 编写高质量iOS与OS X代码的52个有效方法》中第八条的介绍。
要点总结:
1. 若想检测对象的等同性,请提供“isEqual:”与hash方法。
2. 相同的对象必具有相同的哈希码,但是两个哈希码相同的对象却未必相同。
3. 不要盲目的逐个检测每条属性,而是应该根据具体的需求来定制检测方案。
4. 编写hash方法时,应该使用计算速度快而且哈希码碰撞几率低的算法。

注:根据《Effective Objective-C 2.0 编写高质量iOS与OS X代码的52个有效方法》进行摘抄和整理。

你可能感兴趣的:(Objective-C,对象等同性的判定方法)