Effective Objective-C 2.0: Object Equality

原创 2013年12月03日 11:57:45

这篇挺有用的,指出了很多注意点;以后用到Object Equality可以参考


Item 8: Understand Object Equality

Being able to compare objects for equality is extremely useful. However, comparing using the == operator is usually not what you want to do, since doing so compares the pointers themselves rather than the objects to which they point. Instead, you should use theisEqual: method declared within the NSObject protocol to check any two objects for equality. Usually, however, two objects of a different class are always determined to be unequal. Some objects also provide special equality-checking methods that you can use if you already know that the two objects you are checking are of the same class. Take, for example, the following code:

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

Here, you can see the difference between == and using equality methods. NSString is an example of a class that implements its own equality-checking method, called isEqualToString:. The object passed to this method must also be an NSString; otherwise, the results are undefined. This method is designed to be faster than calling isEqual:, which has to do extra steps because it doesn’t know the class of the object being compared.

The two methods at the heart of equality checking from the NSObjectprotocol are as follows:

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

The default implementations of these methods from the NSObjectclass itself work such that two objects are equal if and only if their pointer values are exactly the same. To understand how to override these for your own objects, it’s important to understand the contract.Any two objects determined to be equal using the isEqual: method must return the same value from the hash method. However, two objects that return the same value from the hash method do not have to be equal according to the isEqual: method.

For example, consider the following class:

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

Two EOCPerson objects are equal if all the fields are equal. So theisEqual: method would look like this:

- (BOOL)isEqual:(id)object {
    if (self == object) return YES;
    if ([self class] != [object class]) return NO;

    EOCPerson *otherPerson = (EOCPerson*)object;
    if (![_firstName isEqualToString:otherPerson.firstName])
        return NO;
    if (![_lastName isEqualToString:otherPerson.lastName])
        return NO;
    if (_age != otherPerson.age)
        return NO;
    return YES;
}

First, the object is checked for direct pointer equality to self. If the pointers are equal, the objects must be equal, since they are the same object! Next, the class of the two objects is compared. If the class is not the same, the two objects cannot be equal. After all, anEOCPerson cannot be equal to an EOCDog. Of course, you may want an instance of EOCPerson to be equal to an instance of a subclass of it: for example, EOCSmithPerson. This illustrates a common problem in inheritance hierarchies with equality. You should consider this when implementing your isEqual: methods. Last, each property is checked for equality. If any of them are not equal, the two objects are deemed unequal; otherwise, they are equal.

That leaves the hash method. Recall the contract whereby equal objects must return the same hash, but objects with the same hash do not necessarily need to be equal. Therefore, this is essential to override if you override isEqual:. A perfectly acceptable hash method would be the following:

- (NSUInteger)hash {
    return 1337;
}

However, this could lead to performance problems if you ever put these objects in a collection, since the hash is used as an index within the hash tables that collections use. A set implementation might use the hash to bin objects into different arrays. Then when an object is added to the set, the array corresponding to its hash is enumerated to see whether any objects in that array are equal. If they are, the object is already in the set. Therefore, if you return the same hash value for every object and you add 1,000,000 objects to the set, each further addition to the set has to scan each of those 1,000,000 objects.

Another implementation of the hash method might be:

- (NSUInteger)hash {
    NSString *stringToHash =
        [NSString stringWithFormat:@"%@:%@:%i",
            _firstName, _lastName, _age];
    return [stringToHash hash];
}

This time, the algorithm of NSString’s hash method is piggybacked by creating a string and returning the hash of that. Doing so adheres to the contract, since two EOCPerson objects that are equal will always return the same hash. However, the downside of this approach is that it is much slower than returning a single value, since you have the overhead of creating a string. This can again cause performance issues when adding the object to a collection, since the hash has to be calculated for the object being added to the collection.

A third and final approach is to create a hash like this:

