1. 程式人生 > >[ios開發基礎之]程式碼塊 ——block

[ios開發基礎之]程式碼塊 ——block

iOS4引入了一個新特性,支援程式碼塊的使用, 這將從根本上改變你的程式設計方式。程式碼塊是對C語言的一個擴充套件,因此在Objective-C中完全支援。如果你學過Ruby,Python或Lisp程式設計 語言,那麼你肯定知道程式碼塊的強大之處。簡單的說,你可以通過程式碼塊封裝一組程式碼語句並將其當作一個物件。程式碼塊的使用是一種新的編碼風格,可以讓你運用 自如的使用iOS4中新增API。
    我們先來看兩個在iOS4中使用程式碼塊的例子(你很有可能已經見過):view animations 和enumeration
使用程式碼塊的例子
     第一個例子,假設我們建立一個紙牌遊戲,需要展現紙牌被派發到玩家面前的動畫

效果。幸運的是通過UIKit框架可以很容易的實現一個動畫效果。但是最終是什麼樣的動畫是由你的程式決定的。你可以在程式碼塊中指定動畫的內容然後再將程式碼塊傳給animateWithDuration:animations:方法,像下面這樣:
[UIView animateWithDuration:2.0
    animations:^ {
        self.cardView.alpha = 1.0;
        self.cardView.frame = CGRectMake(176.0, 258.0, 72.0, 96.0);
        self.cardView.transform = CGAffineTransformMakeRotation(M_PI);
    }
];

    當這個動畫程式碼塊執行時,我們的紙牌會展現三種方式的動畫:改變它的alpha值從而淡入顯示,改變它的位置到右下角(玩家的位置),以及自轉180度(為了使其效果更好)。
第二個程式碼塊的例子是迭代一個紙牌的集合,並列印其名字和在集合裡的索引值。
你可以通過使用for迴圈來達到目的,但是在iOS4中NSArray類有一個使用了程式碼塊的方便方法:enumerateObjectsUsingBlock:。下面是如何使用它:

NSArray *cards = [NSArray arrayWithObjects:@"Jack", @"Queen", @"King", @"Ace", nil];
 
[cards enumerateObjectsUsingBlock:^(id object, NSUInteger index, BOOL *stop) {
    NSLog(@"%@ card at index %d", object, index);
}];

    這個程式碼塊使用了三個引數:陣列中的一個物件,該物件的索引,以及一個標識迭代是否結束的標誌。我們稍候再對其進一步探討。enumerateObjectsUsingBlock: 這個方法會將集合中的每一個元素傳入相應的引數並呼叫程式碼塊中的方法。

    因此在你的mac和iOS程式中使用程式碼塊的優勢是:它允許你附加任意的程式碼到蘋果官方提供的方法上。儘管在概念上與代理相似,但是在方法中使用簡短的內聯程式碼塊往往更加方便,更加優雅。
    這是一個好的開始,但重要的是要明白它內部的處理。當我學習新東西的時候,我喜歡先將其分為一個個簡單的部分,瞭解它們如何工作,然後再將它們組裝到一塊,這樣我會對自己寫的程式碼以及快速解決出現的問題充滿信心。因此,讓我們先回頭學習下如何宣告和呼叫簡單的程式碼塊。
程式碼塊的基本概念

    一個程式碼塊可以簡單看作是一組可執行的程式碼。例如,下面是一個列印當前日期和時間的程式碼塊:
^ {
    NSDate *date = [NSDate date];


    NSLog(@"The date and time is %@", date);
};
    插 入符號(^)宣告一個程式碼塊的開始,一對大括號{}構成了程式碼塊的體部。你可以認為程式碼塊與一個匿名函式類似。那麼,如果是一個匿名的函式,我們該怎麼調 用這個程式碼塊呢?最常見使用程式碼塊的方式是將其傳入方法中供方法回撥,就像之前我們已經見到了view animations 和enumeration。另一種使用程式碼塊的方式是將其賦予程式碼塊變數,然後可使用該變數來直接呼叫程式碼塊。以下是如何宣告我們的程式碼塊並將它賦予程式碼 塊變數now:
