使用NSArray containObject:方法比较对象

问题是这么来的…

我写了一个类方法返回了一个NSArray

+(NSArray *)validOperator
{
    return @[@"+", @"-", @"*", @"/", @"%", @"(", @")", @"="];
}

然后我想判断某个操作符是否在上面返回的数组中

-(BOOL)isOperator:(NSString *)ch
{
    if ([[Calculate validOperator] containsObject:ch]) {
        return YES;
    }else
        return NO;
}

这时候问题就来了,即使ch在数组中,方法返回的也是NO。

然后我在网上搜索了一下…

然后我在网上搜了一下,大概找到问题的原因了。
苹果的文档对[NSArray containsObject:] 是这么说的:

This method determines whether anObject is present in the receiver by sending an isEqual: message to each of the receiver’s objects (and passing anObject as the parameter to each isEqual: message).

所以问题应该是比较的是对象的引用(地址?),而不是对象的值。
[见补充3]

找到原因了下面就要解决问题了

看到StackOverflow上这个人遇到的问题跟我差不多,他采纳的答案中提到了两种方法,还给了一个关于实现-isEqual:和-hash重载的问题链接

you will either need to send [collection containsObject:] an instance of a variable it contains (e.g. A, B, or C), or you will need to override the [NSObject isEqual:] method in your Fruit class.

最后问题来了

  1. 请问上面那个回答中提到的第一种方法可行吗?我发送的ch原本定义的是char,然后使用了stringWithUTF8String:转化成NSString。
  2. 关于实现-isEqual:和-hash的重载,我看了上面提到的问题链接,有-isEqual:的重载模版,但是-hash的重载还是没说清楚的样子,请问能不能给个有关的例子可以学习一下。

刚接触iOS开发,希望各位路过的好心人能解答一下,万分感谢!

补充1

对了,我也考虑过不使用containObject:的,就是用NSString的isEqualToString方法,把数组里的元素拿出来。

-(BOOL)isOperator:(NSString *)ch
{
    NSArray *optrArray = [Calculate validOperator];  
    for (NSString *optr in optrArray) {
    NSLog(@"optr:%@ ,ch:%@", optr,ch);
    if ([optr isEqualToString:ch]) {
        return YES;
    }
}
    return NO;
}

还是不行(╥﹏╥)

补充2

好吧,我发现我的问题在哪里了,不是containObject:的问题,我要比较的还是NSString的两个对象,所以问题是传递的变量ch上。我发现,如果直接传一个@"+",不管使用containObject:还是用isEqualToString的方法都没问题,但是按下面的代码就有问题了:

//inputString是从textfield获取的一个NSString
for (int i = 0; i < [inputString length]; i++) {   
    char ch = [inputString characterAtIndex:i];
    NSString *str = [NSString stringWithUTF8String:&ch];
    if (![self isOperator:str]) {
    ……
    }
    ……
}

痛定思痛,追根溯源,找到了症结:

char ch = [inputString characterAtIndex:i];
NSString *str = [NSString stringWithUTF8String:&ch];

对,问题就在stringWithUTF8String:这个方法上,暂时先把这个方法放放,把它改成stringWithFormat:的方法如下:

NSString *str = [NSString stringWithFormat:@"%c", ch];

然后,世界终于清静了……containObject:和isEqualToString应该可以随便用,都是NSString没什么好害羞的。今天太晚了,明天等程序写完了再研究下用stringWithUTF8String:是怎么样出错的。至于stringWithUTF8String:,因为它会在str末尾会包含"\n"= =

补充3

2015.1.13

上面那里之前没说清楚(大概自己之前也没好好理解==),楼下有小伙伴指出来了。今天又仔细看了下觉得应该是这样的,首先containsObject:方法的完整定义是:

- (BOOL)containsObject:(id)anObject

苹果给的文档里说的是,当判断anObject是否在当前的NSArray中的时候,是通过调用isEqual:这个方法来判断的,即对于NSArray中的每个对象都会调用一次isEqual:anObject,如果返回YES自然就是说这个数组包含anObject。
那这个时候问题可能就来了,比如说这个问题。究其原因的话还在于isEqual:这个方法。
isEqual:定义

If two objects are equal, they must have the same hash value.

所以isEqual:本质是在比较两个对象的hash。苹果也说了:

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.

