Objective-C——遍歷Scribble的頂點
在第2章中討論的TouchPainter應用程式中,有一個組合資料結構,容納在螢幕上畫線條用的所有觸控點。這一結構有個抽象型別叫Mark。Stroke對它進行了實現。第13章中對Mark及組合模式進行了詳細討論。Mark定義了包含單個點和由頂點組成的線條的“部分-整體”組合結構(樹)的行為。這個“部分-整體”結構沒有定義與遍歷有關的任何邏輯。因為它是樹,所以它可按不同順序遍歷。如果把任何遍歷行為硬編碼到樹中,以後Mark的介面與實現以及客戶程式碼將面臨大量的改動。同把遍歷策略放到樹結構中相比,更好的辦法是,向Mark新增一個工廠方法(見工廠方法,第4章),讓協議的實現者建立並返回適當的列舉器,對樹做特定順序的遍歷。我們將通過子類化NSEnumerator為Stroke實現一個迭代器(列舉器)。我們把它叫做MarkEnumerator,它對Mark組合樹做後序遍歷。NSEnumerator有兩個抽象方法:allObjects和nextObject。allObjects返回組合體中未被遍歷的Mark例項的陣列,而nextObject返回名冊中的下一個元素。說明這一思想的類圖如圖14-3所示。
開始討論MarkEnumerator之前,先來看一下Mark,這樣會對MarkEnumerator如何遍歷其子節點有更好的理解。我們需要向Mark新增一個叫enumerator的工廠方法,其實現者可通過這個方法返回NSEnumerator的子類(即MarkEnumerator)的例項,如程式碼清單14-1所示。
程式碼清單14-1 Mark.h
@protocol Mark <NSObject>
@property (nonatomic, retain) UIColor *color;
@property (nonatomic, assign) CGFloat size;
@property (nonatomic, assign) CGPoint location;
@property (nonatomic, readonly) NSUInteger count;
@property (nonatomic, readonly) id <Mark> lastChild;
- (void) addMark:(id <Mark>) mark;
- (void) removeMark:(id <Mark>) mark;
- (id <Mark>) childMarkAtIndex:(NSUInteger) index;
- (NSEnumerator *) enumerator;
@end
MarkEnumerator會對Mark樹結構執行後序遍歷,一次列舉一個元素。因此我們打算用棧來實現這個功能。可是,基礎框架中沒有現成的棧類。我們需要自己動手做一個。但在做之前,先來完成MarkEnumerator的類定義。雖然Objective-C中在子類再次宣告過載的方法不是必需的,但這麼做是個好習慣。帶有過載NSEnumerator方法的MarkEnumerator的類定義如程式碼清單14-2所示。
程式碼清單14-2 MarkEnumerator.h
#import "NSMutableArray+Stack.h"
#import "Mark.h"
@interface MarkEnumerator : NSEnumerator
{
@private
NSMutableArray *stack_;
}
- (NSArray *)allObjects;
- (id)nextObject;
@end
根據我們的類設計,MarkEnumerator物件應該由Mark的實現類建立並初始化。同時,MarkEnumerator在建立時需要知道要處理什麼Mark,因此需要在其匿名範疇中定義一個私有的initWithMark:方法,如程式碼清單14-3所示。
程式碼清單14-3 MarkEnumerator+Private.h中宣告私有方法的匿名範疇
@interface MarkEnumerator ()
- (id) initWithMark:(id <Mark>)mark;
- (void) traverseAndBuildStackWithMark:(id <Mark>)mark;
@end
把私有方法放到匿名範疇中的理由是,我們想把它們的實現也放到@implementation主程式碼塊。在這個私有範疇中,我們還定義了另一個用於遍歷Mark組合物件的私有方法。雖然在兩個地方既定義了共有方法又定義了私有方法,但是它們的實現都在同一個.m檔案中,如程式碼清單14-4所示。
程式碼清單14-4 MarkEnumerator.m
#import "MarkEnumerator.h"
#import "MarkEnumerator+Internal.h"
@implementation MarkEnumerator
- (NSArray *)allObjects
{
// 返回還未訪問的Mark節點的陣列
// 也就是棧中的剩餘元素
return [[stack_ reverseObjectEnumerator] allObjects];
}
- (id)nextObject
{
return [stack_ pop];
}
- (void) dealloc
{
[stack_ release];
[super dealloc];
}
#pragma mark -
#pragma mark Private Methods
- (id) initWithMark:(id <Mark>)aMark
{
if (self = [super init])
{
stack_ = [[NSMutableArray alloc] initWithCapacity:[aMark count]];
// 後序遍歷整個Mark聚合體
// 然後把單個Mark加到私有棧中
[self traverseAndBuildStackWithMark:aMark];
}
return self;
}
- (void) traverseAndBuildStackWithMark:(id <Mark>)mark
{
// 把後序遍歷壓入棧中
if (mark == nil) return;
[stack_ push:mark];
NSUInteger index = [mark count];
id <Mark> childMark;
while (childMark = [mark childMarkAtIndex:--index])
{
[self traverseAndBuildStackWithMark:childMark];
}
}
@end
在其私有initWithMark:方法中,它使用傳進來的Mark引用,然後呼叫自己的traverse- AndBuildStackWithMark:(id )mark方法來遍歷aMark。這個方法對自身遞迴呼叫並把mark及其子節點壓入棧中(在棧中構建後序遍歷)。注意,子節點是被反向壓入棧中的(從右至左)。當訪問棧中的元素時,子節點會以原來的順序被取出(從左至右)。現在Mark聚合體中的所有元素都放入了棧中,已做好了準備,等待客戶端向MarkEnumerator傳送來nextObject訊息取得集合中的下一個Mark。
你也許注意到,nextObject方法只有一條語句——return [stack_ pop];。遍歷Mark聚合體之後,最先壓入棧中的元素將最後彈出。所以第一子節點將是nextObject方法返回的第一個元素。父節點會在所有子節點之後返回。allObjects應該返回NSArray的例項,包含未被訪問的元素的集合。因為棧在集合中向前彈出,並且彈出的元素會從棧中刪除,以升序方向返回棧中剩餘元素就剛好合適。
我們藉助棧的幫助遍歷了Mark樹,但是基礎框架中並沒有NSStack這樣的類可供我們使用。因此我們需要利用基礎類中最接近的一個——NSMutableArray,自己做一個棧,如程式碼清單14-5所示。
程式碼清單14-5 NSMutableArray+Stack.h
@interface NSMutableArray (Stack)
- (void) push:(id)object;
- (id) pop;
@end
我們向NSMutableArray增加了兩個方法作為範疇,像真正的棧一樣壓入和彈出物件。它的push方法把物件新增在最後面,而pop方法總是返回並刪除最後一個元素,如程式碼清單14-6所示。
程式碼清單14-6 NSMutableArray+Stack.m
#import "NSMutableArray+Stack.h"
@implementation NSMutableArray (Stack)
- (void) push:(id)object
{
[self addObject:object];
}
- (id) pop
{
if ([self count] == 0) return nil;
id object = [[[self lastObject] retain] autorelease];
[self removeLastObject];
return object;
}
@end
現在我們回到之前離開的Mark家族。Stroke是家族中唯一一個物件含有子節點的成員,因此它實現了enumerator方法,而家族中其他成員沒有。簡潔起見,省去了其他成員。Stroke的enumerator方法只是建立一個MarkEnumerator例項並用self為引數進行初始化,然後返回這個例項,如程式碼清單14-7所示。
程式碼清單14-7 Stroke.m
#import "Stroke.h"
#import "MarkEnumerator+Internal.h"
@implementation Stroke
@synthesize color=color_, size=size_;
- (id) init
{
if (self = [super init])
{
children_ = [[NSMutableArray alloc] initWithCapacity:5];
}
return self;
}
- (void) setLocation:(CGPoint)aPoint
{
// 不設定任何位置
}
- (CGPoint) location
{
// 返回第一個子節點的位置
if ([children_ count] > 0)
{
return [[children_ objectAtIndex:0] location];
}
// 否則,返回原點
return CGPointZero;
}
- (void) addMark:(id <Mark>) mark
{
[children_ addObject:mark];
}
- (void) removeMark:(id <Mark>) mark
{
// 如果mark在這一層,將其移除並返回
// 否則,讓每個子節點去找它
if ([children_ containsObject:mark])
{
[children_ removeObject:mark];
}
else
{
[children_ makeObjectsPerformSelector:@selector(removeMark:)
withObject:mark];
}
}
- (id <Mark>) childMarkAtIndex:(NSUInteger) index
{
if (index >= [children_ count]) return nil;
return [children_ objectAtIndex:index];
}
// 返回最後子節點的便利方法
- (id <Mark>) lastChild
{
return [children_ lastObject];
}
// 返回子節點數
- (NSUInteger) count
{
return [children_ count];
}
- (void) dealloc
{
[color_ release];
[children_ release];
[super dealloc];
}
#pragma mark -
#pragma mark enumerator method
- (NSEnumerator *) enumerator
{
return [[[MarkEnumerator alloc] initWithMark:self] autorelease];
}
@end
因為enumerator是工廠方法,所以它可以返回不同的MarkEnumerator子類的物件而無需修改客戶程式碼。如果想要工廠方法支援不同的遍歷方式,可以增加一個引數指定遍歷型別,以便在執行時選擇不同的MarkEnumerator。
說明:在遍歷時修改聚合體物件可能有危險。如果向聚合體新增或從聚合體刪除了元素,可能導致對一個元素訪問兩次或完全漏掉一個元素。簡單的辦法是對聚合體進行一個深複製,再對副本進行遍歷,但是如果建立與儲存聚合體的另一個副本可能影響效能,代價就比較大。 有很多方法可以實現不受元素插入與刪除影響的迭代器。大部分依靠向聚合體註冊迭代器。一種實現方法是在插入與刪除操作時,聚合體或者調整由它生成的迭代器的內部狀態,或者在內部維護資訊,以保證正確的遍歷。
遍歷Scribble的頂點(內部迭代器)
到目前為止所討論的迭代器(列舉器)是個外部迭代器,就是迭代器模式的原始描述中的那種。它需要客戶端請求集合物件返回其迭代器,然後通過迭代器做迴圈,訪問其每個元素。客戶端在迴圈中每次發一個nextObject訊息給迭代器,取出一個元素,直到集合取光為止。
可以不讓客戶端直接使用任何迭代器(列舉器),實現一個內部迭代器作為另一種方式。內部或被動迭代器(通常就是集合物件本身)控制迭代。可以讓客戶端向內部迭代器提供某種回撥機制,讓它準備好之後返回集合中的下一個元素,來實現內部迭代器。
從iOS SDK 4.0開始,可以在應用程式的開發中使用Objective-C的塊。塊是一種函式型別。定義好後,便可在程式中任何地方複用。塊在很多方面比C語言的函式指標功能更強大。在Objective-C用塊來實現內部迭代器是自然而然的選擇。
圖14-4中的類圖顯示了Mark家族的內部迭代器的一種可能的實現方式。
我們向Mark新增了一個新方法:enumerateMarksUsingBlock:markEnumerationBlock。客戶端會提供一個定義好的塊作為引數,其簽名為^(id item, BOOL *stop)。塊定義了處理從內部迭代器返回的每個Mark元素的演算法。如果演算法想要在當前位置停止列舉,它可以把*stop變數設為YES。正如前面附表中討論的那樣,是組合物件本身而不是客戶端在維護內部迭代器。enumerateMarksUsingBlock:方法使用通過MarkEnumerator的例項進行的迴圈來監控列舉過程。從列舉器得到的每個元素會被傳給指定的塊,以使其中的演算法能夠處理元素。
圖14-4 MarkEnumerator和Stroke的修改版本的類圖。通過引入MarkEnumerationBlock實現了內部迭代器
如果想讓客戶端只能使用內部迭代器,那麼可以將返回MarkEnumerator例項的enumerator工廠方法放到匿名範疇中,就像程式碼清單14-3中MarkEnumerator類那樣。這完全取決於設計。
程式碼清單14-8和程式碼清單14-9中的程式碼段顯示了對Mark協議以及Stroke類的修改。
程式碼清單14-8 Mark.h
@protocol Mark <NSObject>
@property (nonatomic, retain) UIColor *color;
@property (nonatomic, assign) CGFloat size;
@property (nonatomic, assign) CGPoint location;
@property (nonatomic, readonly) NSUInteger count;
@property (nonatomic, readonly) id <Mark> lastChild;
- (void) addMark:(id <Mark>) mark;
- (void) removeMark:(id <Mark>) mark;
- (id <Mark>) childMarkAtIndex:(NSUInteger) index;
- (NSEnumerator *) enumerator;
// 用於實現內部迭代器
- (void) enumerateMarksUsingBlock:(void (^)(id <Mark> item, BOOL *stop)) block;
@end
Stroke中對enumerateMarksUsingBlock:(void (^)(id item, BOOL *stop)) block的實現如程式碼清單14-9所示。
程式碼清單14-9 Stroke.m中enumerateMarksUsingBlock:方法的實現
- (void) enumerateMarksUsingBlock:(void (^)(id <Mark> item, BOOL *stop)) block
{
BOOL stop = NO;
NSEnumerator *enumerator = [self enumerator];
for (id <Mark> mark in enumerator)
{
block (mark, &stop);
if (stop)
break;
}
}
相關文章
- jquery遍歷節點jQuery
- jQuery DOM節點的遍歷jQuery
- Python字典的遍歷,包括key遍歷/value遍歷/item遍歷/Python
- DOM 節點遍歷:掌握遍歷 XML文件結構和內容的技巧XML
- 面試題目-遍歷,點選面試題
- js的map遍歷和array遍歷JS
- 【Codeforces Round 362 (Div 2)D】【樹的遍歷 概率均分思想】Puzzles 兄弟節點的等概率遍歷下 樹的遍歷每點期望時間戳時間戳
- 二叉樹的建立、前序遍歷、中序遍歷、後序遍歷二叉樹
- jQuery遍歷函式,javascript中的each遍歷jQuery函式JavaScript
- 建立二叉樹:層次遍歷--樹的寬度高度,後序遍歷--祖先節點二叉樹
- jQuery的遍歷結構設計之遍歷同胞jQuery
- jQuery的遍歷結構設計之遍歷祖先jQuery
- 圖的遍歷演算法-馬遍歷棋盤演算法
- 【筆記】jQuery原始碼(節點遍歷)筆記jQuery原始碼
- 二叉樹的廣度遍歷和深度遍歷()二叉樹
- 二叉樹建立,前序遍歷,中序遍歷,後序遍歷 思路二叉樹
- JS中的遍歷JS
- DOM元素的遍歷
- JavaScript 中的遍歷JavaScript
- 樹的遍歷方式
- Collection集合的遍歷
- JS 物件的遍歷JS物件
- 層序遍歷樹的節點,佇列實現佇列
- 遍歷 ES 節點校驗分詞(qbit)分詞
- jQuery 遍歷jQuery
- 遍歷 FlowDocument
- Javascript樹(一):廣度遍歷和深度遍歷JavaScript
- 二叉樹的遍歷 → 不用遞迴,還能遍歷嗎二叉樹遞迴
- Matlab對資料夾的層次遍歷和深度遍歷Matlab
- JS遍歷物件的方式JS物件
- 微信小程式的遍歷微信小程式
- 深入JS物件的遍歷JS物件
- 集合index by 的遍歷方法Index
- 資料結構與演算法——二叉樹的前序遍歷,中序遍歷,後序遍歷資料結構演算法二叉樹
- jmeter_遍歷轉換浮點時間戳JMeter時間戳
- 陣列遍歷陣列
- 資料遍歷
- jQuery 遍歷方法jQuery