--- title: 'Objective-C 物件導向、例外' disqus: kyleAlien --- Objective-C 物件導向、例外 === ## Overview of Content Objective-C 最大的特色就是它是一門物件導向語言,具有封裝、繼承、多型... 等等特色 :::info * **物件導向設計、命令式設計差異** 物件導向最小單元是類(Class),命令式設計最小單元是方法(Method) > 物件導向:Objective-C, Java, Kotlin, Python 語言 > 命令式:C 語言 ::: [TOC] ## 類別、物件 類別是模擬真實世界中的一種抽象(只有一個),而物件是一個類的實例(可有多個) ### 宣告類別 - @interfacet * **類別++宣告++**:在 Objective-C 中,類別宣告叫做 **介面檔案**,它的 **副檔名須是 `.h` 結尾** 格式:**使用 `@interfacet`、`@end` 關鍵字** ```objectivec= @interfacet 類別名稱: 超類 { } @end ``` 範例:`MyFirstClz.h` 檔案 ```objectivec= @interface MyFirstClz { } @end ``` :::info * Objective-C 的類預設都有一個基本的超類(父類)`NSObject`,可以不用特別寫出來,不過需要知道;就像是 Java 中的 Object 類是所有類的基類 > `NSObject` 存在 `Foundation.h` 中,所以必須 import ```objectivec= #import <Foundation/Foundation.h> @interface MyFirstClz: NSObject { } @end ``` ::: ### 定義類別 - @implementation * **類別++定義++**:類別定義與宣告必須分開,並且類別定義的 **副檔名須是 `.m` 結尾** 格式:**使用 `@implementation`、`@end` 關鍵字** ```objectivec= @@implementation 類別名稱 { } @end ``` 範例:`MyFirstClz.m` 檔案 ```objectivec= #import "MyFirstClz.h" @implementation MyFirstClz { } @end ``` ### 創建物件 * 物件是類在記憶體中的實體(可以有多個),可拆分為三個步驟 1. **物件的宣告**:宣告並不耗用記憶體空間,它是描述該物件的類是哪一個,並且物件名前必須使用 `*` 號,代表了該物件的指向 > `*` 就如同 C 語言中的指標,之後再呼叫物件的其他成員時會配合使用 `->` 符號 格式: ```objectivec= 類名 *物件名; ``` 範例: ```objectivec= // 物件的宣告 MyFirstClz *mfc; ``` 2. **分配物件空間**:也就是 分配記憶體空間;`=` 運算子後的類別名後使用 `alloc` 關鍵字,來分配該物件的記憶體空間 格式: ```objectivec= 物件名 = [類名 alloc]; ``` 範例: ```objectivec= MyFirstClz *mfc; // 分配物件空間 mfc = [MyFirstClz alloc]; ``` :::success * Objective-C 與其他語言不同的是,它使用很多的中括號 `[ ]` 來代表創建、互叫 ::: 3. **初始化物件**:其實就是初始化記憶體空間;使用 `init` 關鍵字進行物件初始化 格式: ```objectivec= 物件名 = [物件名 init] ``` 範例: ```objectivec= MyFirstClz *mfc; mfc = [MyFirstClz alloc]; // 初始化物件 mfc = [mfc init]; ``` :::info * `alloc`、`init` 是什麼? `alloc`、`init` 可以記憶成關鍵字,但其實 **`alloc`、`init` 這兩個字是方法**,它們 **是 `NSObject` 提供的方法** ::: * 可以將以上 `alloc`、`init` 合併為一個步驟 ```objectivec= MyFirstClz *mfc = [[MyFirstClz alloc] init]; ``` * **`new` 關鍵字**:Objective-C 也有提供 `new` 這個關鍵字來幫我完成類的創建、初始化,使用方式如下 > `new` 這個關鍵字也是 `NSObject` 提供的方法 ```objectivec= MyFirstClz *mfc = [MyFirstClz new]; ``` :::success * 其他知識點 在創建物件使用 `#import "目標類.h"`; 而使用 `""` 代表我們要尋找的是自定義檔案,而使用 `<>` 則代表要找系統檔 ::: ## 類別成員 ### 成員變數 - @public * 類別成員又稱為個體變數,它 **定義在類的宣告檔中(`.h` 檔)**;可以在宣告檔中的花括號內 `{ }` 定義成員 格式: ```objectivec= 物件名->成員 ``` :::warning * **`@public` 標記** 類別成員預設會是用 `protect`,讓外部無法訪問,如果外部要訪問要使用 `@public` 標記 > ![](https://hackmd.io/_uploads/ryAz9LGtn.png) ::: 範例: ```objectivec= #import <Foundation/Foundation.h> @interface MyFirstClz: NSObject { @public int myValueX; @public int myValueY; } @end ``` * 建立物件後,可以使用 `->` 來呼叫類中的成員,範例: ```objectivec= MyFirstClz *mfc = [MyFirstClz new]; // set mfc->myValueX = 10; mfc->myValueY = 20; // get printf("myValueX: %i\n", mfc->myValueX); printf("myValueY: %i\n", mfc->myValueY); ``` > ![](https://hackmd.io/_uploads/SJWgoLMth.png) ### 變數類型 * 變數可以透過關鍵字去描述該變數的作用域、生命週期;其中常見的變數類型如下表 | 關鍵字 | 概述 | | - | - | | `static` | 它就有兩個特性:**區域性**、**靜態性** | | `extern` | 宣告該變數是外部變數(已在其他檔案定義) | | `auto` | 宣告變數是區域變數(預設設定),離開區域後會自動釋放,可以不用太在意 | | `const` | 標明該變數不可變 | | `voliate` | 該變數是原子變數,每次被設定都必須寫入,與 Thread 息息相關 | 1. `static` 靜態、區域性 ```objectivec= void staticVariable(void) { int normalLocalVariable = 0; static int callFuncTimes = 0; callFuncTimes += 1; normalLocalVariable += 1; printf("Call func times: %i, local variable: %i\n", callFuncTimes, normalLocalVariable); } ``` 呼叫 `staticVariable` 方法三次結果如下,可以看到 static 變數每次都會紀錄,並且該變數的活動範圍被限定在 `staticVariable` 方法內 > ![](https://hackmd.io/_uploads/rJTAAIzFh.png) 2. `extern` 宣告外部變數:可以把 `extern` 當成是一個 **占位符號**,它會去其他檔案中尋找定義該變數的地方,並取用 ```objectivec= void externVariable(void) { // 不用定義,它會去外部尋找名為 Hello 的定義 extern int Hello; extern int World; printf("Hello World: %i\n", Hello+World); } // 外部定義 int Hello = 100; int World = 3333; ``` > ![](https://hackmd.io/_uploads/ry74lwMth.png) ## 方法 一般來說在物件導向的程式設計中,我們會把以往的函數稱之為方法,它存在更強的區域性(封裝)關係 ### 方法宣告 - `.h` * 方法的宣告必須在宣告檔(`.h` 檔)中,**告訴編譯器該方法的 ++識別符號++** 格式: ```objectivec= @interface 類別名稱: NSObect { ... } // 方法宣告 (聲明) -/+(方法回傳類型) 方法名; // 方法 & 參數 -/+(方法回傳類型) 方法名:(參數類型)參數名; // 方法 & 多參數 -/+(方法回傳類型) 方法名1:(參數類型)參數名1 方法名2:(參數類型)參數名2; @end ``` :::info * **`-/+` 號**: `-/+` 號代表的是該方法是屬於物件還是類別,如下表 | 符號 | 說明 | | - | - | | `-` | 該方法屬於 **物件方法** | | `+` | 該方法屬於 **類方法**,就像是一個 **靜態方法**,不需要物件就可以呼叫 | ::: 範例: ```objectivec= @interface MyFirstClz: NSObject { @public int myValueX; @public int myValueY; } // 方法宣告 (聲明) +(void) sayHello; -(void) sayWorld; -(void) showTimes:(int)times; -(void) showHello:(int)timesH showWorld:(int)timesW; @end ``` ### 方法實現 - `.m` * 方法的實現就必須定義在實作檔案(`.m` 檔),並且實現的方法名、參數、返回值都必須與宣告相同 格式: ```objectivec= @@implementation 類別名稱: NSObect { ... } // 方法宣告 (聲明) -/+(方法回傳類型) 方法名 { } // 方法 & 參數 -/+(方法回傳類型) 方法名:(參數類型)參數名 { } // 方法 & 多參數 -/+(方法回傳類型) 方法名1:(參數類型)參數名1 方法名2:(參數類型)參數名2 { } @end ``` 範例:記得宣告檔記得先宣告函數,並按照宣告的方法實現 ```objectivec= #import "MyFirstClz.h" @implementation MyFirstClz { } +(void) sayHello { printf("Hello~ I'm class function.\n"); } -(void) sayWorld { printf("World~ I'm instance function.\n"); } -(void) showTimes:(int)times { for(int i = 0; i < times; i++) { printf("Hello World %i.\n", i); } } -(void) showHello:(int)timesH showWorld:(int)timesW { for(int i = 0; i < timesH; i++) { printf("Hello %i.\n", i); } for(int i = 0; i < timesW; i++) { printf("Hello %i.\n", i); } } @end ``` ### 方法呼叫 * Objective-C 呼叫方法的方式與其他語言不同,它不使用 `.`、`()` 符號,而 **使用方括號 `[]` 取代之**,其格式如下 ```objectivec= // 呼叫類方法 [類 方法名]; // 呼叫物件方法 [物件 方法名]; ``` 帶有參數的方法格式如下 ```objectivec= // 呼叫類方法 [類 方法名:參數]; // 呼叫物件方法 [物件 方法名:參數]; // 多參數物件方法 [物件 方法名1:參數1 方法名2:參數2]; ``` 範例如下: ```objectivec= void callClzMethod(void) { [MyFirstClz sayHello]; } void callInstanceMethod(void) { MyFirstClz *mfz = [MyFirstClz new]; [mfz sayWorld]; [mfz showTimes:5]; [mfz showHello:1 showWorld:3]; } ``` > ![](https://hackmd.io/_uploads/Hy-_ivMKh.png) ## 屬性 Objective-C 中,可以使用屬性來加強程式碼撰寫的速度、直觀性(也更符合物件導向封裝概念);它提供了便捷的設定、取得成員變數的方式 > 屬性 就像是對成員變數提供一個 setter/getter 函數,不讓外部成員直接訪問 :::info 有了屬性就相當於幫屬性設定了方法 ::: ### 宣告屬性 - @property * 在 Objective-C 中如果要修飾宣告成員變數(`.h` 檔),讓其變成屬性,就 **要使用 `@property` 標示**;標示後,該成員變數就不可直接存取,而必須透過屬性存取 格式: ```objectivec= @interface 類名: 超類 { ... } // 屬性要宣告在 `{}` 之外 @property 類型 成員變數名; @end ``` 範例: ```objectivec= @interface MyFirstClz: NSObject { ... } // 宣告屬性 @property int myPropertyValue; @end ``` :::info * 編譯器會幫我們隱式宣告 `@property` 描述的成員 ```objectivec= @interface MyFirstClz: NSObject { // 隱式建立 int myPropertyValue; } // 宣告屬性 @property int myPropertyValue; @end ``` ::: ### 定義屬性 - @synthesize * 宣告完屬性後,要實現屬性要切換到實現檔(`.m` 檔),並 **對宣告好的屬性使用 `@synthesize` 標註**(並且這時 **不用指定屬性**) 格式: ```objectivec= @interface 類名: 超類 { ... } // 屬性要宣告在 `{}` 之外,並且 不用指定數性 @synthesize 成員變數名; @end ``` 範例:記得要對應宣告檔的宣告屬性名 ```objectivec= @implementation MyFirstClz { } // 不用指定數性 @synthesize myPropertyValue; @end ``` ### 使用屬性 * 屬性其實就是編譯器幫我們建構的屬性的訪問方法,所以要使用數性就如同呼叫方法一樣;編譯器幫我們建構的方法規則如下 規則: | 原始成員 | 轉換後 getter | 轉換後 setter | | - | - | - | | helloProperty | helloProperty | setHelloProperty | > 主要還是 `setter` 改變 範例: ```objectivec= void callProperty(void) { MyFirstClz *mfc = [MyFirstClz new]; [mfc setMyPropertyValue:10]; int getVal = [mfc myPropertyValue]; printf("Property value: %i\n", getVal); } ``` > ![](https://hackmd.io/_uploads/ry_SunzF2.png) ### 帶參屬性 - 成員屬性方法 * 除了上述的 `@property` 直接幫我們宣告成員,我們還 **可以對已宣告成員 手動建立 setter/getter 方法(手動建立屬性)**; 這種方式可以讓我們 **對屬性有更多個操控**,格式如下 ```objectivec= @interface 類名: 超類 { 類型 成員名稱; } // 宣告 setter 屬性 -(void) set成員名稱: (參數類型) 參數名; // 宣告 getter 屬性 -(類型) 成員名稱; @end ``` **範例**: 1. **宣告**:寫在 `.h` 檔案 ```objectivec= @interface MyFirstClz: NSObject { int defaultValue; } // setter -(void) setDefaultValue: (int) value; // getter -(int) defaultValue; } ``` 2. **定義**:寫在 `.m` 檔案,如同定義一般方法 ```objectivec= @implementation MyFirstClz { } -(void) setDefaultValue: (int) value { // 可以在這定義更多個操作 defaultValue = value + 10; } -(int) defaultValue { return defaultValue; } @end ``` * 使用屬性 ```objectivec= void callPropertyWithParams(void) { MyFirstClz *mfc = [MyFirstClz new]; [mfc setDefaultValue:99]; int getVal = [mfc defaultValue]; printf("Property with params value: %i\n", getVal); } ``` > ![](https://hackmd.io/_uploads/SJRlC3zYh.png) ## 特殊屬性 帶參數性有幾個特殊設定值(它們與成員的無關),並個有不同功能,如下表(列出幾種常見的) | 特殊屬性 | 功能 | | - | - | | `assign` | 設定成員變量 | | `retain` | **釋放舊物件,並將舊物件的數值賦予成員變量** | | `copy` | 複製並建立一個新物件 | | `readonly` | 唯讀物件,編譯器不會產生 `setter` 方法 | | `readwrite` | 可讀寫物件(預設) | | `atomic` | 原子操作(預設),對於線程安全很重要 | | `nonatoic` | 設定不原子操作 | 並需搭配 `@property` 註釋方式,格式如下 ```objectivec= @interface 類名: 超類 { } @property("特殊屬性") 類型 成員名稱; @end ``` :::info * 這些屬性 **可以複合設定**,每個設定使用逗號 `,` 分開 > eg. `@property(nonatoic, retain, readwrite) 類型 成員名稱` ::: :::success * 成員變量與 `@property` 描述變量的相同,成員變量是否是必要的呢? 編譯器會根據 **`@property` 註解自動生成一個成員變量**,並為其生成默認的訪問方法。**通常情況下,這個成員變量是私有的,由編譯器自動合成** > 假設你重複設定同名的變量,也是可以通過編譯 ```objectivec= @interface 類名: 超類 { // 同名重複也 Okay 類型 成員名稱; } @property("特殊屬性") 類型 成員名稱; @end ``` ::: ### 特殊屬性 - assign * 特殊屬性 `assign` 可以用來 設定成員變量,範例如下 1. `@property` 宣告特殊屬性(`.h` 檔) ```objectivec= @interface MySecondClz: NSObject { } // 設定 assign 關鍵字 @property (assign) int protectVal; @end ``` 2. `@synthesize` 定義屬性(`.m` 檔) ```objectivec= @implementation MySecondClz { } @synthesize protectVal; @end ``` * 使用屬性 ```objectivec= void assignProperty(void) { MySecondClz *msc = [MySecondClz new]; [msc setProtectVal:300]; int getVal = [msc protectVal]; printf("Assign property value: %i\n", getVal); } ``` > ![](https://hackmd.io/_uploads/SkIKGs7Kh.png) ### 特殊屬性 - retain * 特殊屬性 `retain` 可以用來 **釋放舊物件,並將舊物件的數值賦予成員變量**,範例如下 1. `@property` 宣告特殊屬性(`.h` 檔) ```objectivec= @interface MySecondClz: NSObject { } // 設定 retain 關鍵字 @property (retain) NSString *str; @end ``` 2. `@synthesize` 定義屬性(`.m` 檔) ```objectivec= @implementation MySecondClz { } @synthesize str; @end ``` * 使用屬性 ```objectivec= void retainProperty(void) { MySecondClz *msc = [MySecondClz new]; [msc setStr:@"HelloWorld"]; NSString *getVal = [msc str]; printf("Retain property value: %s\n", [getVal UTF8String]); } ``` > ![](https://hackmd.io/_uploads/H1904iQF2.png) ### 特殊屬性 - readonly * 特殊屬性 `readonly` 可以用來 標示該屬性只能讀(**唯讀**)不能寫,範例如下 1. `@property` 宣告特殊屬性(`.h` 檔) ```objectivec= @interface MySecondClz: NSObject { } // 設定 readonly 關鍵字 @property (readonly) int roProperty; @end ``` 2. `@synthesize` 定義屬性(`.m` 檔) ```objectivec= @implementation MySecondClz { } @synthesize roProperty; @end ``` * 使用屬性 ```objectivec= void roProperty(void) { MySecondClz *msc = [MySecondClz new]; // Error,不可設定 [msc setRoProperty:123]; int getVal = [msc roProperty]; printf("Ro property value: %i\n", getVal); } ``` > 可以看到編譯期間 readonly 屬性用來設定,就會出錯 > > ![](https://hackmd.io/_uploads/By2-Lo7Y2.png) ## 例外處理 Objective-C 可以透過 `@try/@catch/@finally` 關鍵字來捕捉異常錯誤(像 Java),格式如下 ```objectivec= @try { } @catch (錯誤類型 錯誤變數) { } @finally { } ``` > `@try/@catch` 必須要寫,而 `@finally` 並非一定要寫 ### 拋出異常 - `@throw` * 使用 `@throw` 關鍵字,可以讓我們 **在程式運行期間拋出程式異常**(這通常使用在使用者沒有閱讀文檔,沒有按照文檔規範傳入參數時的處置) 範例: 1. 拋出 Objective-C 既有異常 `NSException` ```objectivec= void throwException(void) { @try { @throw [NSException exceptionWithName:@"Throw Exception" reason:@"Test Exception" userInfo:nil]; } @catch (NSException *exception) { NSLog(@"Get exception: %@", exception); } @finally { NSLog(@"Finish operation"); } } ``` > ![](https://hackmd.io/_uploads/BkKt0aas3.png) 2. 拋出自定義異常 * 自定義異常 ```objectivec= // 類定義 MyException.h @interface MyException: NSException { // 繼承 NSException } +(void) throwTest; @end // 類實作 -------------------------------------------- @implementation MyException { } // 類方法中拋出異常 +(void) throwTest { @throw [NSException exceptionWithName:@"Throw My Exception" reason:@"Test My Exception" userInfo:nil]; }; @end ``` * 測試拋出繼承 `NSException` 的異常 ```objectivec= void throwException2(void) { @try { // 呼叫類方法 [MyException throwTest]; } @catch (NSException *exception) { NSLog(@"Get exception: %@", exception); } @finally { NSLog(@"Finish operation"); } } ``` > ![](https://hackmd.io/_uploads/r1CEfC6on.png) ### 捕捉指定 Exception * 捕捉單一指定例外:以下指定捕捉 `NSException` 類型的意外 ```objectivec= void catchException(void) { NSString *str = [NSString new]; @try { char firstChar = [str characterAtIndex:0]; NSLog(@"First char: %c", firstChar); } @catch (NSException *exception) { NSLog(@"Get exception: %@", exception); } @finally { NSLog(@"Finish operation"); } } ``` > ![](https://hackmd.io/_uploads/ByOgYTps3.png) * 捕捉多種制定例外:以下捕捉 `MyException`、`NSException`、`id` 類型錯誤 ```objectivec= void catchMutliException(void) { NSString *str = [NSString new]; @try { char firstChar = [str characterAtIndex:0]; NSLog(@"First char: %c", firstChar); } @catch (MyException *exception) { NSLog(@"Get MyException:\n %@", exception); } @catch (NSException *exception) { NSLog(@"Get NSException:\n %@", exception); } @catch (id exception) { NSLog(@"Get idException:\n %@", exception); } @finally { NSLog(@"Finish operation"); } } ``` > ![](https://hackmd.io/_uploads/rJksjpao2.png) :::danger * 如果沒有指定到要捕捉的異常,仍會拋出,導致程式 Crash ::: ### 可預期錯誤 NSError * **Exception、Error 在 Objective-C 中的差異**:兩者的使用時機不同 * `Exception` 是一種不可預期的錯誤(用戶的非法行為) * `Error` 則是一種可預期錯誤(可預期行為,並回傳錯誤原因) * 在 Objective-C 中 Error 使用 `NSError` 類來表示,在使用時是將指標傳入方法中,當方法發生錯誤時,會填寫使用者傳入的指標 範例如下 ```objectivec= void useNSError(void) { NSFileManager *fm = [NSFileManager defaultManager]; NSError *error; [fm createDirectoryAtPath:@"" withIntermediateDirectories:NO attributes:nil error:&error]; if (error != nil) { NSLog(@"create diretory fail:\n %@", error); } } ``` > ![](https://hackmd.io/_uploads/Sk2-806jh.png) ## Appendix & FAQ :::info ::: ###### tags: `iOS`