void (^now)(void) = ^ {
    NSDate *date = [NSDate date];
    NSLog(@"The date and time is %@", date);
};
    宣告一個塊變數的語法需要一些時間適應,這才有趣。如果你使用過函式指標,程式碼塊變數與其類似。在上面程式碼等號右邊是我們已經介紹過的程式碼塊。等號左邊我們聲明瞭一個程式碼塊變數now。


    程式碼塊變數之前有^符號並被小括號包著,程式碼塊變數有型別定義的。因此,上圖中的now變數可以應用任何無參,無返回值的程式碼塊。我們之前宣告的程式碼塊符合這要求,,所以我們可以放心的把它分配給now變數。
    只要有一個程式碼塊變數,並在其作用域範圍內,我們就可以像呼叫函式一樣來呼叫它。下面是如何呼叫我們的程式碼塊:
now();
    你可以在C函式或者Objective-c方法中宣告程式碼塊變數,然後在同一作用域內呼叫它,就像我們前面說明那樣。當代碼塊執行時,它列印當前的日期和時間。目前為止,進展順利。
程式碼塊是閉包
    如 果這就是程式碼塊的全部的話,那麼他與函式是完全相同的。但事實是程式碼塊不僅僅是一組可執行的程式碼。程式碼塊能夠捕捉到已宣告的同一作用域內的變數,同時由於 程式碼塊是閉包,在程式碼塊宣告時就將使用的變數包含到了程式碼塊範圍內。為了說明這一點,讓我們改變一下前面的例子,將日期的初始化移到程式碼塊之外。
NSDate *date = [NSDate date];

void (^now)(void) = ^ {
    NSLog(@"The date and time is %@", date);
};

now();
    當你第一次呼叫這個程式碼塊的時候,它與我們之前的版本結果完全一致:列印當前的日期和時間。但是當我們改變日期後再呼叫程式碼塊,那麼就會有顯著的不同了,
sleep(5);

date = [NSDate date];

now();
    盡 管我們在呼叫程式碼塊之前改變了日期,但是當代碼塊呼叫時仍然列印的是之前的日期和時間。就像是日期在程式碼塊宣告時停頓了一樣。為什麼會這樣呢,當程式執行 到程式碼塊的宣告時,程式碼塊對同一作用域並且塊內用到的變數做一個只讀的備份。你可以認為變數在程式碼塊內被凍結了。因此,不論何時當代碼塊被呼叫時,立即調 用或5秒鐘之後,只要在程式退出之前,它都是列印最初的日期和時間。
    事實上,上面那個展示程式碼塊是閉包的例子並不十分完善,畢竟,你可以將日期作為一個引數傳入到程式碼塊中(下面講解)。但是當你將程式碼塊在不同方法間傳遞時閉包的特性就會變得十分有用,因為它裡面的變數是保持不變的。
程式碼塊引數
    就像函式一樣,程式碼塊可以傳入引數和返回結果。例如,我們想要一個能夠返回指定數的三倍的程式碼塊,下面是實現的程式碼塊:
^(int number) {
    return number * 3;
};
    為程式碼塊宣告一個變數triple,如下:
int (^triple)(int) = ^(int number) {
    return number * 3;
};
    上面說過,我們需要熟悉等號左邊宣告程式碼塊變數的語法。現在讓我們從左到右分開來說明:


最 左邊的int是返回值型別,中間是小括號包圍插入符號^及程式碼塊變數的名字,最後又一個小括號,包圍著引數的型別(上面例子中只有一個int引數)。等號 右邊的程式碼塊宣告必須符合左側的定義。有一點要說明的是,為了方便,可以不宣告程式碼塊的返回型別,編譯器會從返回語句中做出判斷。
    要呼叫這個程式碼塊,你需要傳入一個需要乘3的引數,並接受返回值,像這樣:
int result = triple(2);

    下面你將知道如何宣告並建立一個需要兩個int型引數,將它們相乘然後返回結果的程式碼塊:
int (^multiply)(int, int) = ^(int x, int y) {
    return x * y;
};

    這是如何呼叫這個程式碼塊:
int result = multiply(2, 3);

    宣告程式碼塊變數使我們有機會探討程式碼塊型別以及如何呼叫。程式碼塊變數類似函式指標,呼叫程式碼塊與呼叫函式相似。不同於函式指標的是,程式碼塊實際上是Objective-C物件,這意味著我們可以像物件一樣傳遞它們。
