13

I ran into an issue, where I got the same hash value for different dictionaries. Maybe I'm doing something obvious wrong, but I thought, objects with different content (a.k.a. not equal objects) should have different hash values.

NSDictionary *dictA = @{ @"foo" : @YES };
NSDictionary *dictB = @{ @"foo" : @NO };

BOOL equal = [dictA hash] == [dictB hash];

NSAssert(!equal, @"Assuming, that different dictionaries have different hash values.");

Any thoughts?

Tobias Kräntzer
  • 1,694
  • 1
  • 13
  • 23

5 Answers5

27

There is no guarantee that two different objects will have different hash values.

In the latest open-source version of CoreFoundation, the hash of a CFDictionary (which is equivalent to an NSDictionary) is defined as:

static CFHashCode __CFDictionaryHash(CFTypeRef cf) {
    return __CFBasicHashHash((CFBasicHashRef)cf);
}

and __CFBasicHashHash is defined as:

__private_extern__ CFHashCode __CFBasicHashHash(CFTypeRef cf) {
    CFBasicHashRef ht = (CFBasicHashRef)cf;
    return CFBasicHashGetCount(ht);
}

which is simply the number of entries stored in the collection. In the other words, both [dictA hash] and [dictB hash]'s hash value are 1, the number of entries in the dictionaries.

While it is a very bad hash algorithm, CF didn't do anything wrong here. If you need to have a more accurate hash value, you can provide one yourself in an Obj-C category.

kennytm
  • 510,854
  • 105
  • 1,084
  • 1,005
  • „While it is a very bad hash algorithm,…“ This for sure. I guess I have to work around this in my case. Thanks. – Tobias Kräntzer Aug 16 '12 at 09:52
  • 2
    **There is a very good reason it is implemented this way.** As NSArray is immutable, it's [-copy] returns self. If it would be e.g. added to a dictionary as a key, the hash should never change, even if the objects in the array are mutated. – Patrick Pijnappel Apr 26 '14 at 08:47
  • 2
    @Patrick Well the "sum of the first 32 pointer values of the array" is also immutable. The only good reason is that it's extremely fast to compute, or since hardly anyone uses an NSDictionary as a key let's not create a complicated hash. – kennytm Apr 27 '14 at 07:19
  • @kennytm, well, "sum of the first 32 pointer values of the array" is indeed immutable - but then you may end up with two equal array objects with two different hash values which would violate hash/isEqual invariant. for example: `NSArray *arr1 = @[@(3)];` `NSArray *arr2 = @[@(3.000)];` they are isEqual, but if we use your hash-scheme (first 32 pointer values), their hash values would differ (because @(3) and @(3.000) have different pointer values) – jason na Jul 21 '17 at 17:52
5

With a dictionary with only integers, strings etc. I would use dict.description.hash as a quick code.

superarts.org
  • 7,009
  • 1
  • 58
  • 44
1

A solution based on igor-kulagin's answer which is not order dependent:

@implementation NSDictionary (Extensions)

- (NSUInteger) hash
{
    NSUInteger prime = 31;
    NSUInteger result = 1;
    for (NSObject *key in [[self allKeys] sortedArrayUsingSelector:@selector(compare:)]) {
        result = prime * result + [key hash];
        result = prime * result + [self[key] hash];
    }
    return result;
}
@end

However, there is still a possibility of collision if the dictionary contains other dictionaries as values.

Community
  • 1
  • 1
Jan Berkel
  • 3,373
  • 1
  • 30
  • 23
1

The function 'hash' is not a real hash function. It gives different values for strings (all predictable) but for collections (arrays and dictionaries) it just returns the count. If you want a unique hash you can calculate it yourself using primes, or the functions srandom() and random() or explore a real hash function like SHA1 available in CommonCrypto/CommonDigest.h

Peter B. Kramer
  • 16,385
  • 1
  • 16
  • 20
-1

I created NSDictionary category and overridden hash method based on this answer: Best practices for overriding isEqual: and hash

@implementation NSDictionary (Extensions)

- (NSUInteger) hash {
    NSUInteger prime = 31;
    NSUInteger result = 1;

    NSArray *sortedKeys = [self.allKeys sortedArrayUsingSelector: @selector(compare:)];
    for (NSObject *key in sortedKeys) {
        result = prime * result + key.hash;
        id value = self[key];
        if ([value conformsToProtocol: @protocol(NSObject)] == YES) {
            result = prime * result + [value hash];
        }
    }

    return result;
}

@end

And Swift implementation.

extension Dictionary where Key: Comparable, Value: Hashable {
    public var hashValue: Int {
        let prime = 31
        var result = 1

        let sortedKeys = self.keys.sorted()
        for (key) in sortedKeys {
            let value = self[key]!
            result = Int.addWithOverflow(Int.multiplyWithOverflow(prime, result).0, key.hashValue).0
            result = Int.addWithOverflow(Int.multiplyWithOverflow(prime, result).0, value.hashValue).0
        }

        return result
    }
}

Perfectly this also requires to implement Equatable protocol for Dictionary so you can also add Hashable protocol conformance.

Igor Kulagin
  • 1,701
  • 15
  • 20
  • 2
    this might return different hash values for the same hash, since the ordering of the keys might change. a better solution is to order the keys first. – Jan Berkel May 20 '14 at 13:25