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 读书笔记

第 1 章 熟悉 Objective-C 第 2 章 对象消息运行时 第 3 章 接口和 API 设计 第 4 章 协议与分类 第 5 章 内存管理 循环引用 普通的两个变量互相引用 Block 循环...
  • xsl_bj
  • xsl_bj
  • 2016年06月06日 12:27
  • 4769

编写高质量的iOS代码--Effective Objective-C 2.0 读书笔记

编写高质量的iOS代码--Effective Objective-C 2.0 读书笔记 这本书年初刷完,感觉不错,介绍了很多小点,都是平日不怎么关注的. 第1章 熟悉Objective-C...
  • uxyheaven
  • uxyheaven
  • 2014年12月26日 23:56
  • 5054

【Effective Objective-C 2.0读书笔记】第三章:接口和API设计

一旦你完成了一个应用,你可能会希望在以后的工程中重用部分代码。你也可能会发布一些代码以供其他人来使用。这意味着你需要使用Objective-C的范式和理解众多的陷阱。...
  • freeWayWalker
  • freeWayWalker
  • 2015年07月06日 15:23
  • 594

iOS-Effective Objective-C 2.0 读书笔记(三)

第三章的内容主要是说接口和API设计相关的注意事项。比如说我们自己写的代码需要设计以便于代码复用时,应该注意的一些问题包括哪些。简单总结  有些注意事项实际上很简单,而且很常见,我觉得并不需要太长篇幅...
  • linyousong
  • linyousong
  • 2016年04月29日 22:31
  • 415

effective stl 第19条:理解相等(equality)和等价(equivalence)的区别

#include #include #includeusing namespace std;bool ciStringCompare(const string l, const string r) {...
  • u014110320
  • u014110320
  • 2016年09月20日 23:36
  • 251

Effective Object-C 2.0

关于Object-C的类和对象的分析先暂停两天,还有关于编译器如何将OC的代码转换成C或者C++代码的部分没有做,那些对于理解OC的类和对象,消息传递等等会有不少的用途。这两天在着手翻译 Effect...
  • lipeng08
  • lipeng08
  • 2014年03月28日 15:22
  • 1558

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

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

《Effective Objective-C 2.0编写高质量iOS与OS X代码的52个有效方法》读书笔记(下)

1.为避免在不经意间使用了无效对象,一般在release之后会清空指针,=nil; 2.通常利用弱引用或者“手动”解除引用的方式破坏循环引用。 3.ARC下,规定以alloc、new、copy、mut...
  • zhaochen_009
  • zhaochen_009
  • 2017年04月06日 10:44
  • 326

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

个人觉得这本书很不错!里面有很多很实用但以前没有太注意的地方,所以想纪录下来,当作自己的读书笔记吧。 熟悉Objective-C 1)Objective-C语言使用“消息结构”而不是“函数调用”。O...
  • weimeng809
  • weimeng809
  • 2016年01月11日 00:18
  • 585

《Effective Objective-C 2.0编写高质量iOS与OS X代码的52个有效方法》读书笔记(上)

1.OC对象所占内存总是分配在"堆"中,而绝不会分配在"栈"中,不能再栈中分配OC对象。"栈"中对象借助栈帧进行维护,"堆"中对象的管理借助引用计数机制. -(NSMutableArray *)te...
  • zhaochen_009
  • zhaochen_009
  • 2017年04月05日 10:10
  • 292
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:Effective Objective-C 2.0: Object Equality
举报原因:
原因补充:

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