呼叫程式碼塊的方法
    在實際中,程式碼塊經常被作為引數傳入方法中供其回撥。當把程式碼塊作為一個引數時,相比分配一個程式碼塊變數,更通常的做法是作為內聯程式碼塊。例如,我們之前看到的例子:view animations 和enumeration。
    蘋 果官方已經增加了一些使用程式碼塊的方法到他們的框架中。你也可以寫一些使用程式碼塊的API了。例如,我們要建立一個Worker類的使用程式碼塊的類方法, 該方法重複呼叫程式碼塊指定的次數,並處理程式碼塊每次返回的結果。下面是我們使用內聯程式碼塊呼叫這個方法,程式碼塊負責返回1到10的每個數的三倍。
[Worker repeat:10 withBlock:^(int number) {
    return number * 3;
}];

    這個方法可以將任何接受一個int型引數並返回一個int型結果的程式碼塊作為引數,如果想得到數字的二倍,只需要改變傳入方法的程式碼塊。

編寫使用程式碼塊的方法
    在第一部分我們留下了一個任務:寫一個Work類的呼叫程式碼塊的類方法,並且重複呼叫程式碼塊指定的次數,還要處理每次程式碼塊的返回值。如果我們想要得到1到5的三倍的話,那麼下面是我們該如何調這個帶有內聯程式碼塊的方法:
[Worker repeat:5 withBlock:^(int number) {
    return number * 3;
}];
    我 經常這樣設計一個類,首先寫程式碼呼叫一個虛構的方法,這也是在提交之前一種形成API的簡單方式,一旦認為這個方法呼叫正確,我就去實現這個方法。這樣, 那個方法的名字是repeat:withBlock:,我認為不合適(我知道在第一部分是叫這個名字,但我已經改變注意了)。這個名字容易使人混淆,因為 該方法實際上並不是重複做相同的事情。這個方法從1迭代到指定的次數,並處理程式碼塊的返回。所以讓我們開始正確的重新命名它:
[Worker iterateFromOneTo:5 withBlock:^(int number) {
    return number * 3;
}];
    我對這個使用兩個引數的方法的名字iterateFromOneTo:withBlock:很滿意,一個int型引數表示呼叫程式碼塊的次數和一個要被呼叫的程式碼塊引數。現在讓我們去實現這個方法。
    對 於初學者,我麼該如何宣告這個 iterateFromOneTo:withBlock:方法呢?首先我們需要知道所有引數的型別,第一個引數很容易,是個int型別;第二個引數是一個 程式碼塊,程式碼塊是有返回型別的。在這個例子中,這個方法可以接受任何有一個int型引數並返回int型結果的程式碼塊作為引數。下面是實際的程式碼塊型別:
int (^)(int)
    已經有了方法的名字和它的引數型別,我們就可以宣告這個方法了。這是Worker類的類方法,我們在worker.h中宣告它:
@interface Worker : NSObject {
}
 
+ (void)iterateFromOneTo:(int)limit withBlock:(int (^)(int))block;
 
@end
    第 一眼看去,程式碼塊引數不容易理解。有個要記住訣竅是:在Objective-C中所有的方法引數有兩個部分組成。被括起來的引數型別以及引數的名稱。這個 例子中,引數的要求是一個是int型和一個是int(^)(int)型的程式碼塊(你可以為引數命名為任意的名字,不一定非得是block)。這個方法的實 現是在Worker.m檔案檔案中,比較簡單:
#import "Worker.h"
 
@implementation Worker
 
+ (void)iterateFromOneTo:(int)limit withBlock:(int (^)(int))block {
    for (int i = 1; i <= limit; i++) {
        int result = block(i);
        NSLog(@"iteration %d => %d", i, result);
    }
}
 
@end
    方 法通過一個迴圈來每次呼叫程式碼塊,並打印出程式碼塊的返回結果。記住一旦我們在作用域內有一個程式碼塊變數,那麼就可以像函式一樣使用它。在這裡程式碼塊引數就 是一個程式碼塊變數。因此,當執行block(i)時就會呼叫傳入的程式碼塊。當代碼塊返回結果後會繼續往下執行。現在我們可以使用內聯程式碼塊的方式呼叫 iterateFromOneTo:withBlock:方法,像這樣:
[Worker iterateFromOneTo:5 withBlock:^(int number) {
    return number * 3;
}];
我們也可以不使用內聯程式碼塊的方式,傳入一個程式碼塊變數作為引數:
int (^tripler)(int) = ^(int number) {
    return number * 3;
};
 
