How to implement equality based on property values? - objective-c

I’m implementing -isEqual: for my custom class. The equality is based on the property values, ie. if all properties are equal, the objects are considered equal. Together with the traditional class check the code looks like this:
- (BOOL) isEqual: (id) object
{
return [object class] == [self class]
&& [[object someProperty] isEqual:someProperty]
&& [[object otherProperty] isEqual:otherProperty];
}
But this fails for nil property values, ie. two objects of the class having nil values stored in someProperty are considered non-equal, whereas I would like them to be equal. Thus I arrived at the following version:
- (BOOL) isEqual: (id) object
{
#define equals(a, b) ((a == b) || ([a isEqual:b]))
return equals([object class], [self class])
&& equals([object someProperty], someProperty)
&& equals([object otherProperty], otherProperty);
}
This seems to work fine. Is this the “standard” way to solve the equality? Seems overly complex to me.

isEqual: is very much object-specific. It's a semantic equality. It is up to you to define for every class what isEqual means. Thus there is no standard way to do it.
The simplest implementation is return self == object, your second implementation is very generic and nice, but not necessarily well suited to every class. Per example, for a Person, comparing the emails could be sufficient, or the emails and first names if you suppose an email could be used by several family members.

Related

Why doesn't NSNull behave like nil for unimplemented methods?

