---
# System prepended metadata

title: Objective-C 物件導向、例外
tags: [iOS]

---

---
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`
