DearMiku

iOS对象判等---hash函数

字数统计: 1.4k阅读时长: 5 min
2017/11/22 Share

iOS对象判等—hash函数

要比较相等,我们需要实现 isEqual: 方法。我们希望 isEqual: 方法仅在所有属性都相等的时候返回真。Mike Ash 的 Implement Equality and Hashing 和 NSHipster 的 Equality 为我们很好地阐述了如何实现。首先,我们需要写一个 isEqual: 方法:

1
2
3
4
5
6
7
8
9
10
11
- (BOOL)isEqual:(id)obj
{
if(![obj isKindOfClass:[Person class]]) return NO;

Person* other = (Person*)obj;

BOOL nameIsEqual = self.name == other.name || [self.name isEqual:other.name];
BOOL birthDateIsEqual = self.birthDate == other.birthDate || [self.birthDate isEqual:other.birthDate];
BOOL numberOfKidsIsEqual = self.numberOfKids == other.numberOfKids;
return nameIsEqual && birthDateIsEqual && numberOfKidsIsEqual;
}

如上,我们先检查输入和自身是否是同样的类。如果不是的话,那肯定就不相等了。然后对每一个对象属性,判断其指针是否相等。|| 操作符的操作看起来好像是不必要的,但是如果我们需要处理两个属性都是 nil 的情形的话,它能够正确地返回 YES。比较像 NSUInteger 这样的标量是否相等时,则只需要使用 == 就可以了。

还有一件事情值得一提:这里我们将不同的属性比较的结果分开存储到了它们自己的 BOOL 中。在实践中,可能将它们放到一个大的判断语句中会更好,因为如果这么做的话你就可以避免一些不必要的取值和比较了。比如在上面的例子中,如果 name 已经不相等了的话,我们就没有必要再检查其他的属性了。将所有判断合并到一个 if 语句中我们可以自动地得到这样的优化。

接下来,按照文档所说,我们还需要实现一个 hash 函数。苹果如是说:

如果两个对象是相等的,那么它们必须有同样的 hash 值。如果你在一个子类里定义了 isEqual: 方法,并且打算将这个子类的实例放到集合类中的话,那么你一定要确保你也在你的子类里定义了 hash 方法,这是非常重要的。

首先,我们来看看如果不实现 hash 方法的话,下面的代码会发生什么;

1
2
3
4
Person* p1 = [[Person alloc] initWithName:name birthDate:start numberOfKids:0];
Person* p2 = [[Person alloc] initWithName:name birthDate:start numberOfKids:0];
NSDictionary* dict = @{p1: @"one", p2: @"two"};
NSLog(@"%@", dict);

第一次运行上面的代码是,一切都很正常,字典中有两个条目。但是第二次运行的时候却只剩一个了。事情变得不可预测,所以我们还是按照文档说的来做吧。

作为字典的键,NSSet的元素这种需要判断不重复的情况.

可能你还记得你在计算机科学课程中学到过,编写一个好的 hash 函数是一件不太容易的事情。好的 hash 函数需要兼备确定性和均布性。确定性需要保证对于同样的输入总是能生成同样的 hash 值。均布性需要保证输出的结果要在输出范围内均匀地对应输入。你的输出分布越均匀,就意味着当你将这些对象用在集合中时,性能会越好。

首先我们得搞清楚到底发生了什么。让我们来看看没有实现 hash 函数时候的情况下,使用 Person 对象作为字典的键时的情况:

1
2
3
4
5
6
7
8
9
NSMutableDictionary* dictionary = [NSMutableDictionary dictionary];

NSDate* start = [NSDate date];
for (int i = 0; i < 50000; i++) {
NSString* name = randomString();
Person* p = [[Person alloc] initWithName:name birthDate:[NSDate date] numberOfKids:i++];
[dictionary setObject:@"value" forKey:p];
}
NSLog(@"%f", [[NSDate date] timeIntervalSinceDate:start]);

这在我的机子上花了 29 秒时间来执行。作为对比,当我们实现一个基本的 hash 方法的时候,同样的代码只花了 0.4 秒。这并不是精确的性能测试,但是却足以告诉我们实现一个正确的 hash 函数的重要性。对于 Person 这个类来说,我们可以从这样一个 hash 函数开始:

1
2
3
4
- (NSUInteger)hash
{
return self.name.hash ^ self.birthDate.hash ^ self.numberOfKids;
}

这将从我们的属性中取出三个 hash 值,然后将它们做 XOR (异或) 操作。在这里,这个方法对我们的目标来说已经足够好了,因为对于短字符串 (以前这个上限是 96 个字符,不过现在不是这样了,参见 CFString.c 中 hash 的部分) 来说,NSString 的 hash 函数表现很好。对于更正式的 hash 算法,hash 函数应该依赖于你所拥有的数据。这在 Mike Ash 的文章和其他一些地方有所涉及。

在 hash 文档中,有下面这样一段话:

如果一个被插入集合类的可变对象是依据其 hash 值来决定其在集合中的位置的话,这个对象的 hash 函数所返回的值在该对象存在于集合中时是不允许改变的。因此,要么使用一个和对象内部 状态无关的 hash 函数,要么确保在对象处于集合中时其内部状态不发生改变。比如说,一个可 变字典可以被放到一个 hash table 中,但是只要这个字典还在 hash table 中时,你就不能 更改它。(注意,要知道一个给定对象是不是存在于某个集合中是一件很困难的事情。)

这也是你需要确保对象的不可变性的另一个重要原因。只要确保了这一点,你就不必再担心这个问题了。

转载截取 : 值对象

CATALOG
  1. 1. iOS对象判等—hash函数