这篇挺有用的,指出了很多注意点;以后用到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 NSObject
protocol are as follows:
- (BOOL)isEqual:(id)object;
- (NSUInteger)hash;
The default implementations of these methods from the NSObject
class 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. NSArray
checks 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 NSMutableSet
and a few NSMutableArray
s. 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
Provide
isEqual:
and hash methods for objects that you will want to check for equality.
Objects that are equal must have the same hash, but objects that have the same hash do not necessarily have to be equal.
Determine what is necessary to test for equality rather than bluntly testing every property.
Write hash methods that will be quick but provide a reasonably low level of collisions.