如何对包含自定义对象的NSMutableArray进行排序?

1324

我想做的事情似乎很简单,但是在网上找不到任何答案。我有一个包含对象的NSMutableArray,假设它们是“Person”对象。我想按Person.birthDate(一个NSDate)对NSMutableArray进行排序。

我认为与此方法有关:

NSArray *sortedArray = [drinkDetails sortedArrayUsingSelector:@selector(???)];

在Java中,我会让我的对象实现Comparable接口,或者使用带有内联自定义比较器的Collections.sort()方法...但在Objective-C中该怎么做呢?

27个回答

2347

比较方法

您可以为您的对象实现一个比较方法:

- (NSComparisonResult)compare:(Person *)otherObject {
    return [self.birthDate compare:otherObject.birthDate];
}

NSArray *sortedArray = [drinkDetails sortedArrayUsingSelector:@selector(compare:)];

NSSortDescriptor(更好的选择)

或者通常情况下更优:

NSSortDescriptor *sortDescriptor;
sortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"birthDate"
                                           ascending:YES];
NSArray *sortedArray = [drinkDetails sortedArrayUsingDescriptors:@[sortDescriptor]];
您可以通过将多个键添加到数组中轻松地按多个键进行排序。也可以使用自定义比较器方法。请查看文档
还有一种使用块进行排序的可能性,自Mac OS X 10.6和iOS 4以来已经支持:
NSArray *sortedArray;
sortedArray = [drinkDetails sortedArrayUsingComparator:^NSComparisonResult(Person *a, Person *b) {
    return [a.birthDate compare:b.birthDate];
}];

性能

-compare: 方法和基于块的方法通常比使用 NSSortDescriptor 更快,因为后者依赖于 KVC。而使用 NSSortDescriptor 的主要优点是它提供了一种使用数据来定义排序顺序的方式,而不是通过代码。这使得例如设置用户可以通过点击标题行对 NSTableView 进行排序变得容易。


69
第一个示例有一个错误:你将一个对象的birthDate实例变量与另一个对象本身进行比较,而不是它的birthDate变量。 - Martin Gjaldbaek
93
@Martin:谢谢!有趣的是在我获得75个赞之前,没有其他人注意到它。 Translated: @Martin: 谢谢!有趣的是在我得到75个赞之前,没有其他人注意到这件事。 - Georg Schölly
79
因为这是被大多数用户认可的最终答案,所以可能会有帮助,如果添加第三个基于块的示例,以便用户知道它也存在。 - jpswain
6
@orange80: 我试过了。我不再拥有一台 Mac 了,所以如果你能看一下这段代码就太好了。 - Georg Schölly
11
如果您有一个 NSMutableArray,我更喜欢使用方法 sortUsingDescriptorssortUsingFunctionsortUsingSelector 进行排序。由于数组是可变的,通常我不需要一个已排序的副本。 - Stephan
显示剩余23条评论

113

请查看NSMutableArray方法sortUsingFunction:context:

你需要设置一个compare函数,该函数接受两个对象 (类型为Person, 因为你要比较两个Person对象) 和一个context参数。

这两个对象只是Person的实例。第三个对象是一个字符串,例如@"birthDate"。

此函数返回一个NSComparisonResult:如果PersonA.birthDate < PersonB.birthDate,则返回NSOrderedAscending;如果PersonA.birthDate > PersonB.birthDate,则返回NSOrderedDescending;最后,如果PersonA.birthDate == PersonB.birthDate,则返回NSOrderedSame

这是粗略的伪代码;你需要详细说明一个日期是如何比较大小的(例如比较从纪元开始算起的秒数等):

NSComparisonResult compare(Person *firstPerson, Person *secondPerson, void *context) {
  if ([firstPerson birthDate] < [secondPerson birthDate])
    return NSOrderedAscending;
  else if ([firstPerson birthDate] > [secondPerson birthDate])
    return NSOrderedDescending;
  else 
    return NSOrderedSame;
}

如果你想要更紧凑的代码,可以使用三元运算符:

NSComparisonResult compare(Person *firstPerson, Person *secondPerson, void *context) {
  return ([firstPerson birthDate] < [secondPerson birthDate]) ? NSOrderedAscending : ([firstPerson birthDate] > [secondPerson birthDate]) ? NSOrderedDescending : NSOrderedSame;
}

如果您经常这样做,使用内联可能会稍微加快速度。


9
使用sortUsingFunction:context:可能是最C语言的方式,也肯定是最难懂的一种。 - Georg Schölly
12
这并没有什么大问题,但我认为现在有更好的替代选择。 - Georg Schölly
6
也许是这样,但我认为对于一个Java背景的人来说,并不会因为寻找类似于Java的抽象比较器类(abstract Comparator class),它实现了compare(Type obj1, Type obj2)方法而难以理解。 - Alex Reynolds
5
我感觉你们中有几个人正在寻找任何理由来批评这个完全正确的答案,即使那些批评在技术上没有太多的实质性价值。很奇怪。 - Alex Reynolds
1
@Yar:你可以使用我在第一段提供的解决方案,或者您可以使用多个排序描述符。sortedArrayUsingDescriptors: 接受一个排序描述符数组作为参数。 - Georg Schölly
显示剩余4条评论

66

我在iOS 4中使用一个block实现了这个操作。需要将数组元素从id类型转换为我的类类型。在这种情况下,它是一个名为Score的类,其中包含一个名为points的属性。

此外,如果您的数组元素不是正确的类型,您需要决定要做什么。对于这个示例,我只返回了NSOrderedSame,但在我的代码中,我抛出了异常。