之前那个问题就属于这种需要额外注意的情况。
其实这里可以引出一个关于怎么判断相等的问题,鉴于如楼下的小伙伴说的这问题确实是写得太长了不如写博客了==这里就不细细说啦,找到一篇讲得特别棒的文章强烈推荐给大伙儿。
总之,就是在比较两个对象是否相等或者是判断一个对象是否在一个collection里面的时候,就要特别注意:

  • 对于普通的NSObject比较用isEqual:方法比较hash,上面那个文章的Thompson大叔说比较的是内存地址(我觉得挺有道理的不然普通的两个对象还在哪里有可比性==)
  • 对于Foundation里面的NSObject的子类如NSString、NSData、NSDate等(详细地还是看文章哈,反正没有NSArray╮(╯▽╰)╭),有定义相应的判断方法,所以用的话尽量采用子类中的比较方法
  • 对于自己定义的类的话,如果要用到比较,就要重新实现isEqual:和hash方法,实现的方法还有栗子什么Thompson大叔也说了,嗯不然会很容易悲剧的╮(╯_╰)╭

补(tu)充(cao)4

2015.1.13

关于楼下的朋友说的自问自答真的不是故意的==
不过确实是自己把楼歪了,楼会歪的原因是,当初的bug跟提出的问题其实没什么关系…
不过今天又来仔细研究了当初被莫名提出的问题,觉得受益匪浅,嘿嘿:D

阅读 35.1k
6 个回答

关于那两个问题

问题一

原题主的代码是这样的:

NSArray *collection = [[NSArray alloc] initWithObjects:A, B, C, nil]; //A, B, C are custom "Item" objects
Item *tempItem = [[Item alloc] initWithLength:1 width:2 height:3];  
//3 instance variables in "Item" objects
if([collection containsObject:tempItem]) {
    NSLog(@"collection contains this item");
}

方法一:

you will either need to send [collection containsObject:] an instance of a variable it contains (e.g. A, B, or C)
向[collection containsObject:]传collection中包含的实例变量,比如[collection containObject:A]。

感觉这应该是可以的,毕竟如果就是A, B, C本身的话在内存空间的地址应该是一样的。但是这么做的话觉得这代码也没什么意思了,我用A, B, C初始化了一个collection,然后我又来判断A, B, C是不是在这个collection里面==

方法二:

you will need to override the [NSObject isEqual:] method in your Fruit class.

这应该是正解,详见补充3。

问题二

补充3里面NSHipster的文章里面有详细讲到,下面都是节选翻译自动脑补的。

继承自NSObject的子类要Override这个isEqual:方法要做以下几件事:

  • Implement a new isEqualTo__ClassName__: method, which performs the meaningful value comparison.
  • Override isEqual: to make class and object identity checks, falling
    back on the aforementioned value comparison method.
  • Override hash

如果是自己定义的子类:

@interface Person
@property NSString *name;
@property NSDate *birthday;

- (BOOL)isEqualToPerson:(Person *)person;
@end

@implementation Person

- (BOOL)isEqualToPerson:(Person *)person {
  if (!person) {
    return NO;
  }

  BOOL haveEqualNames = (!self.name && !person.name) || [self.name isEqualToString:person.name];
  BOOL haveEqualBirthdays = (!self.birthday && !person.birthday) || [self.birthday isEqualToDate:person.birthday];

  return haveEqualNames && haveEqualBirthdays;
}

#pragma mark - NSObject

- (BOOL)isEqual:(id)object {
  if (self == object) {
    return YES;
  }

  if (![object isKindOfClass:[Person class]]) {
    return NO;
  }

  return [self isEqualToPerson:(Person *)object];
}

- (NSUInteger)hash {
  return [self.name hash] ^ [self.birthday hash];
}

最后,重载hash的时候还有一个trick,即用到了XOR,大叔是这么说的,

In reality, a simple XOR over the hash values of critical properties is sufficient 99% of the time.

嗯,就是对性能优化什么的有好处啦,关于这个他推荐了这篇文章

关于po主的真正Bug

使用stringWithUTF8String:造成比较错误,因为它会在str末尾会包含"\n"。
改成stringWithFormat:的方法即可。
详见补充2.

补充

话说真不是故意自问自答的(答题时间可以证明po主的清白!╮(╯▽╰)╭)

你传过去的参数类型对吗?

楼主,判断iseuqal不是判断指针和地址,是看hash值是不是相同
图片描述

建议楼主去写一篇文章好了。。竟然自问自答 =͟͟͞͞( •̀д•́)

新手上路,请多包涵

我以为是提问回答环节,没想到题主 自我解决了问题.还非常详细!非常好!赞一下

新手上路,请多包涵

containObject比较字符串,实际是看字符串的hash,相同的字符串虽然对象存储地址不同,但hash相同,所以可以直接查找

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题
宣传栏