As per my understanding, NSNull exists as a replacement of nil in situations where an object is explicitly required, such in NSArray and NSDictionary collections.
One of the good parts of nil's behavior is that many verbose null checks can be skipped and it would convenient to have the same behavior implemented by NSNull. Consider the following example:
NSDictionary * json = // deserialized JSON from a REST service
NSString * name = json[#"first_name"];
if (name != nil && ![name isEqual:[NSNull null]] && [name isEqualToString:#"Gabriele"]) { ... }
the check can currently be simplified to
if (![name isEqual:[NSNull null]] && [name isEqualToString:#"Gabriele"]) { ... }
but if [NSNull null] was mimicking the behavior of nil, it could be simplified even more to
if ([name isEqualToString:#"Gabriele"]) { ... }
So my question is why didn't Apple override the -forwardInvocation: method of NSObject to mimic the behavior of nil on an unrecognized selectors?
I would like to get the philosophy behind it and to understand whether there is some drawback or some broken convention that I cannot think of.
My understanding is that nil's behavior is part of the Objective C language, not of any particular library. NSNull, on the other hand, is part of a library, with a single very specific purpose:
The NSNull class defines a singleton object used to represent null values in collection objects (which don’t allow nil values).
It is not intended for use as a "sentinel" object: if you wish to construct such an object, you can always do so, because collections themselves do not assign NSNull any particular significance. It is provided for programmers' convenience in situations when they wish to store nils, so that programmers could avoid reinventing their own null object every time they need it.
Also note that [NSNull null] is a singleton, so you can compare it with == / != operators instead of calling isEqual:
if (name != [NSNull null] && [name isEqualToString:#"Gabriele"])

Comparing objects using the compare method in objective-c

If I were to try and compare two objects in objective-c, I believe as of IOS 6 it is possible to just use normal operators such as == (proof below), however, I would like to know how this would be done with a -compare method.
As far as I know, the compare method works something in this fashion:
if ([objOne compare:objTwo] == NSOrderedAscending){
// objOne is greater
First of all, is that correct? And secondly, what are the other comparison methods, I.e, NSOrderedAscending?
the writer of a class is resposible for comparing, meaning you have to define when two objects are equal.
This is done by defining a method compare:, that should return either NSOrderedAscending, NSOrderedDescending, NSOrderedSame
From the docs
NSComparisonResult
These constants are used to indicate how items in a request are
ordered.
enum {
NSOrderedAscending = -1,
NSOrderedSame,
NSOrderedDescending
};
typedef NSInteger NSComparisonResult;
Note, that nothing stops you from creating other methods that return those NSComparisionResults. iE NSString has compare: and caseInsensitiveCompare:
== will just check for identity: if the pointers are the same, this will return true, while if there are to different objects but that are the same in mening of the same properties, it will still return false.
From NSComparisonMethods Protocol Reference
The default implementation provided for many of these methods by NSObject is appropriate for objects that implement a single comparison method whose selector, signature, and description match the following:
- (NSComparisonResult)compare:(id)object;
This method should return NSOrderedAscending if the receiver is less
than object, NSOrderedDescending if the receiver is greater than
object, and NSOrderedSame if the receiver and object are equal. For
example, NSString does not implement most of the methods declared in
this informal protocol, but NSString objects still handle messages
conforming to this protocol properly because NSString implements a
compare: method that meets the necessary requirements. Cocoa also
includes appropriate compare: method implementations for the NSDate,
NSDecimalNumber, and NSValue classes.
This means that if you provided a -compare: method, you can also use -isEqual:.
if ([objOne isEqual:objTwo]){ //…

Comparing one property of an instance against an array of other instances

I'm trying to write an instance method for a Card class that compares a single card against an array. The class has some properties like: shape and color. The otherCards array is filled with other instances of this class, that also have their shapes and colors.
Now, I want to write a method that can check all of these attributes separately. How can I pass in a particular attribute, as in: [allAttributesIsEqual:otherCards compareWith: self.shape]? So I can pass in self.shape or self.color when actually comparing?
- (BOOL)allAttributesIsEqual: (NSArray *)otherCards
{
//self.shape is equal to othercards.shape
}
You can't just pass in self.shape, because that will give you the value of the property. Thanks to some of Cocoa/ObjC's dynamite, however, you can pass in the name of a property (or method) and get the results later.
The clever (dare I say, perhaps even "Pythonic") way:
// The name of the property we're interested in.
NSString * key = #"color";
// Get the values of that property for all the Cards in the array, then
// collapse duplicates, because they'll give the same results when comparing
// with the single card.
NSSet * vals = [NSSet setWithArray:[arrayOfCards valueForKey:key]];
// Now, if the set has only one member, and this member is the same
// as the appropriate value of the card we already have, all objects
// in the array have the same value for the property we're looking at.
BOOL colorIsEqual = ([vals count] == 1 && [vals containsObject:[myCard valueForKey:key]]);
Then your method can look like this:
- (BOOL)allOtherCards: (NSArray *)otherCards haveEqualAttribute: (NSString *)key;
Dan F's suggestion to implement - (BOOL)<#property#>Equal: (NSArray *)otherCards; for each property you're interested in is not at all a bad idea, however. Of course, each of these could call through to the base "clever" version.
The idea is that you (as the Card class) know what it means for two instances to be "equal". It sounds like in your case, two Cards are equivalent if their color and shape properties match. Start by implementing -isEqual: (along with -hash) on your custom Card class. This is the standard way of having an object expose a notion of whether it is the same as some other object. You can implement this however you need. Within this isEqual method, you can check all of the relevant properties:
- (BOOL)isEqual:(id)otherObject
{
if (![otherObject isKindOfClass:[self class]) {
return NO;
}
Card * otherCard = (Card *)otherObject;
// now compare the attributes that contribute to "equality"
return ([self.shape isEqual:otherCard.shape] && [self.color isEqual:otherCard.color]);
}
Now, once your custom object supports this -isEqual:, you can check all the cards in the array to see if any are equal to the candidate card. You could do the loop yourself and use the -isEqual:, but the nice thing about doing this in the system standard way is that you can also use system provided convenience methods to check for collection membership, like:
if ([myCardList containsObject:candidateCard]) {
// one of the cards compared as "equal"
}
If you would prefer to do this as you request in a method on your class, you could then structure it like so:
- (BOOL)isRepresentedInArray:(NSArray *)arr
{
return [arr containsObject:self];
}

Objective-C: writing a smart reusable compare function [duplicate]

I'm trying to write an instance method for a Card class that compares a single card against an array. The class has some properties like: shape and color. The otherCards array is filled with other instances of this class, that also have their shapes and colors.
Now, I want to write a method that can check all of these attributes separately. How can I pass in a particular attribute, as in: [allAttributesIsEqual:otherCards compareWith: self.shape]? So I can pass in self.shape or self.color when actually comparing?
- (BOOL)allAttributesIsEqual: (NSArray *)otherCards
{
//self.shape is equal to othercards.shape
}
You can't just pass in self.shape, because that will give you the value of the property. Thanks to some of Cocoa/ObjC's dynamite, however, you can pass in the name of a property (or method) and get the results later.
The clever (dare I say, perhaps even "Pythonic") way:
// The name of the property we're interested in.
NSString * key = #"color";
// Get the values of that property for all the Cards in the array, then
// collapse duplicates, because they'll give the same results when comparing
// with the single card.
NSSet * vals = [NSSet setWithArray:[arrayOfCards valueForKey:key]];
// Now, if the set has only one member, and this member is the same
// as the appropriate value of the card we already have, all objects
// in the array have the same value for the property we're looking at.
BOOL colorIsEqual = ([vals count] == 1 && [vals containsObject:[myCard valueForKey:key]]);
Then your method can look like this:
- (BOOL)allOtherCards: (NSArray *)otherCards haveEqualAttribute: (NSString *)key;
Dan F's suggestion to implement - (BOOL)<#property#>Equal: (NSArray *)otherCards; for each property you're interested in is not at all a bad idea, however. Of course, each of these could call through to the base "clever" version.
The idea is that you (as the Card class) know what it means for two instances to be "equal". It sounds like in your case, two Cards are equivalent if their color and shape properties match. Start by implementing -isEqual: (along with -hash) on your custom Card class. This is the standard way of having an object expose a notion of whether it is the same as some other object. You can implement this however you need. Within this isEqual method, you can check all of the relevant properties:
- (BOOL)isEqual:(id)otherObject
{
if (![otherObject isKindOfClass:[self class]) {
return NO;
}
Card * otherCard = (Card *)otherObject;
// now compare the attributes that contribute to "equality"
return ([self.shape isEqual:otherCard.shape] && [self.color isEqual:otherCard.color]);
}
Now, once your custom object supports this -isEqual:, you can check all the cards in the array to see if any are equal to the candidate card. You could do the loop yourself and use the -isEqual:, but the nice thing about doing this in the system standard way is that you can also use system provided convenience methods to check for collection membership, like:
if ([myCardList containsObject:candidateCard]) {
// one of the cards compared as "equal"
}
If you would prefer to do this as you request in a method on your class, you could then structure it like so:
- (BOOL)isRepresentedInArray:(NSArray *)arr
{
return [arr containsObject:self];
}

Comparing objects in Obj-C

How does one compare objects in Objective-C?
Is it as simple as == ?
I want to check an array for an object and if it doesnt exist add it to the array otherwise, remove it from the array.
Comparing objects in Objective-C works much the same as in Java or other object-oriented languages:
== compares the object reference; in Objective-C, whether they occupy the same memory address.
isEqual:, a method defined on NSObject, checks whether two objects are "the same." You can override this method to provide your own equality checking for your objects.
So generally to do what you want, you would do:
if(![myArray containsObject:anObject]) {
[myArray addObject:anObject];
}
This works because the Objective-C array type, NSArray, has a method called containsObject: which sends the isEqual: message to every object it contains with your object as the argument. It does not use == unless the implementation of isEqual: relies on ==.
If you're working entirely with objects that you implement, remember you can override isEqual: to provide your own equality checking. Usually this is done by comparing fields of your objects.
Every Objective-C object has a method called isEqual:.
http://developer.apple.com/library/mac/#documentation/Cocoa/Reference/Foundation/Protocols/NSObject_Protocol/Reference/NSObject.html#//apple_ref/occ/intfm/NSObject/isEqual:
So you would want to override this for your custom object types.
One particular important note in the documentation:
If two objects are equal, they must
have the same hash value. This last
point is particularly important if you
define isEqual: in a subclass and
intend to put instances of that
subclass into a collection. Make sure
you also define hash in your subclass.
== will compare the pointer, you need to override
- (BOOL)isEqual:(id)anObject
Implement isEqual: and hash
Per the Apple documentation on NSObject you need to implement isEqual: and hash at a minimum. Below you'll find one way to implement object equality, of course how to implement hash enters the land of serious debate here on StackOverflow, but this will work. General rule - you need to define what constitutes object equality and for each unique object they should have a unique hash. It is best practice to add an object specific equality method as well, for example NSString has isEqualToString:.
- (BOOL)isEqual:(id)object
{
BOOL result = NO;
if ([object isKindOfClass:[self class]]) {
CLPObject *otherObject = object;
result = [self.name isEqualToString:[otherObject name]] &&
[self.shortName isEqualToString:[otherObject shortName]] &&
[self.identifier isEqualToString:[otherObject identifier]] &&
self.boardingAllowed == [otherObject isBoardingAllowed];
}
return result;
}
- (NSUInteger)hash
{
NSUInteger result = 1;
NSUInteger prime = 31;
result = prime * result + [_name hash];
result = prime * result + [_shortName hash];
result = prime * result + [_identifier hash];
result = prime * result + _boardingAllowed;
return result;
}