原文地址https://github.com/scutdavy/nil/blob/master/cocoa_collections.md TL;DR; 集合用来表示一对多的关系。
回忆一下大学时候的核心基础课程:数据结构。老早就有人说数据结构是计算机科学本科最重要的课程,没有之一。也有国外的编程大牛认为,设计良好的数据结构,算法就是自然而然的东西了。
那么我们学过的数据结构包括什么呢?数组,链表,队列,堆栈,哈希表,二叉树,有集合么?
然后的编程工作就是苦逼的用 C\C++ 实现这些概念~
加一句吐槽:当前国内的 C++ 教育方向完全错了,入门教育应该站在更高的抽象层级之上,嗯,传说中的(站在巨人的 JB 上);嗯嗯,如果让我来选择,我选择 C 语言加 smalltalk。
##Foundation 提供的类型 然后看看 cocoa 为我们提供了神马可以利用的东西。
嗯,应该就是这么多:)
首先,cocoa 提供的很多数据类型是有 imutable 和 mutable 版本的区分的,像 NSString, NSNumber 根本就木有 mutable 的版本。最开始学习使用 cocoa 的时候,觉得这种东西分明是给自己找别扭。后来代码敲的多了,学习更加深入的时候才觉得,这可是 cocoa 设计的一大亮点。嗯嗯,水果公司到底还是聚集了众多的天才。使用 imutable 版本的对象
实际上完全使用 immutable 对象进行编程也是可以实现的,例如
NSArray *array = @[@1, @2];
array = [array arrayByAddingObject:@3];
这种就属于 cocoa 的函数式编程风格,数组对象是不变的,如果需要改变数组,就创建一个新的数组对象。类似于:
NSNumber *sum = @1;
sum = @( [sum integerValue] + 1);
然后在看这些个集合类,NSString 就先不说了,cocoa 编程还没见过把 NSString 当作集合操作的情景;indexSet 和 indexPath 是和 NSArray 合起来使用的。orderedSet 应该还是有点用的,不过需要在 iOS5.0 以后使用,嗯嗯,还是淡定吧。
嗯嗯,这样说下来,主要就三个集合类型 NSArray, NSDictionary, NSSet;其实说起来,array 和 set 的功能都可以由 dictionary 来实现的,嗯 lua 其实就是这么干的。
当然了,正常人都不会这么干。抛出去性能方面的考量(大多数时候都不是事),使用特定的类型能更明确的表达设计意图。嗯,就是说:
NSArray
NSDictionary
NSSet
嗯,平时用的最多的也就是前两者,嗯,大学的数据结构知识基本没用上。。。
##集合上的操作
对于 mutable 版本的类型,加两条
这几种操作里面,我对 cocoa api 设计不满意的就是枚举接口的设计。苹果的工程师似乎对 smalltalk 的集合 api 设计并不感冒。即使很多年以前,c 语言还没有 block 这么个东西,但是 ios4.0 之后的接口有了一点意思,但是还觉得差上那么一点点。
##集合的枚举操作
为了继续下面的笔记,虚构一个需求先:假设有一个数组
NSArray *array = @[@1, @2, @3, @4, @5];
要求依次打印每一个数值~
//C程序员做法:
for (NSInteger i = 0; i < [array count]; i++) {
NSLog(@"%@", array[i]);
}
//2B程序员做法:
NSEnumerator *enumerator = [array objectEnumerator];
NSNumber *obj = nil;
while (obj == [enumerator nextObject]) {
NSLog(@"%@", obj);
}
//普通程序员:
for (NSNumber *obj in array) {
NSLog(@"%@", obj);
}
//文艺程序员:
[array enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
NSLog(@"%@", obj);
}];
C 程序员的做法是 C 编程里面基本的“模式”,笔者在做 C 程序员的时候写这东西基本上已经内化为本能了,不过做 iOS 开发之后就很少这么玩了。其实在这个“模式”里面还是有很多讲究的,比如在 for 语句内声明变量 i, 从 0 开始计数,半开半闭合区间等等。但是在这里面有一个本质的不同,cocoa 的数组不再是 C 数组,这种形式显然没有 C 语言中的那么高效,水果公司的文档也不建议采用这种方式。另外这种方式似乎没法遍历 NSDictionary 和 NSSet.
第二种做法显然更面向对象,标准的外部迭代器用法;之所以说是 2B 写法,是因为实现同样的功能,多了一倍的代码,多了两个局部变量。。。类似于拒绝采用 ARC 的 cocoa 程序员,多写了好多的 autorelease, 还有种莫名的优越感。。。
第三种是 apple 推荐的用法,据说是最快的枚举方法。这种东西本质上应该是第二种做法的语法糖。有人对块糖表示过异议,认为这只是实现了一种很有局限性的操作,却增加了语言复杂度。理论上说水果公司应该提供更好的对 block 的支持,然后用库来实现更好的遍历方式。
第四种就是上面所说的做法了,看起来功能上跟第三种快速枚举没差别。不过如果需求要求打印序数,文艺程序员就有优越感了,明显不需要多声明一个变量~
下面需求变了,恩,需求又变了。。。
要求对数组求和,下面只给出快速枚举和基于 block 的枚举方式
//快速枚举
NSInteger sum = 0;
for (NSNumber *obj in array) {
sum += [obj integerValue];
}
//文艺枚举
__block NSInteger sum = 0;
[array enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
sum += [obj integerValue];
}];
这下普通程序员乐了,很简单的一个累加求和,文艺用法偏偏还得加个__block
,要多丑有多丑,文艺变 2B 了。。。
毛主席教导我们说:当文艺显着有些 2B,就说明文艺的还不够,应该沿着文艺的道路继续走下去~
首先实现一个基于NSArray
的扩展:
typedef id(^AccumulationBlock)(id sum, id obj);
@interface NSArray (BlockKit)
- (id)reduce:(id)initial withBlock:(AccumulationBlock)block;
@end
@implementation NSArray (BlockKit)
- (id)reduce:(id)initial withBlock:(AccumulationBlock)block {
__block id result = initial;
[self enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
result = block(result, obj);
}];
return result;
}
@end
实现的细节并不是很重要,恩,丑陋的细节被封装起来了。。。
于是文艺版本的求和变成了这个样子:
NSInteger sum = [[array reduce:@0 withBlock:^id(id sum, id obj) {
return @( [sum integerValue] + [obj integerValue] );
}] integerValue];
这种做法的优势是什么?计算过程不需要局部变量参与,把那个丑陋的__block NSInteger sum
封装到数组的 reduce 方法里面去了。方法的调用者只需要关注于各个元素怎么“加和”到一起就行了。
类似的还可以这么用:
NSArray *mappedArray = [array reduce:@[] withBlock:^id(id sum, id obj) {
return [sum arrayByAddingObject:@( [obj integerValue] * 2)];
}];
这个 reduce 方法实现的是把原数组的每个整数加倍,然后形成一个新数组。为了更好的抽象和更好的表达设计意图,一般都是再加多一个扩展,然后可以这样写:
NSArray *douleArray = [array map:^id(id obj) {
return @( [obj integerValue] * 2 );
}];
这样有更明确的表意,更加的简单,编写代码的时只需要指名,怎样映射就好了。就是说通用逻辑由库实现,应用程序员可以只关注业务逻辑。很遗憾,苹果那些家伙并没有实现这种方法,不过没关系,我们可以用catagory
和block
构造出我们想要的东西。嗯嗯
如果可以像苹果的工程师提意见,我最想要的 foundation 结合支持的方法是reduce,map, select, reject, match
。。。
当然了,如果他们能参考一下 Ruby 和 smalltalk 的集合实现,并且还能给 objective-c 加上 mixin 的官方支持。。。好吧,我想多了。
##函数 style
其实 cocoa 编程也可以从函数式编程中汲取一些精华的东西。
什么是函数式编程呢,我也不是很清楚。但是我记住了函数式编程的两个基本特性:
函数式编程和面向对象编程应该是正交的概念,显然我们可以利用上面两条基本特性来让代码质量变得更好,程序员的生活也变得更好。
第一条,有了高阶函数的支持,我们就可以构造出类似于 map, reduce 之类的抽象
第二条和前面说的 mutability 有某种微妙的联系,感脚上有点强加限制的意思。不过貌似计算机科学上每每做一些减法之后,总是能获得超乎寻常的能力。
说了这么多玄而又玄的东西,那和 cocoa 的集合有毛线关系呢?
我们之前说的 reduce,map 方法都是没有副作用的,他们看起来像数学中的函数类似,map 方法无论你调用多少遍,都不会对数组对象产生神马影响。他们只是默默的产生新的数据对象。
但是到目前为止,使用这些方法并没有看出来比 apple 推荐的快速枚举有更多的好处,除了节省了一个局部变量,除了看起来比较文艺~
好吧,继续虚构一个需求:
对于 twitter 的一个请求:
http://search.twitter.com/search.json?q=@SoundCloud&rpp=100
对于返回的 tweet 数组,过滤掉非 dictionary 类型,过滤掉英文内容,然后得到一个只包含所有推文的数组~
NSArray *tweets = [json valueForKey:@"results"];
NSArray *result = [[[tweets select:^BOOL(id obj) {
return [obj isKindOfClass:[NSDictionary class]];
}] reject:^BOOL(NSDictionary *tweet) {
return [tweet[@"iso_language_code"] isEqualToString:@"en"];
}] map:^id(NSDictionary *tweet) {
return tweet[@"text"];
}];
恩,文艺么?我们大概可以想象一下全部用快速枚举还实现,会是怎样的一坨东西?这些串联起来的高阶方法,明显更加容易读懂,更加容易维护。(不过用 ruby 的家伙肯定说,这太啰嗦了,ruby 单行就搞定。。。恩,不过语法单位上看是一样的)
这里面就有一种类似于流水线的概念,原始数据拿过来,然后经过各种工序,得到想要的结果。嗯嗯,objc 编程的函数式 style,很文艺有没有?
##实现
BlockKit 覆盖的比较全面,但是太全面了,对集合的支持只是一部分。不支持 iOS4.3,嗯嗯,你懂的
Underscore 点语法的粉丝适用,不过使用前要先转换成新类型,有点脱了裤子放屁的感脚
DMCollectionKit 我自己的项目,暂时只实现了基于NSArray
的扩展,感觉上基本能满足我的需求了
或者自己实现一套,满足自己的需求,其实很简单的
前两个项目有实现类似 each 的方法,建议弃用,除了文艺,似乎没有更好的优势。如果你觉得需要用 each,那么用 objc 的快速枚举似乎是更好的选择,恩,糟糕选择中的最好选择。