NSArray *sorted = [_scores sortedArrayUsingComparator:^(id obj1, id obj2){
    if ([obj1 isKindOfClass:[Score class]] && [obj2 isKindOfClass:[Score class]]) {
        Score *s1 = obj1;
        Score *s2 = obj2;

        if (s1.points > s2.points) {
            return (NSComparisonResult)NSOrderedAscending;
        } else if (s1.points < s2.points) {
            return (NSComparisonResult)NSOrderedDescending;
        }
    }

    // TODO: default is the same?
    return (NSComparisonResult)NSOrderedSame;
}];

return sorted;

PS:这是按降序排列。


7
你其实不需要在这里使用"(Score *)"转换,你可以直接写成"Score *s1 = obj1;"因为id类型可以自由转换为其他类型而不会引发编译器警告 :-) - jpswain
正确的 downcasting 不需要在弱变量之前进行转换。 - geekay
你应该始终将 nil 和非 nil 排序到顶部或底部,因此默认的结束返回可能是 return ((!obj1 && !obj2) ? NSOrderedSame : (obj1 ? NSOrderedAscending : NSOrderedDescending)) - Scott Corscadden
嗨Chris,我尝试了这段代码,我的程序中有一个刷新功能。第一次正确执行,输出降序排列。但是当我刷新(使用相同的数据再次执行相同的代码)时,它改变了顺序,不再是降序了。假设我的数组中有4个对象,其中3个具有相同的数据,1个不同。 - Nikesh K
如果您确实希望排序那些不属于“Score”类的对象,您需要更加小心地进行排序。否则,您会出现其他 == score1 < score2 == 其他这种不一致的情况,可能会引起麻烦。您可以返回一个值,该值意味着Score对象在所有其他对象之前排序,并且所有其他对象相互排序相等。 - gnasher729

31

我尝试了所有方法,但这个对我起作用。在一个类中,我有另一个名为"crimeScene"的类,并想按"crimeScene"的属性进行排序。

这个方法非常有效:

NSSortDescriptor *sorter = [[NSSortDescriptor alloc] initWithKey:@"crimeScene.distance" ascending:YES];
[self.arrAnnotations sortUsingDescriptors:[NSArray arrayWithObject:sorter]];

30

从iOS 4开始,您还可以使用块进行排序。

针对此特定示例,我假设数组中的对象具有“position”方法,该方法返回一个NSInteger

NSArray *arrayToSort = where ever you get the array from... ;
NSComparisonResult (^sortBlock)(id, id) = ^(id obj1, id obj2) 
{
    if ([obj1 position] > [obj2 position]) 
    { 
        return (NSComparisonResult)NSOrderedDescending;
    }
    if ([obj1 position] < [obj2 position]) 
    {
        return (NSComparisonResult)NSOrderedAscending;
    }
    return (NSComparisonResult)NSOrderedSame;
};
NSArray *sorted = [arrayToSort sortedArrayUsingComparator:sortBlock];

注意:"sorted"数组将会自动释放。


28

Georg Schölly的第二个答案中缺少了一步,但之后它可以正常工作。

NSSortDescriptor *sortDescriptor;
sortDescriptor = [[[NSSortDescriptor alloc] initWithKey:@"birthDate"
                                              ascending:YES] autorelease];
NSArray *sortDescriptors = [NSArray arrayWithObject:sortDescriptor];
NSArray *sortedArray;
sortedArray = [drinkDetails sortedArrayUsingDescriptors:sortDescriptors];

//添加了“s”,因为当我复制和粘贴时,如果不加上“s”,在sortedArrayUsingDescriptors中会浪费时间并失败


方法调用实际上是 "sortedArrayUsingDescriptors:",末尾带有 's'。 - CIFilter

21
NSSortDescriptor *sortDescriptor;
sortDescriptor = [[[NSSortDescriptor alloc] initWithKey:@"birthDate" ascending:YES] autorelease];
NSArray *sortDescriptors = [NSArray arrayWithObject:sortDescriptor];
NSArray *sortedArray;
sortedArray = [drinkDetails sortedArrayUsingDescriptors:sortDescriptors];

谢谢,它可以正常工作了...


19
您的 Person 对象需要实现一个方法,比如说 compare: 方法,它接受另一个 Person 对象作为参数,根据两个对象之间的关系返回 NSComparisonResult
然后您可以使用 sortedArrayUsingSelector: 并传入 @selector(compare:),就完成了排序操作。
虽然有其他方式可以实现,但据我所知,在 Cocoa 中没有类似于 Java 中 Comparable 接口的实现。通过使用 sortedArrayUsingSelector: 可能是最简单的方法。

12

iOS 4的块会救你的 :)

featuresArray = [[unsortedFeaturesArray sortedArrayUsingComparator: ^(id a, id b)  
{
    DMSeatFeature *first = ( DMSeatFeature* ) a;
    DMSeatFeature *second = ( DMSeatFeature* ) b;

    if ( first.quality == second.quality )
        return NSOrderedSame;
    else
    {
        if ( eSeatQualityGreen  == m_seatQuality || eSeatQualityYellowGreen == m_seatQuality || eSeatQualityDefault  == m_seatQuality )
        {
            if ( first.quality < second.quality )
                return NSOrderedAscending;
            else
                return NSOrderedDescending;
        }
        else // eSeatQualityRed || eSeatQualityYellow
        {
            if ( first.quality > second.quality )
                return NSOrderedAscending;
            else
                return NSOrderedDescending;
        } 
    }
}] retain];

http://sokol8.blogspot.com/2011/04/sorting-nsarray-with-blocks.html 一些描述


10
对于NSMutableArray,请使用sortUsingSelector方法。它原地排序(in-place),而不创建新实例。

仅供更新:我也在寻找原地对可变数组排序的方法,现在iOS 7提供了所有“sortedArrayUsing”方法的“sortUsing”等效方法,例如sortUsingComparator: - jmathew

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接