[Worker iterateFromOneTo:5 withBlock:tripler];
不論那種方式,我們得到的輸出如下:
iteration 1 => 3
iteration 2 => 6
iteration 3 => 9
iteration 4 => 12
iteration 5 => 15
    當然我們可以傳入進行任何運算的程式碼塊。想要得到數字的平方嗎?沒問題,只要傳入一個不同的程式碼塊:
[Worker iterateFromOneTo:5 withBlock:^(int number) {
    return number * number;
}];
現在我們的程式碼是可以執行的,下面將程式碼稍微整理下吧。
善於使用Typedef
    匆忙的宣告程式碼塊的型別容易混亂,即使在這個簡單的例子中,函式指正的語法還是有許多不足之處:
+ (void)iterateFromOneTo:(int)limit withBlock:(int (^)(int))block;
    試想程式碼塊要使用多個引數,並且有些引數是指標型別,這樣的話你幾乎需要完全重寫你的程式碼。為了提高可讀性和避免在.h和.m中出項重複,我們可以使用typedef修改Worker.h檔案:
typedef int (^ComputationBlock)(int);
 
@interface Worker : NSObject {
}
 
+ (void)iterateFromOneTo:(int)limit withBlock:(ComputationBlock)block;
 
@end
    typedef 是C語言的一個關鍵字,其作用可以理解為將一個繁瑣的名字起了一個暱稱。在這種情況下,我們定義一個程式碼塊變數ComputationBlock,它有一 個int型引數和一個int型返回值。然後,我們定義iterateFromOneTo:withBlock:方法時,可以直接使用 ComputationBlock作為程式碼塊引數。同樣,在Worker.m檔案,我們可以通過使用ComputationBlock簡化程式碼:
#import "Worker.h"
 
@implementation Worker
 
+ (void)iterateFromOneTo:(int)limit withBlock:(ComputationBlock)block {
    for (int i = 1; i <= limit; i++) {
        int result = block(i);
        NSLog(@"iteration %d => %d", i, result);
    }
}
 
@end
    嗯, 這樣就好多了,程式碼易於閱讀,沒有在多個檔案重複定義程式碼塊型別。事實上,你可以使用ComputationBlock在你程式的任何地方,只要 import “Worker.h”,你會碰到類似的typedef在新的iOS4的API中。例如,ALAssetsLibrary類定義了下面的方法:
- (void)assetForURL:(NSURL *)assetURL      
        resultBlock:(ALAssetsLibraryAssetForURLResultBlock)resultBlock 
       failureBlock:(ALAssetsLibraryAccessFailureBlock)failureBlock
    這個方法呼叫兩個程式碼塊,一個程式碼塊是找到所需的資源時呼叫,另一個是沒找到時呼叫。它們 的 typedef如下:
typedef void (^ALAssetsLibraryAssetForURLResultBlock)(ALAsset *asset);
typedef void (^ALAssetsLibraryAccessFailureBlock)(NSError *error);
    然後在你的程式中可以使用ALAssetsLibraryAssetForURLResultBlock和ALAssetsLibraryAccessFailureBlock去表示相應的程式碼塊變數。
    我建議在寫一個使用程式碼塊的公用方法時就用typedef,這樣有助於你的程式碼整潔,並可以讓其他開發人員方便使用。
再來看一下閉包
    你應該還記得程式碼塊是閉包,我們簡要的講述一下在第一部分提及的閉包。在第一部分閉包的例子並不實用,而且我說閉包在方法間傳遞時會變得特別有用。現在我們已經知道如何寫一個實用程式碼塊的方法,那麼就讓我們分析下另一個閉包的例子:
int multiplier = 3;
 
[Worker iterateFromOneTo:5 withBlock:^(int number) {
    return number * multiplier;
}];
    我們使用之前寫的iterateFromOneTo:withBlock:方法,有一點不同的是沒有將要得到的倍數硬編碼到程式碼塊中,這個倍數被宣告在程式碼塊之外,為一個本地變數。該方法執行的結果與之前一致,將1到5之間的數乘3:
