[译文]如何才能不崩溃 #2: Mutation Exceptions

原文地址:

http://inessential.com/2015/05/16/hownottocrash2mutationexceptions

你从某处获得了一个集合并且枚举它 - 然后你得到了一个关于集合在枚举中被改变的错误。然后app崩溃了。

你可以用一种简单的方法避免这种不幸的命运:不枚举可变集合。

不同意我

你可能会认为真正正确的答案是枚举中不去改变一个可变的集合。你应该对你的app有足够的了解,来编写安全的可枚举可变集合的代码。

是的,你应该,你绝对应该。

然而,编写不崩溃的代码就是在移除疑惑的问题。将发生错误的机率降到最低,将未来引入改变而导致崩溃的机率降到最低(通过你或其他人)。

可变集合不应该为公共API

这个应该非常罕见 - 或者,更加,绝不 - 一个对象拥有一个公共的可变集合的属性。可变集合应该放在对象的内部。

(此外,尽可能的让公共的集合都是只读属性。当然,这并不一直都可能。)

现在,一个拥有公共集合属性的对象完全有可能在内部是一个可变集合。追踪一个operations集合,它的公共部分可能像这样:

@property (nonatomic, readonly) NSArray *operations;

它的内部是这样:

@property (nonatomic) NSMutableArray *mutableOperations;

- (NSArray *)operations {
  return self.mutableOperations;
}

这是一个完美的合法代码:因为mutableOperations是一个可变数组(NSMutableArray),它同时也是一个不可变数组(NSArray)。(好多年我都是这么做的,我想:“我是一个成熟开发者,我可以搞定它。”但是我没有意识到的是成熟开发者编写代码出错的可能性较小。)

规定不可变的属性实际上应该是不可变的

在上面的例子中,你认为operations 是一个在任何时刻都可以被安全的枚举的数组。另一个人 - 或者你自己,看着这六个月 - 你没有真正认识到,你实际上拿回的是一个可变数组,它不能安全的被枚举。

下面是推荐的真正的解决方法:

- (NSArray *)operations {
  return [self.mutableOperations copy];
}

(这是一个拷贝,它不会损坏一个可变属性,但是我承认,我并不是一直这么做。它能清楚的让使用者了解这个API发生了什么事情。)

你可能会倒回来,引证性能或者内存的问题,或者两者全部 - 但是我承认这些事情:我是一个性能迷,我使用Instruments花费了一堆不相称的时间去保证程序尽可能的快,并且不耗费大量内存。我从来没有发现这是一个问题。如果你的app有性能或者内存使用的问题,问题应该在其他地方,而不在于这些拷贝。(你可以考虑使用@autoreleasepool来让这些拷贝不至于存活很长时间。)

拷贝吧。

加分点:不要相信他们的谎言

我最近修复了几个突发错误在枚举NSTextStorage layoutManagers的时候:

@property (readonly, copy) NSArray *layoutManagers;

这明显是一个安全的枚举,这是一个不可变数组,它是安全的,并且是一个拷贝。不错。

但这是一个谎言,在debug的时候我发现这是一个NSMutableArray (__NSArrayM) - 并不是一个拷贝。 这是NSTextStorage’s _layoutManagers实例变量,这申明为一个NSMutableArray

在我的枚举block中,有些代码引起了layoutManagers的突变,然后app崩溃了。

答案是:枚举layoutManagers的拷贝。问题解决。

这是一个普通点:如果你从不是你的代码中获取一个集合,枚举一个拷贝并不会破坏它的防御。

Article by 付军