- (NSUInteger)hash {
    NSUInteger firstNameHash = [_firstName hash];
    NSUInteger lastNameHash = [_lastName hash];
    NSUInteger ageHash = _age;
    return firstNameHash ^ lastNameHash ^ ageHash;
}

This approach is a middle ground between efficiency and creating at least some range of hashes. There will, of course, be collisions with hashes created using this algorithm, but at least multiple return values are possible. The tradeoff between collision frequency and a computationally intensive hash method is something that you should experiment with and see what works for your object.

Class-Specific Equality Methods

Other than NSString as described earlier, classes that provide a specific equality method include NSArray (isEqualToArray:) andNSDictionary (isEqualToDictionary:), both of which will throw an exception if the object being compared is not an array or a dictionary, respectively. Objective-C has is no strong type checking at compile time, so you could easily accidentally pass in an object of the wrong type. Therefore, you need to be sure that the object you’re passing in is indeed of the correct type.

You may decide to create your own equality method if equality is likely to be checked frequently; therefore, the extra speed from not having to check types is significant. Another reason for providing a specific method is purely cosmetic where you think that it looks better and is more readable, which is likely part of the motivation for NSString’sisEqualToString: method. Code that uses this method is easier to read, as you don’t have to hunt for the types of the two objects being compared.

If you do create a specific method, you should override the isEqual:method also and pass through if the class of the object being compared is the same as the receiver. If it’s not, passing through to the superclass implementation is common practice. For example, theEOCPerson class could implement the following:

- (BOOL)isEqualToPerson:(EOCPerson*)otherPerson {
    if (self == object) 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];
    }
}

Deep versus Shallow Equality

When you create an equality method, you need to decide whether to check the whole object for equality or simply a few fields. NSArraychecks whether the two arrays being compared contain the same number of objects and if so, iterates through them and calls isEqual:on each. If all objects are equal, the two arrays are deemed to be equal, known as deep equality. Sometimes, however, if you know that only a selection of the data determines equality, it is valid to not check every bit of data for equality.

For example, using the EOCPerson class, if instances had come from a database, they might have another property added with a unique identifier used as the primary key in the database:

@property NSUInteger identifier;

In such a scenario, you may decide to check only that the identifiers match, especially if the properties were declared readonly externally such that you can be certain that if two objects have the same identifier, they are indeed representing the same object and are therefore equal. This would save on having to check through every single bit of data that the EOCPerson object contains when you can assert that if the identifiers match, so must the rest of the data, since it came from the same data source.

Whether or not you check all fields in your equality method depends entirely on the object in question. Only you can know what it means for two instances of your object to be equal.

Equality of Mutable Classes in Containers

An important scenario to consider is when mutable classes are put into containers. Once you add an object to a collection, its hash should not change. Earlier, I explained how objects are binned according to their hash. If their hash changes once in a bin, the objects would be in the wrong bin. To get around this problem, you can either ensure that the hash is not dependent on the mutable portions of the object or simply not mutate objects once they are in collections. In Item 18, I explain why you should make objects immutable. This is a great example of such a reason.

You can see this problem in action by testing with an NSMutableSetand a few NSMutableArrays. Start by adding one array to the set:

NSMutableSet *set = [NSMutableSet new];

NSMutableArray *arrayA = [@[@1, @2] mutableCopy];
[set addObject:arrayA];
NSLog(@"set = %@", set);
// Output: set = {((1,2))}

The set contains one object: an array with two objects in it. Now add an array that contains equal objects in the same order, such that the array already in the set and the new one are equal:

NSMutableArray *arrayB = [@[@1, @2] mutableCopy];
[set addObject:arrayB];
NSLog(@"set = %@", set);
// Output: set = {((1,2))}

The set still contains just a single object, since the object added is equal to the object already in there. Now we add to the set an array that is not equal to the array already in the set:

NSMutableArray *arrayC = [@[@1] mutableCopy];
[set addObject:arrayC];
NSLog(@"set = %@", set);
// Output: set = {((1),(1,2))}