iteration 1 => 3
iteration 2 => 6
iteration 3 => 9
iteration 4 => 12
iteration 5 => 15
    這個程式碼的執行是一個說明閉包強大的例子。程式碼打破了一般的作用域規則。實際上,在iteratefromOneTo:withBlock:方法中呼叫multiplier變數,可以把它看作是本地變數。
    記 住,程式碼塊會捕捉周圍的狀態。當一個程式碼塊宣告時它會自動的對其內部用到的變數做一個只讀的快照。因為我們的程式碼塊使用了multiplier變數,這個 變數的值被程式碼塊儲存了一份供之後使用。也就是說,multiplier變數已經成為了程式碼塊狀態啊的一部分。當代碼塊被傳入到 iterateFromOneTo:withBlock:方法,快的狀態也傳了進去。
    好吧,如果我們想在程式碼塊的內部改變multiplier變數該怎麼辦?例如,程式碼塊每次被呼叫時要讓multiplier變為上一次計算的結果。你可能會試著在程式碼塊裡直接改變multiplier變數,像這樣:
int multiplier = 3;
 
[Worker iterateFromOneTo:5 withBlock:^(int number) {
    multiplier = number * multiplier;
    return multiplier;  // compile error!
}];
    這樣的話是通不過編譯的,編譯器會報錯“Assignment of read-only variable 'mutilplier'”。這是因為程式碼塊內使用的是變數的副本,它是堆疊裡的一個常量。這些變數在程式碼塊中是不可改變的。
    如果你想要修改一個在塊外面定義,在塊內使用的變數時,你需要在變數宣告時增加新的字首_block,像這樣:
__block int multiplier = 3;
 
[Worker iterateFromOneTo:5 withBlock:^(int number) {
    multiplier = number * multiplier;
    return multiplier;
}];
 
NSLog(@"multiplier  => %d", multiplier);
這樣程式碼可以通過編譯,執行結果如下:
iteration 1 => 3
iteration 2 => 6
iteration 3 => 18
iteration 4 => 72
iteration 5 => 360
multiplier  => 360
    要 注意的是程式碼塊執行之後,multiplier變數的值已經變為了360。換句話說,程式碼塊內部修改的不是變數的副本。宣告一個被_block修飾的變數 是將其引用傳入到了程式碼塊內。事實上,被_block修飾的變數是被所有使用它的程式碼塊共享的。這裡要強調的一點是:_block不要隨便使用。在將一些 東西移入記憶體堆中會存在邊際成本,除非你真的確定需要修改變數,否則不要用_block修飾符。
編寫返回程式碼塊的方法
    有時我們會需要編寫一個返回程式碼塊的方法。讓我先看一個錯誤的例子:
+ (ComputationBlock)raisedToPower:(int)y {
    ComputationBlock block = ^(int x) {
        return (int)pow(x, y);
    };
    return block;  // Don't do this!
}
    這種方法簡單的建立了一個計算y的x次冪的程式碼塊然後返回它。它使用了我們之前通過typedef使用的ComputationBlock。下面是我們對所返回程式碼塊的期望效果:
ComputationBlock block = [Worker raisedToPower:2];
block(3);  // 9
block(4);  // 16
block(5);  // 25
    在上面的例子中,我們使用的得到程式碼塊,傳入相應的引數,它應該會返回傳入值的平方。但是當我們執行它時,會得到執行時錯誤”EXC_BAD_ACCESS”。
    怎麼辦?解決這個問題的關鍵是瞭解程式碼塊是怎麼分配記憶體的。程式碼塊的生命週期是在棧中開始的,因為在棧中分配記憶體是比較塊的。是棧變數也就意味著它從棧中彈出後就會被銷燬。方法返回結果就會發生這樣的情況。
    回 顧我們的raisedToPower:方法,可以看到在方法中建立了程式碼塊並將它返回。這樣建立程式碼塊就是已明確程式碼塊的生存週期了,當我們返回程式碼塊變 量後,程式碼塊其實在記憶體中已經被銷燬了。解決辦法是在返回之前將程式碼塊從棧中移到堆中。這聽起來很複雜,但是實際很簡單,只需要簡單的對程式碼塊進行 copy操作,程式碼塊就會移到堆中。下面是修改後的方法,它可以滿足我們的預期:
+ (ComputationBlock)raisedToPower:(int)y {
    ComputationBlock block = ^(int x) {
        return (int)pow(x, y);
    };
    return [[block copy] autorelease];
}
    注 意我們使用了copy後就必須跟一個autorelease從而平衡它的引用計數器,避免記憶體洩露。當然我們也可以在使用程式碼塊之後將其手動釋放,不過這 就不符合誰建立誰釋放的原則了。你不會經常需要對程式碼塊進行copy操作,但是如果是上面所講的情況你就需要了,這點請留意。
