Objective C 教程 @Part-03

● 類別部署 ( Category )

再物件導向的觀念裡,將整個專案設計元件化,一來方便管理,一來易於重複性使用。舉Angry Bird這款有名的遊戲來說,每隻鳥都繼承自一個鳥的基底物件,而如果今天要設計一隻有新特性的鳥,我們將直接繼承鳥物件再進而針對這隻新的鳥成員新增對應程式碼。以上所說是正常物件設計情況下以繼承方式即可達成目的。但有一種特定情況,例如你想對每隻鳥都會去繼承的鳥基底物件新增一些程式碼,但是又不想直接修改程式碼而破壞原本寫好的物件結構時,在 Objective C 裡提供了Category這個速成的捷徑,它能在不更動目標物件程式碼的任何內容情況下,對該目標物件新增你想加入的程式碼片段。以下我們以最簡單的方式示範Category應用 : 

@interface

#import <Foundation/Foundation.h>

@interface NSObject(StrongObject)

-(void)sayHelloWorld;
-(int)getRandNumber;

@end
  • 首先我們在Xcode上的專案按右鍵 -> New File -> 新增Objective-C category文件,Category的名稱輸入StrongObject , Category on 部分則選擇 NSObject,此時你的專案會新增兩個文件 NSObject+StrongObject.h 與 NSObject+StrongObject.m。
  • 新增Category新增的方式雖然有一點點的不一樣,不過在書寫過程其實大同小異,不過卻能避免與其他類別應用搞混。
  • NSObject是我們一路走來一直在繼承的基底物件,或許這時你已感到困惑,Apple提供內建的東西我們能這樣任意修改嗎? 答案當然是不能,雖然我們不作修改,但利用Category一樣能達到在外部新增對應程式碼片段的目的。而(StrongObject)則是你擴充NSObject內容的Category名稱。
  • 可以看到宣告了兩個沒什麼意義的方法,一切的一切只是為了證明我們真的擴充了apple底層框架提供的NSObject物件。
  • 還有一點需要特別注意,Category的機制沒有提供新增全域變數成員。

@implementation

#import "NSObject+StrongObject.h"

@implementation NSObject(StrongObject)

-(void)sayHelloWorld
{
   NSLog(@"Hello World");
}
-(int)getRandNumber
{
   return rand() % 10 + 1;
}

@end
  • 實作出"NSObject+StrongObject.h"的方法,時做完成後在此專案內所有調用甚至繼承NSObject這個Class的地方,通通都可以呼叫以上兩個新定義的方法並執行方法內的所有內容。
  • 順帶一提,rand() 是Objective C 提供的快速取亂數的函示。以上用意在於取1~10之間的亂數。

@main

#import <Foundation/Foundation.h>
#import "NSObject+StrongObject.h"

int main(int argc, const char * argv[])
{

   NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];

   NSObject *testObject = [[NSObject alloc] init];
   [testObject sayHelloWorld];
   NSLog(@"多了一個隨取亂數的功能 : %d",[testObject getRandNumber]);

   [pool drain];
}

®輸出結果 :

 Hello World
 多了一個隨取亂數的功能 : 16807
  • 在此我們直接調用底層框架物件NSObject來測試剛剛我們利用Category機制添加上的兩個方法是否能順利執行,結果當然是可行的。
  • 在某物件繼承NSObject物件時當然也能呼叫以Categoty新增出來的方法,只要以 [ self 方法名稱 ] 呼叫父類別代以執行,即可順利達成。

● 協定 ( Protocol)

在Objective C裡 Protocol協定能達到類似多重繼承的效果,在Objective C裡一次只能繼承一個物件,就像我們一直以來繼承了NSOBject後,後面就不能再新增 “:其他物件"去繼承其他的物件,但Protocol能達到將新定義的方法集合起來讓其他需要擴充的類別自行取用協定好的.h物件。舉例來說 MyClass : NSObject <Protocol_1, Protocol_2,Protocol_3 >,"< >"內就是你擴充的協定物件。以下做簡單的擴充運算範例 : 

@interface – protocol

#import <Foundation/Foundation.h>

@protocol CounterProtocol

-(int) toAdd;
-(int) toSub;
-(int) toMul;
-(int) toDiv;

