从 C++ 到 Objective-C(19):STL 和 Cocoa

C++ 标准库是其强大的一个原因。即使它还有一些不足,但是已经能够算作是比较完备的了。这并不是语言的一部分,而是属于一种扩展,其他语言也有类似的部分。在 Objective-C 中,你不得不在 Cocoa 里面寻找容器、遍历器或者其他一些真正可以使用的算法。

容器

Cocoa 的容器比 C++ 更加面向对象,它不使用模板实现,只能存放对象。现在可用的容器有:

  • NSArrayNSMutableArray:有序集合;
  • NSSetNSMutableSet:无序集合;
  • NSDictionaryNSMutableDictionary:键值对形式的关联集合;
  • NSHashTable:使用弱引用的散列表(Objective-C 2.0 新增)。

你可能会发现这其中并没有 NSList 或者 NSQueue。事实上,这些容器都可以由NSArray实现。

不同于 C++ 的vector<T>,Objective-C 的NSArray真正隐藏了它的内部实现,仅能够使用访问器获取其内容。因此,NSArray没有义务为内存单元优化其内容。NSArray的实现有一些妥协,以便NSArray能够像数组或者列表一样使用。既然 Objective-C 的容器只能存放指针,单元维护就会比较有效率了。

NSHashTable等价于NSSet,但它使用的是弱引用(我们曾在前面的章节中讲到过)。这对于垃圾收集器很有帮助。

遍历器

经典的枚举

纯面向对象的实现让 Objective-C 比 C++ 更容易实现遍历器。NSEnumerator就是为了这个设计的:

NSArray* array = [NSArray arrayWithObjects:object1, object2, object3, nil];
NSEnumerator* enumerator = [array objectEnumerator];
NSString* aString = @"foo";
id anObject = [enumerator nextObject];
while (anObject != nil)
{
    [anObject doSomethingWithString:aString];
    anObject = [enumerator nextObject];
}

容器的objectEnumerator方法返回一个遍历器。遍历器可以使用nextObject移动自己。这种行为更像 Java 而不是 C++。当遍历器到达容器末尾时,nextObject返回nil。下面是最普通的使用遍历器的语法,使用的 C 语言风格的简写:

NSArray* array = [NSArray arrayWithObjects:object1, object2, object3, nil];
NSEnumerator* enumerator = [array objectEnumerator];
NSString* aString = @"foo";
id anObject = nil;
while ((anObject = [enumerator nextObject])) {
    [anObject doSomethingWithString:aString];
}
// 双括号能够防止 gcc 发出警告

快速枚举

Objective-C 2.0 提供了一个使用遍历器的新语法,隐式使用NSEnumerator(其实和一般的NSEnumerator没有什么区别)。它的具体形式是:

NSArray* someContainer = ...;
for(id object in someContainer) { // 每一个对象都是用 id 类型
    ...
}
for(NSString* object in someContainer) { // 每一个对象都是 NSString
    ...// 开发人员需要处理不是 NSString* 的情况
}

函数对象

使用选择器

Objective-C 的选择器很强大,因而大大减少了函数对象的使用。事实上,弱类型允许用户无需关心实际类型就可以发送消息。例如,下面的代码同前面使用遍历器的是等价的:

NSArray* array = [NSArray arrayWithObjects:object1, object2, object3, nil];
NSString* aString = @"foo";
[array makeObjectsPerformSelector:@selector(doSomethingWithString:)
                       withObject:aString];

在这段代码中,每个对象不一定非得是NSString类型,并且对象也不需要必须实现了doSomethingWithString:方法(这会引发一个异常:selector not recognized)。

IMP 缓存

我们在这里不会详细解释这个问题,但是的确可以获得 C 函数的内存地址。通过仅查找一次函数地址,可以优化同一个选择器的多次调用。这被称为 IMP 缓存,因为 Objective-C 用于方法实现的数据类型就是 IMP。

调用class_getMethodImplementation()就可以获得这么一个指针。但是请注意,这是指向实现方法的真实的指针,因此不能有虚调用。它的使用一般在需要很好的时间优化的场合,并且必须非常小心。

算法

STL 中那一大堆通用算法在 Objective-C 中都没有对等的实现。相反,你应该仔细查找下各个容器中有没有你需要的算法。

Leave a Reply