將所學的整合在一起
    那麼,讓我們來把所學的東西整合為一個更實際點的例子。假設我們要設計一個簡單的播放電影的類,這個類的使用者希望電影播放完之後能夠接受一個用於展現應用特定邏輯的回撥。前面已經證明程式碼塊是處理回撥很方便的方法。
讓我們開始寫程式碼吧,從一個使用這個類的開發人員的角度來寫:
MoviePlayer *player = 
    [[MoviePlayer alloc] initWithCallback:^(NSString *title) {
        NSLog(@"Hope you enjoyed %@", title);
}];
 
[player playMovie:@"Inception"];
    可 以看出我們需要MoviePlayer類,他有兩個方法:initWithCallback:和playMovie:,初始化的時候接受一個程式碼塊,然後 將它儲存起來,在執行playMovie:方法結束後再呼叫程式碼塊。這個程式碼塊需要一個引數(電影的名字),返回void型別。我們對回撥的程式碼塊型別使 用typedef,使用property來儲存程式碼塊變數。記住,程式碼塊是物件,你可以像例項變數或屬性一樣使用它。這裡我們將它當作屬性使用。下面是 MoviePlayer.h:
typedef void (^MoviePlayerCallbackBlock)(NSString *);
 
@interface MoviePlayer : NSObject {
}
 
@property (nonatomic, copy) MoviePlayerCallbackBlock callbackBlock;
 
- (id)initWithCallback:(MoviePlayerCallbackBlock)block; 
- (void)playMovie:(NSString *)title;
 
@end
下面是MoviePlayer.m:
#import "MoviePlayer.h"
 
@implementation MoviePlayer
 
@synthesize callbackBlock;
 
- (id)initWithCallback:(MoviePlayerCallbackBlock)block {
    if (self = [super init]) {
        self.callbackBlock = block;
    }
    return self;
}
 
- (void)playMovie:(NSString *)title {
    // play the movie
    self.callbackBlock(title);
}
 
- (void)dealloc {
    [callbackBlock release];
    [super dealloc];
}
 
@end
    在 initWithCallback:方法中將要使用的程式碼塊宣告為callbackBlock屬性。由於屬性被宣告為了copy方式,程式碼塊會自動進行 copy操作,從而將其移到堆中。當playMovie:方法呼叫時,我們傳入電影的名字作為引數來呼叫程式碼塊。
    現在我們假設一個開發人員要在程式中使用我們的MoviePlayer類來管理一組你打算觀看的電影。當你看完一部電影之後,這部電影就會從組中移除。下面是一個簡單的實現,使用了閉包:
NSMutableArray *movieQueue = 
    [NSMutableArray arrayWithObjects:@"Inception", 
                                     @"The Book of Eli", 
                                     @"Iron Man 2", 
                                     nil];
 
MoviePlayer *player = 
    [[MoviePlayer alloc] initWithCallback:^(NSString *title) {
        [movieQueue removeObject:title];
}];
 
for (NSString *title in [NSArray arrayWithArray:movieQueue]) {
    [player playMovie:title];
};

    請 注意程式碼塊使用了本地變數movieQueue,它會成為程式碼塊狀態的一部分。當代碼塊被呼叫,就會從陣列movieQueue中移除一個電影,儘管此時 陣列是在程式碼塊作用域之外的。當所有的電影播放完成之後,movieQueue將會是一個空陣列。下面是一些需要提及的重要事情:
1、movieQueue變數是一個數組指標,我們不能修改它的指向。我們修改的是它指向的內容,因此不需要使用_block修飾。
2、為了迭代movieQueue陣列,我們需要建立一個它的copy,否則如果我們直接使用movieQueue陣列,就會出現在迭代陣列的同事還在移除它的元素,這會引起異常。
3、如果不使用程式碼塊,我們可以宣告一個協議,寫一個代理類,並註冊這個代理作為回撥。很明顯該例子使用內聯程式碼塊更方便。
4、在不改變MoviePlayer類的前提下可以給他增加新功能。比如另一個開發者可以在看完一部電影后將其分享到twitter或對電影進行評價等。