@end
  • 首先我們在Xcode上的專案按右鍵 -> New File -> 新增Objective-C protocol文件。新增協定名稱為"CounterProtocol"。
  • 此時專案只會新增一個CounterProtocol.h的檔案並不會有.m檔,原因是因為協定檔只需定義空Function予以調用協定的類別遵照CounterProtocol.h的內容實作所有方法內容。PS : 調用協定的物件並不一定要全部實做出來,只需調用本身需要的部分來實作。此種做法有些類似Java裡的抽象觀念。
  • 此協定我們定義了四種運算方式的空方法 : 加-減-乘-除。

@interface

#import <Foundation/Foundation.h>
#import "CounterProtocol.h"

@interface MyNumber : NSObject{
   int firstNum;
   int secondNum;
}
@property int firstNum;
@property int secondNum;

@end
  • 生成一MyNumber的類別並新增兩個int屬性變數。為了可以取得剛剛所定義的協定物件,我們需先將此協定從外部載入,所以我們需要這行 #import “CounterProtocol.h" 來載入,載入後當然並非已經取得使用,我們必須在繼承的物件後面加上協定語法<CounterProtocol>,以上兩個動作完成後,MyNumber類別已完成與CounterProtocol的繫結。

@implementation

#import "MyNumber.h"

@implementation MyNumber

@synthesize firstNum, secondNum;

-(int) toAdd
{
   return firstNum + secondNum;
}
-(int) toSub
{
   return firstNum - secondNum;
}
-(int) toMul
{
   return firstNum * secondNum;
}
-(int) toDiv
{
   return firstNum / secondNum;
}

@end
  • 在XCode上你會發現你在實做檔打 -(int) 並按下"esc"鍵即可看到協定好的所有方法名稱,此可你只要照著把內容實做完成即可。
  • 或許你感到困惑,以上的程式碼就算不透過協定我們打出來效果不是也一樣嗎? 理論是對的,但因為我們只是要證明一般類別對protocol的繫結成果,所以我們並不想把程式碼寫得太過於複雜,不過解釋protocol的用意是必要的,他真正深入的用意不在於擴充程式碼,而是當你設己某些元件它需要在特定的情況下做CallBack(回呼)動作,而你回呼的方法名稱想要固定不想被重新定義造成程式碼混亂,此時protocol協定出固定的方法宣告就可派上用場。
  • 何謂CallBack : 當A物件生成B物件並呼叫B物件裡的某個方法,而這個方法執行完的結果B物件會自動回呼A物件的某個方法名稱,但B物件呼叫的是一個固定的方法名稱,而這個方法名稱A物件並不存在也不知道,此時A物件只需協定跟B物件有關的Protocol協定,實作B物件欲回呼的方法名稱即可完美與B物件對應。其實在iOS的UI設計裡大量用到Protocol與CallBack回呼,例如當螢幕被使用者觸碰時的回傳就是個常用的協定。

@main

#import <Foundation/Foundation.h>
#import "MyNumber.h"

int main(int argc, const char * argv[])
{
   NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];

   MyNumber *myNumber = [[[MyNumber alloc] init] autorelease];
   myNumber.firstNum = 10;
   myNumber.secondNum = 5;
   NSLog(@"To Add : %d",[myNumber toAdd]);
   NSLog(@"To Sub : %d",[myNumber toSub]);
   NSLog(@"To Mul : %d",[myNumber toMul]);
   NSLog(@"To Div : %d",[myNumber toDiv]);

   [pool drain];
}

®輸出結果 :

 To Add : 15
 To Sub : 5
 To Mul : 50
 To Div : 50

在XCode印出結果驗證一下吧孩子!!

● 例外處理 (Exception)

在程式運行時,往往我們會有疏忽處理的小細節,例如程式在跟使用者互動的時候,使用者做出的動作超出我們預期而造成程式整個錯誤無法執行。雖然這些事情無法預期,但為了讓使用者有最好的使用觀感,在某些特定容易造成例外事件發生時候(如網路同步資料,處理執行續等等),我們應在這類的片段式程式碼做一些基本防範,也就是大多數物件導向語言都會提供的Exception例外機制。與多數語言用法一致,都是由 @try @catch @final ly相輔相成。以下我們試著去呼叫一個從未存在的類別並作出適當的例外處理機制 : 

@interface

#import <Foundation/Foundation.h>

