Objective-C——遍歷Scribble的頂點

出版圈郭志敏發表於2012-03-27

在第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所示。

enter image description here

程式碼清單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的例項進行的迴圈來監控列舉過程。從列舉器得到的每個元素會被傳給指定的塊,以使其中的演算法能夠處理元素。

enter image description here 圖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;
  }
}

相關文章