翻译自 Mike Ash 的 Implementing Equality and Hashinghtml
对象判等是一个基本的概念,在代码中常常会被使用到。在 Cocoa 编程中,它经过 isEqual:
方法被实现。一些比较简单的例子像[array indexOfObject:]
会在底层使用到它,因此说对象支持判等是很是重要的。算法
在 Cocoa 编程中,它已经为咱们在 NSObject 中提供了一个默认的判等实现。这个默认的实现只是经过对象的指针地址来进行判等。换句话说,一个对象只会与它本身相等。这个默认实现从功能上看以下面代码所示:编程
- (BOOL)isEqual: (id)other {
return self == other;
}
复制代码
虽然该默认的判等实现看上去过于简单,但实际上它对于许多对象来讲,是十分有用的。好比,咱们永远不会把一个 NSView 看作跟另一个 NSView 同等。一样的,对于不少具备该特性的类来讲,这个默认的判等实现已是足够了。这或许是个好消息,由于这意味着若是你的类具备相同的判等特性,那么你不须要作任何事情就能够免费获得想要的结果。数组
有时候你须要实现更深层次的判等。这对于许多对象来讲是很正常的,特别是那些被看做“值类型的对象”,他们是根据逻辑上的判等来区分。举个例子:缓存
// 使用可变类型保证生成的是不一样的字符串对象
NSMutableString *s1 = [NSMutableString stringWithString: @"Hello, world"];
NSMutableString *s2 = [NSMutableString stringWithString: @"%@, %@", @"Hello", @"world"];
BOOL equal = [s1 isEqual: s2]; // 返回 YES !
复制代码
固然啦,NSMutableString 在这种状况下已经为你作了判等实现。可是若是你想要为自定义的对象作一样的操做该怎么办?安全
MyClass *c1 = ...;
MyClass *c2 = ...;
BOOL equal = [c1 isEqual: c2];
复制代码
在这种状况下你须要实现你本身版本的isEqual:
方法。bash
测试相等性在大多数状况下是至关简单的。把你的类中全部相关的属性收集起来,再测试他们的相等性。若是他们当中有不相等的,那么返回 NO ,不然返回 YES 。数据结构
有一个微妙的点就是,当你的对象所对应的类也是检测相等性中的一个重要的属性。去检测 MyClass 和 NSString 的相等性是十分合理的,可是这种比较的结果永远不会返回 YES (除非 MyClass 是 NSString 的一个子类)。测试
有一个稍微不那么微妙的点就是,确保你测试的属性对于判等来讲是很是重要的。一些像缓存 caches 这样的属性对于你的对象的外部视角而言是可有可无的,那么它就不须要被用做判等的因素。ui
好比说你的类看起来像这样:
@interface MyClass: NSObject {
int _length;
char *_data;
NSString *_name;
NSMutableDictionary *_cache;
}
复制代码
你的判等实现看起来会像这样:
- (BOOL)isEqual: (id)other {
return ([other isKindOfClass: [MyClass class]] && [other length] == _length && memcmp([other data], _data, _length) == 0 && [[other name] isEqual: _name])
// 注意:没有 _cache 的比较
}
复制代码
哈希表是一个普通的数据结构,被用于实现 NSDictionary 和 NSSet 等。不管你往容器类中添加多少对象,都可以支持快速查找到相应的对象。
若是你已经了解哈希表是如何工做的,你能够直接跳过接下来的一到两个段落内容。
哈希表基本上能够被看作是一个带特殊索引的庞大数组。全部被添加到数组的对象都会有一个索引关联着他们的哈希值。这个哈希值本质上是由对象的属性而产生的伪随机的数字。这种机制使得索引有足够的随机性,那么两个对象就不太可能拥有相同的哈希值了,但这是彻底可复写的。当一个对象被插入到数组中时,它的哈希值会被用来决定它该被放到哪一个位置上。当一个对象被查找时,它的哈希值会被用来决定到哪一个位置中查找。
用更加正式的术语来说,一个对象的哈希值被定义了,若是两个对象是相等的,那么他们会有相同的哈希值。要注意的是,反过来讲是不正确的,也不该该这样,由于:两个对象能够有相同的哈希值,可是他们能够不相等。你想要尽量的避免出现这种状况,由于当两个不相等的对象拥有两个相同的哈希值(称为碰撞),那么哈希表就必须采起特殊的措施去处理这种状况,这是一个很是耗时的操做。然而,这已经被证实了要想彻底避免哈希碰撞的发生是不可能的。
在 Cocoa 编程中,哈希的实现经过哈希方法,它的方法签名为:
- (NSUInteger)hash;
复制代码
跟对象判等同样,NSObject 也为你提供了一个默认的哈希实现,但这是经过使用对象的标识来实现的。粗略的讲,它作了这些事情:
- (NSUInteger)hash {
return (NSUInteger)self;
}
复制代码
实际返回的值可能不同,但本质的关键点是,这种方式是基于实际指向 self 的指针的值。跟判等方法同样,若是基于对象标识的判等已经达到你想要的需求,那么默认的实现对你来讲已是有用的了。
由于哈希的语义,若是你重写了isEqual:
方法,你就必须重写哈希方法。若是你不这样作,你会遇到两个相同对象却拥有不一样的哈希值的状况,这是十分不安全的。若是你在字典、集合或者其余须要哈希表的地方使用到这些对象,那么会出现问题。
由于对象哈希值的定义和相等性的关系是十分密切的,一样的,哈希方法的实现和判等方法的实现也十分密切。
一个例外的状况是,不须要在哈希值的定义中包含你的对象所属的类。这主要是做为isEqual:
方法的一个保护措施,是为了确保跟不一样对象之间比较时剩余内容的检测有意义。若是经过不一样的数学方式去合并不一样属性的哈希值,那么你的哈希值极可能跟其余不一样的类的哈希值相比就会很是不同。
检测属性的相等性一般来讲是很简单的,但计算他们的哈希值却不老是那么简单。你如何计算一个属性的哈希值取决于对象的类型是什么。
对于数值型属性,哈希值能够被简单的设定为数字的值。
对于对象型属性,你能够经过调用对象的哈希方法,来使用其返回的哈希值。
对于数据型属性,你会想要使用一些哈希算法来生成哈希值。你可使用 CRC32 ,或者重量型的 MD5 。后者的执行速度相对较慢,但便于使用,它经过把数据封装在 NSData 中,而且获取它的哈希值。在上面的例子中,你能够像这样计算出 _data 的哈希值:
[[NSData dataWithBytes: _data length: _length] hash];
复制代码
因此你已经知道了如何为每一个属性生成哈希值,可是要如何将他们合并在一块儿呢?
最简单的方式就是将他们相加在一块儿,或者使用按位或的特性。然而,这会破坏哈希值的独特性,由于这些操做都是对称性的,意味着区分不一样对象时会出错。举个例子,假设一个对象有 first 和 last name 两个属性,它的哈希方法的实现以下:
- (NSUInteger)hash {
return [_firstName hash] ^ [_lastName hash];
}
复制代码
如今假设你有两个对象,一个是 “George Frederick” ,另外一个是 “Frederick George”。即便他们很明显是不一样的,但他们仍是会有相同的哈希值。虽然哈希碰撞的发生是彻底不可避免的,但咱们也应该尽可能让这种状况不轻易出现。
如何合并哈希值是一个复杂的主题,是没法用一个回答就能解释的。然而,使用任何不对称的方式去合并哈希值倒是一个很好的开始。我打算使用位移运算加上按位异或预算来合并他们。
#define NSUINT_BIT (CHAR_BIT * sizeof(NSUInteger))
#define NSUINTROTATE(val, howmuch) ((((NSUInteger)val) << howmuch) | (((NSUInteger)val) >> (NSUINT_BIT - howmuch)))
复制代码
- (NSUInteger)hash {
return NSUINTROTATE([_firstName hash], NSUINT_BIT / 2) ^ [_firstName hash];
}
复制代码
如今咱们能够运用上述内容来为前面的例子生成一个哈希值。这跟判等方法的实现同样会遵循一些基本的格式,而且会使用上述的技术去获取和合并每一个属性的哈希值。
- (NSUInteger)hash {
NSUInteger dataHash = [[NSData dataWithBytes: _data length: _length] hash];
return NSUINTROTATE(dataHash, NSUINT_BIT / 2) ^ [_name hash];
}
复制代码
若是你还有更多的属性,你能够添加更多的位移运算和按位或操做,并且这个流程都是相似的。你还可能会想要为每个属性调整位移运算来使得他们每个都是不一样的。
你必需要注意当你子类化的是一个实现了自定义的哈希方法和判等方法的父类。尤为是你的子类不该该暴露那些在判等方法的实现中使用到的新的属性。若是你这样作了,那么该子类的实例确定与父类的实例不相等。
为了解释这种状况,假设一个子类拥有 first/last name 属性,且包含一个 birthday 属性,并且 birthday 做为判等计算的一部分。然而,这不能够用在父类的实例中比较相等性,因此它的判等方法看起来像这样:
- (BOOL)isEqual: (id)other {
// 笔者注:若是调用父类的判等实现的结果返回了 NO ,那么不用比较新属性(若是有)也可知道确定也不相等。
if(![super isEqual: other])
return NO;
// 若是执行到这一步,证实经过父类的判等实现的结果返回的是 YES ,接下来观察要判断 other 是不是子类或者子类的子类类型,若是不是,则证实要判等的两个对象实质上是同一个父类对象。
if(![other isKindOfClass: [MyClass class]])
return YES;
// 若是执行到这一步,证实要判等的是两个子类类型,并且对于父类中的属性已被证实是相等的,那么接下来继续判断新属性是否相等便可。
return [[other birthday] isEqual: _birthday];
}
复制代码
如今假设你有一个父类的实例对应 “John Smith” ,我称之为 A ,和一个子类实例对应 “John Smith”,而且生日为 5/31/1982,我称之为 B 。由于有了上述的判等定义,那么结果为,A 等于 B ,B 也等于他本身,获得了指望的结果。
如今假设你有一个子类的实例对应 “John Smith” ,生日为 6/7/1994,我称之为 C 。那么 C 不等于 B ,获得咱们指望的结果。 C 等于 A ,一样获得指望的结果。可是如今出现了一个问题,A 等于 B 和 C ,可是 B 和 C 不相等!这打破了相等操做的传递性,而且会形成很是意外的后果。
一般来说这不该该是一个严重的问题。若是你的子类添加了会影响父类对象判等的新属性,这是你的类层级结构中的一个明显的设计问题。你应该去考虑如何从新设计你的类层级结构,而不是在isEqual:
方法中作一些复杂的实现。
若是你想要在 NSDictionary 中使用你的对象来做为 key 值,你须要实现对应的哈希方法和判等方法,并且你也须要实现-copyWithZone:
方法。作这样的技巧已经超出了本文的内容,但你应该意识到在某些状况下你须要作更多事情。
在 Cocoa 编程中已经为你提供了哈希方法和判等方法的默认实现,这对于许多对象而言是有用的,可是若是你想要为你本身的对象即便在内存地址是不相同的状况下,也想要经过判等结果返回 YES 来指明他们是相等的,那么你就必需要作一点额外的工做。幸运的是,这实现起来并不困难,而且一旦你实现了他们,你自定义的类将能够用在许多 Cocoa 的集合类中。