-

一、isEqual 踩坑

isEqual 其实没啥坑的,是我使用的时候发现了个使用的误区。

起因是在给同事提供接口判断一个对象的内容是否发生了变化,其实很简单,假设BNObject中有3个string:

1
2
3
4
5
6
7
@interface BNObject : NSObject

@property (nonatomic, copy) NSString *stringA;
@property (nonatomic, copy) NSString *stringB;
@property (nonatomic, copy) NSString *stringC;

@end

那么提供的isEqual:接口是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
- (BOOL)isEqual:(id)object {
if (object == self) {
return YES;
} else if (![object isKindOfClass:[BNObject class]]) {
return NO;
} else {
BNObject *objc = (BNObject *)object;
BOOL stringAEqual = [self.stringA isEqualToString:objc.stringA];
BOOL stringBEqual = [self.stringB isEqualToString:objc.stringB];
BOOL stringCEqual = [self.stringC isEqualToString:objc.stringC];

return stringAEqual && stringBEqual && stringCEqual;
}
}

这个 isEqual:方法其实是有问题的,如果 self.stringAobjc.stringA 都是nil,那么应该判定为 Equal,但因为通过通过 nil 获取的 isEqualToString 默认是 NO,所以只采用[A isEqualToString:B]的判定方式,即使A和B都是nil,得到的结果也是NO。

正确的修正方式为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
- (BOOL)isEqual:(id)object {
if (object == self) {
return YES;
} else if (![object isKindOfClass:[BNObject class]]) {
return NO;
} else {
BNObject *objc = (BNObject *)object;
BOOL stringAEqual = [BNObject stringIsEqual:self.stringA stringB:objc.stringA];
BOOL stringBEqual = [BNObject stringIsEqual:self.stringB stringB:objc.stringB];
BOOL stringCEqual = [BNObject stringIsEqual:self.stringC stringB:objc.stringC];

return stringAEqual && stringBEqual && stringCEqual;
}
}

// 判断两个字符串相等,都为nil也表示相等
+ (BOOL)stringIsEqual:(NSString *)stringA stringB:(NSString *)stringB {
if (stringA.length <= 0 && stringB.length <= 0) {
return YES;
}
return [stringA isEqualToString:stringB];
}

2021年10月30日更新:

使用 yy_modelIsEqual,可以不用对每个字段都写判等条件。

二、for 安全遍历踩坑

这实际上是已经很早之前的bug了,突然想起来也是记录一下。

背景是这样:我需要在遍历一个数组的时候,安全的删掉里面的元素,这是一个简单的面试题。

最初我的写法是这样的:

1
2
3
4
5
6
7
NSMutableArray *array = [NSMutableArray arrayWithArray:@[@"1",@"2",@"3",@"4",@"5"]];
[array enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
if ([obj isEqualToString:@"3"]) {
[array removeObject:obj];
}
}];
NSLog(@"%@",array);

实际上这种写法在每次都删除一个元素时,是正常的,上面这行代码输出的结果为:

1
2
3
4
5
(
1,
4,
5
)

所以我的这种写法是没有安全问题的,只删除一个元素的情况下也是符合预期的。但如果是使用 enumerateObjectsUsingBlock 遍历时,删除了两个元素,就会出现index跳位的情况,甚至会导致crash:

1
2
3
4
5
6
7
8
NSMutableArray *array = [NSMutableArray arrayWithArray:@[@"1",@"2",@"3",@"4",@"5"]];
[array enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
if ([obj isEqualToString:@"4"]) {
[array removeObject:obj];
[array removeObjectAtIndex:(idx + 1)];
}
}];
NSLog(@"%@",array);

所以终究来说,在 enumerateObjectsUsingBlock 遍历去删除元素,不是一个很优雅的做法,最稳妥的处理方式是:把需要删除的内容,筛选出来放到一个NSMutableArray中,然后再把这些需要删除的统一从原始数组中删除。

1
2
3
4
5
6
7
NSMutableArray *discardedItems = [NSMutableArray array];
SomeObjectClass *item;
for (item in originalArrayOfItems) {
if ([item shouldBeDiscarded])
[discardedItems addObject:item];
}
[originalArrayOfItems removeObjectsInArray:discardedItems];

三、json解包问题

这个问题我很早之前也遇到过,最近是一个实习生同学问到的:

有如下一个 json 字符串(层级 >= 2):

1
2
3
4
5
6
7
8
9
10
{
"name": "bninecoding",
"like": {
"sport": "basketball",
"job": "code",
"good": {
"test": "hello"
}
}
}

客户端将 json 转成 NSDictionary 的方法是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
+ (NSDictionary *)dictionaryWithJsonString:(NSString *)jsonString {
if (jsonString == nil) {
return nil;
}

NSData *jsonData = [jsonString dataUsingEncoding:NSUTF8StringEncoding];
NSError *err;
NSDictionary *dic = [NSJSONSerialization JSONObjectWithData:jsonData
options:NSJSONReadingMutableContainers
error:&err];

if(err) {
NSLog(@"json解析失败:%@",err);
return nil;
}

return dic;
}

将 json 转成 NSDictionary 之后可以看到 dictionary 是会按 json 格式递归获取数据的:

但实习生同学问到case是,它使用同样的方式去反解 json ,发现 json 无法递归生成 NSDictionary,建议 yy_model 去解 json串。

当时专家会诊得到的结论是通过 NSJSONSerialization JSONObjectWithData:options:error:不能递归去解json串。

其实得到的结论是错的,上面已经验证了,是可以递归解全json串的,最后看了实习生同学改的代码,发现是因为接口传过来的json格式错误导致的(预期是 NSString,但接口传成了 Dictionary)。

当然了,解 json 串,我还是建议使用 yy_model ,更清晰明了。