As expected, the set now contains two arrays: the original one and the new one, since arrayC does not equal the one already in the set. Finally, we mutate arrayC to be equal to the other array already in the set:

[arrayC addObject:@2];
NSLog(@"set = %@", set);
// Output: set = {((1,2),(1,2))}

Ah, oh dear, now two arrays in the set are equal to each other! A set is not meant to allow this, but it has been unable to maintain its semantics because we’ve mutated one of the objects that was already in the set. What’s even more awkward is if the set is then copied:

NSSet *setB = [set copy];
NSLog(@"setB = %@", setB);
// Output: setB = {((1,2))}

The copied set has only a single object in it, just as if the set had been created by making an empty set and one by one adding the entries from the original. This may or may not be what you expected. You may have thought that it would copy verbatim the original, with the corruption included. Or you may have thought that it would do what it did. Both would be valid copying algorithms, which further illustrates the point that the set had become corrupt and so all bets are off when dealing with it.

The moral of this story is to be aware of what can happen when you mutate an object that’s in a collection. It’s not to say you should never do it, but you should be aware of the potential problems and code accordingly.

Things to Remember

Image Provide isEqual: and hash methods for objects that you will want to check for equality.

Image Objects that are equal must have the same hash, but objects that have the same hash do not necessarily have to be equal.

Image Determine what is necessary to test for equality rather than bluntly testing every property.

Image Write hash methods that will be quick but provide a reasonably low level of collisions.

相关文章推荐

Effective Objective-C 2.0:Item 14 Class Object

Item 14: Understand What a Class Object Is Objective-C is extremely dynamic in nature. Item 11 expl...

《Effective Objective-C 2.0》读书笔记

去年阅读了一少半,然后因为项目耽搁了,而且笔记没有记得很清楚,所以这次又从头开始看。这本书还是很经典的,介绍了很多细节性的问题,理论性也很强,没有浪费这么长时间。...
  • null29
  • null29
  • 2017年03月06日 14:48
  • 270

阅读《Effective Objective-C 2.0 编写高质量iOS与OS X代码的52个有效方法》总结

第1条:了解Objective-C语言的起源 Objective-C为C语言添加了面向对象特性,是其超集。Objective-C使用动态绑定的消息结构,也就是说,在运行时才会检查对象类型。接收一...

《Effective Objective-C 2.0》3、枚举类型表示状态、选项

第五条:使用枚举类型表示状态和选项,可以使代码更加清晰,可读性更好。 枚举类型使用关键字enum定义,通常与typedef相结合,定义一组状态或选项: typedef enum CustomState...

<iOS读书笔记>之Effective Objective-C 2.0

本文主要对书中每个章节的要点进行梳理.第1章.熟悉Objective - C第1条.了解Objective-C语言的起源 要点 Objective-C为C语言添加了面向对象特性,是其超集。Ob...

Effective Objective-C 2.0: Item 37: Understand Blocks

Effective Objective-C 2.0 介绍Blocks的文章,有些论点比较新颖,觉得不错,值得一看...

Effective Objective-C 2.0: Item 31: Release References and Clean Up Observation State Only in deallo

Item 31: Release References and Clean Up Observation State Only in dealloc An object going throug...

Effective Objective-C 2.0: Item 32: Beware of Memory Management with Exception-Safe Code

Item 32: Beware of Memory Management with Exception-Safe Code Exceptions are a language feature o...

Effective Objective-C 2.0: Item 6: Understand Properties

前面的runtime没看懂- -; 后面的atomicity 和 thread-safe 说明, 很好很强大 ^ ^ Item 6: Understand Properties Pro...

Effective Objective-C 2.0: Item 38: Create typedefs for Common Block Types

Item 38: Create typedefs for Common Block Types Blocks have an inherent type; that is, they can be ...
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:Effective Objective-C 2.0: Object Equality
举报原因:
原因补充:

(最多只允许输入30个字)