Reputation: 2193
I've been using the block based approach to sort an NSArray
... However, I'd noticed a sort-related bug so started to investigate.
Background: I'm dealing with an NSArray
of EKReminder
objects, which have a creationDate
property. I want to sort the reminders by descending creationDate
(newest reminders, first).
This was my previous code:
// NSArray* fetchedReminders... contents pulled from reminder calendars...
NSArray* sortedArray = [fetchedReminders sortedArrayUsingComparator:^NSComparisonResult(id a, id b) {
NSDate* first = [(EKReminder*)a creationDate];
NSDate* second = [(EKReminder*)b creationDate];
return [second compare:first];
}];
That code, I believe, is correct. However, I ended up with some reminders in the database that had null
as their creation date. This introduced a bug - the resulting sort was incorrect. The null
values were neither at the beginning or the end, and it seems that having nulls in the array messed with this comparison approach, as many of the reminders were out of sequence.
So, I tried swapping out the block-based approach in favour of sortedArrayUsingDescriptors
. Here's the current code:
// NSArray* fetchedReminders... contents pulled from reminder calendars...
NSSortDescriptor* sortDescriptor;
sortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"creationDate" ascending:NO];
NSArray* sortDescriptors = [NSArray arrayWithObject:sortDescriptor];
NSArray* sortedArray = [fetchedReminders sortedArrayUsingDescriptors:sortDescriptors];
This works.
(With the current data, 101 reminders, 6 of them have null
creation dates and they're all placed at the end. The other reminders are in the correct sequence).
First, am I doing anything wrong with the sortedArrayUsingComparator
approach?
If not, is it expected that these different approaches handle null
s differently?
In any case, does that make the NSSortDescriptor
approach the preferred method, if you can potentially have null
s in your data?
Upvotes: 1
Views: 1036
Reputation: 29918
The core issue that you're running into here is that you're not manually handling null values in the block you pass to sortedArrayUsingComparator:
. Depending on which values the block gets called with, first
can be nil
, second
can be nil
, or both can be nil
.
Any message sent to nil
returns the equivalent 0
value (e.g. a method returning a float
, when sent to nil
returns 0.0f
, a method returning int
when sent to nil
returns 0
, and a method returning an object sent to nil
returns nil
). This means that you have the following cases:
[<non-nil> compare:<non-nil>]
(returns a valid value)[<non-nil> compare:nil]
(undefined behavior, as per https://developer.apple.com/library/mac/documentation/Cocoa/Reference/Foundation/Classes/NSDate_Class/#//apple_ref/occ/instm/NSDate/compare:)[nil compare:<non-nil>]
(returns 0; called on nil
)[nil compare:nil]
(returns 0, called on nil
)This means that as the call is being made across the values in the array, some values are being returned that are non-sensical (e.g. [nil compare:[NSDate date]]
returns 0
, equivalent to NSOrderedSame
, which is clearly not true), not to mention the results returned by undefined calls.
In effect, these invalid values are being sorted into strange places in your array. If you had some defined behavior for what should happen if either value is nil
, you'd get consistent behavior.
The following code makes the sorting behavior consistent (and should give you similar behavior to the sort descriptors method above):
NSArray* sortedArray = [fetchedReminders sortedArrayUsingComparator:^NSComparisonResult(id a, id b) {
NSDate* first = [(EKReminder*)a creationDate];
NSDate* second = [(EKReminder*)b creationDate];
if (!first && !second) {
// nils have the same relative ordering
return NSOrderedSame;
} else if (!first) {
// second is not nil; send first toward the end of the array
return NSOrderedDescending;
} else if (!second) {
// first is not nil; send second toward the end of the array
return NSOrderedAscending;
} else {
// Neither is nil; this is valid
return [second compare:first];
}
}];
Upvotes: 3