@interface ExceptionTest : NSObject{
   int firstNumber;
   int secondNumber;
}
-(id)initWithFirstNum:(int)firNum andSenNum:(int)senNum;
-(void)addNumber;
-(void)subNumber;
-(void)mulNumber;

@end
  • 新增一個例外觸發的類別,其內容與剛剛的簡易計算函示相同,只是把相除的方法定義拿掉。
  • 這次我們使用自定的初始方法為資料作初始,所以我們定義一個 initWithFirstNum..方法並接受兩個參數。

@implementation

#import "ExceptionTest.h"

@implementation ExceptionTest

-(id)initWithFirstNum:(int)firNum andSenNum:(int)senNum
{
   if ([self init]) {
     firstNumber = firNum;
     secondNumber = senNum;
   }
   return self;
}
-(void)addNumber
{
   NSLog(@"addNumber : %d",firstNumber+secondNumber);
}
-(void)subNumber
{
   NSLog(@"subNumber : %d",firstNumber-secondNumber);
}
-(void)mulNumber
{
   NSLog(@"mulNumber : %d",firstNumber*secondNumber);
}

@end
  • 順便復習一下自定的初始化,[self init]這個動作是呼叫繼承了NSObject所定義的初始化方法,因為只有呼叫他才能正確配置記憶體。
  • 將外部傳入的值直接傳給本身類別內的全域變數並實做所有方法。

@main

#import <Foundation/Foundation.h>
#import "ExceptionTest.h"

int main(int argc, const char * argv[])
{
   NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
   ExceptionTest *ecpTest = [[[ExceptionTest alloc] initWithFirstNum:20 andSenNum:10]autorelease];
   [ecpTest addNumber];
   [ecpTest subNumber];
   [ecpTest mulNumber];
   //[ecpTest divNumber]; -> Error
   BOOL isSuccessful = YES;
   @try {
     [ecpTest divNumber];
   }
   @catch (NSException *exception) {
     NSLog(@"Caught %@ %@",[exception name],[exception reason]);
     NSLog(@"divNumber方法並不存在於ExceptionTest類別");
     isSuccessful = NO;
   }
   @finally {
     if (isSuccessful)
       NSLog(@"運行成功");
     else
       NSLog(@"程式拋出例外");
   }
   [pool drain];
}
  • 前面呼叫的三個方法本身都存在於ExceptionTest類別內,故可以執行出我們想要的結果,但注解掉的那一行呼叫相除的方法並不存在,直接呼叫程式必定掛點。
  • 假設我們不確定執行 [ecpTest divNumber] 是否有未預期的事發生,為了避免悲劇,我們且用 @try{ } 這個關鍵字將我們擔心受怕的程式碼片段包覆起來。@try本身會幫你檢查此段程式碼是否有無法執行的問題存在,如果檢查到問題會將目標轉移至@catch,而執行到Catch時Catch會抓到發生錯誤例外的原因並以 NSException 回傳所有錯誤資訊。一般處理到Catch時開發者本身可以鬆口氣了,因為至少我們知道錯誤原因且程式不會掛點。而在最後面可選擇性搭配@finally使用,@finally不論你是否有抓到例外事件,在處理完成續後必定執行區塊內程式碼,而此區塊恰恰可以做一個程式片段執行的中繼點,做出執行成功與失敗分別的下一步走向。

®輸出結果 :

 To Add : 30
 To Sub : 10
 To Mul : 200
 2013-04-06 01:04:01.015 ExceptionTest[83294:303] -[ExceptionTest divNumber]: unrecognized selector sent t o instance 0x1001099a0
 2013-04-06 01:04:01.016 ExceptionTest[83294:303] Caught NSInvalidArgumentException -[ExceptionTest divNum ber]: unrecognized selector sent to instance 0x1001099a0
 divNumber方法並不存在於ExceptionTest類別
 程式拋出例外
廣告

發表迴響

在下方填入你的資料或按右方圖示以社群網站登入:

WordPress.com Logo

您的留言將使用 WordPress.com 帳號。 登出 / 變更 )

Twitter picture

您的留言將使用 Twitter 帳號。 登出 / 變更 )

Facebook照片

您的留言將使用 Facebook 帳號。 登出 / 變更 )

Google+ photo

您的留言將使用 Google+ 帳號。 登出 / 變更 )

連結到 %s

%d 位部落客按了讚: