---
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` 標記
> 
:::
範例:
```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);
```
> 
### 變數類型
* 變數可以透過關鍵字去描述該變數的作用域、生命週期;其中常見的變數類型如下表
| 關鍵字 | 概述 |
| - | - |
| `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` 方法內
> 
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;
```
> 
## 方法
一般來說在物件導向的程式設計中,我們會把以往的函數稱之為方法,它存在更強的區域性(封裝)關係
### 方法宣告 - `.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];
}
```
> 
## 屬性
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);
}
```
> 
### 帶參屬性 - 成員屬性方法
* 除了上述的 `@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);
}
```
> 
## 特殊屬性
帶參數性有幾個特殊設定值(它們與成員的無關),並個有不同功能,如下表(列出幾種常見的)
| 特殊屬性 | 功能 |
| - | - |
| `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);
}
```
> 
### 特殊屬性 - 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]);
}
```
> 
### 特殊屬性 - 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 屬性用來設定,就會出錯
>
> 
## 例外處理
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");
}
}
```
> 
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");
}
}
```
> 
### 捕捉指定 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");
}
}
```
> 
* 捕捉多種制定例外:以下捕捉 `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");
}
}
```
> 
:::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);
}
}
```
> 
## Appendix & FAQ
:::info
:::
###### tags: `iOS`