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

語言: CN / TW / HK

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 中都沒有對等的實現。相反,你應該仔細查詢下各個容器中有沒有你需要的演算法。