本條要點:(作者總結)
- 應該用枚舉來表示狀態機的狀態、傳遞給方法的選項以及狀態碼等值,給這些值起個易懂的名字。
- 如果把傳遞給某個方法的選項表示為枚舉類型,而多個選項又可同時使用,那么就將各選項定義為 2 的冪,以便通過按位或操作將其組合起來。
- 用 NS_ENUM 與 NS_OPTIONS 宏來定義枚舉類型,並指明其底層數據類型。這樣做可以確保枚舉是用開發者所選的底層數據類型實現出來的,而不會采用編譯器所選的類型。
- 在處理枚舉類型的 switch 語句中不要實現 default 分支。這樣的話,加入新枚舉之后,編譯器就會提示開發者:switch 語句並未處理所有枚舉。
由於 Objective-C 基於 C 語言,所以 C 語言有的功能它都有。其中之一就是枚舉類型:enum。系統框架中頻繁使用此類型,然而開發者容易忽視它。在以一系列常量來表示錯誤狀態碼或可組合的選項時,極易使用枚舉為其命名。由於 C++11 標准擴充了枚舉的特性(一個字節含有 8 個二進制位,所以至多能表示可取 256 種(2 的 8 次方個)枚舉(編號為 0~255)的枚舉常量),所以最新版系統框架使用了“強類型”(strong type)的枚舉。沒錯,Objective-C 也能得益於 C++11 標准。
枚舉只是一種常量命名方式。某個對象所經歷的各種狀態就可以定義一個簡單的枚舉集(enumeration set)。比如說,可以用下列枚舉表示“套接字連接”(socket connection)的狀態:
1 enum EOCConnectionState { 2 EOCConnectionStateDisconnected, 3 EOCConnectionStateConnecting, 4 EOCConnectionStateConnected, 5 };
由於每種狀態都用一個便於理解的值來表示,所以這樣寫出來的代碼更易讀懂。編譯器會為枚舉分配一個獨有的編號,從 0 開始,每個枚舉值遞增 1 。實現枚舉所用的數據類型取決於編譯器,不過其二進制位(bit)的個數必須能完全表示下枚舉編號才行。在前例中,由於最大編號是 2,所以使用 1 個字節的 char 類型即可。
然而定義枚舉變量的方式卻不太簡潔,要依如下語法編寫:
1 enum EOCConnectionState state = EOCConnectionStateDisconnected;
若是每次不用敲入 enum 而只需要寫 EOCConnectionState 就好了。要想這樣,則需要使用 typedef 關鍵字重新定義枚舉類型:
1 enum EOCConnectionState { 2 EOCConnectionStateDisconnected, 3 EOCConnectionStateConnecting, 4 EOCConnectionStateConnected, 5 }; 6 typedef enum EOCConnectionState EOCConnectionState;
現在可以用簡寫的 EOCConnectionState 來代替完整的 enum EOCConnectionState 了:
1 EOCConnectionState state = EOCConnectionStateDisconnected;
C++11 標准修訂了枚舉的某些特性。其中一項改動是:可以指明用何種“底層數據類型”(underlying type)來保存枚舉類型的變量。這樣的好處是,可以向前聲明枚舉變量了。若不指定底層數據類型,則無法向前聲明枚舉類型,因為編譯器不清楚底層數據類型的大小,所以在用到此枚舉類型時,也就不知道究竟該給變量分配多少空間。
指定底層數據類型所用的語法是:
1 enum EOCConnectionStateConnectionState : NSInteger { /* ... */ };
上面這行代碼確保枚舉的底層數據類型是 NSInteger。也可以在向前聲明時指定底層數據類型:
1 enum EOCConnectionStateConnectionState: NSInteger;
還可以不使用編譯器所分配的序號,而是手工指定某個枚舉成員所對應的值。語法如下:
1 enum EOCConnectionState { 2 EOCConnectionStateDisconnected = 1, 3 EOCConnectionStateConnecting, 4 EOCConnectionStateConnected, 5 };
上述代碼把 EOCConnectionStateDisconnected 的值設為 1 ,而不使用編譯器所分配的 0 。如前所述,接下來幾個枚舉的值都會在上一個的基礎上遞增 1 。比如說,EOCConnectionStateConnected 的值就是 3。
還有一種情況應該使用枚舉類型,那就是定義選項的時候。若這些選項可以彼此組合,則更應如此。只要枚舉定義得對,各選項之間就可以通過“按位或操作符”(bitwise OR operator)來組合。例如,iOS UI 框架中有如下枚舉類型,用來表示某個視圖應該如何在水平或垂直方向上調整大小:
1 typedef NS_OPTIONS(NSUInteger, UIViewAutoresizing) { 2 UIViewAutoresizingNone = 0, 3 UIViewAutoresizingFlexibleLeftMargin = 1 << 0, 4 UIViewAutoresizingFlexibleWidth = 1 << 1, 5 UIViewAutoresizingFlexibleRightMargin = 1 << 2, 6 UIViewAutoresizingFlexibleTopMargin = 1 << 3, 7 UIViewAutoresizingFlexibleHeight = 1 << 4, 8 UIViewAutoresizingFlexibleBottomMargin = 1 << 5 9 };
每個選項均可啟用或禁用,使用上述方式來定義枚舉值即可保證這一點,因為在每個枚舉值(UIViewAutoresizingNone 除外,它點值是 0,對應的二進制值是 0,其中沒有值為 1 的二進制位)所對應的二進制表示中,只有一個二進制位的值是 1。用“按位或操作符”可組合多個選項,例如: UIViewAutoResizingFlexibleWidth | UIViewAutoresizingFlexibleHeight。圖列出了每個枚舉成員的二進制值,並演示了剛才那兩個枚舉組合之后的值。用“按位與操作符”(bitwise AND operator)即可判斷出是否已啟用某個選項:
1 enum UIViewAutoresizing resizing = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; 2 if (resizing & UIViewAutoresizingFlexibleWidth) { 3 // UIViewAutoresizingFlexibleWidth is set 4 }
1 UIViewAutoresizingFlexibleLeftMargin 000001 2 UIViewAutoresizingFlexibleWidth 000010 3 UIViewAutoresizingFlexibleRightMargin 000100 4 UIViewAutoresizingFlexibleTopMargin 001000 5 UIViewAutoresizingFlexibleHeight 010000 6 UIViewAutoresizingFlexibleBottomMargin 100000 7 8 UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight 010010
每個枚舉值的二進制表示,以及對其中兩個枚舉值執行按位或操作之后對二進制值。
系統庫中頻繁使用這個方法。iOS UI 框架中的 UIKit 里面還有個例子,用枚舉值告訴系統視圖所支持的設備顯示方向。這個枚舉類型叫做 UIInterfaceOrientationMask,開發者需要實現一個名為 supportedInterfaceOrientations 的方法,將視圖所支持的顯示方向高速系統:
1 - (UIInterfaceOrientationMask)supportedInterfaceOrientations { 2 return UIInterfaceOrientationMaskPortrait | UIInterfaceOrientationMaskLandscapeLeft; 3 }
Foundation 框架中定義了一些輔助的宏,用這些宏來定義枚舉類型時,也可以指定用於保存枚舉值的底層數據類型。這些宏具備向后兼容(backward compatibility)能力,如果目標平台的編譯器支持新標准,那就使用新式語法,否則改用舊式語法。這些宏是用 #define 預處理指令來定義的,其中一個用於定義像 EOCConnectionState 這種普通的枚舉類型,另一個用於定義像 UIViewAutoresizing 這種包含一系列選項的枚舉類型,其用法如下:
1 typedef NS_ENUM(NSUInteger, EOCConnectionState) { 2 EOCConnectionStateDisconnected, 3 EOCConnectionStateConnecting, 4 EOCConnectionStateConnected, 5 }; 6 7 typedef NS_OPTIONS(NSUInteger, EOCPermittedDirection) { 8 EOCPermittedDirectionUP = 1 << 0, 9 EOCPermittedDirectionDown = 1 << 1, 10 EOCPermittedDirectionLeft = 1 << 2, 11 EOCPermittedDirectionRight = 1 << 3, 12 };
這些宏的定義如下:
1 #define NS_ENUM(...) CF_ENUM(__VA_ARGS__) 2 #define NS_OPTIONS(_type, _name) CF_OPTIONS(_type, _name)
由於需要分別處理不同的情況,所以上述代碼用很多種方式來定義這兩個宏。第一個 #if 用於判斷編譯器是否支持新式枚舉。其中所用的布爾邏輯看上去相當復雜,不過其意思就是想判斷編譯器是否支持新的枚舉特性。如果不支持,那么就用老式語法來定義枚舉。
如果支持新特性,那么用 NS_ENUM 宏所定義的枚舉類型展開之后就是:
1 typedef enum EOCConnectionState : NSUInteger EOCConnectionState; 2 enum EOCConnectionState : NSUInteger { 3 EOCConnectionStateDisconnected, 4 EOCConnectionStateConnecting, 5 EOCConnectionStateConnected, 6 };
根據是否要將代碼按 C++ 模式編譯,NS_OPTIONS 宏的定義方式有所不同。如果不按 C++ 編譯,那么其展開方式就和 NS_ENUM 相同。若按 C++ 編譯,則展開后的代碼略有不同。原因在於,用按位或運算來操作兩個枚舉值時,C++ 編譯模式的處理辦法與非 C++ 模式不一樣。而上面已經提到了,作為選項的枚舉值經常需要用按位或運算來組合。在用或運算操作兩個枚舉值時,C++ 認為運算結果的數據類型應該是枚舉的底層數據類型,也就是NSUInteger。而且 C++ 不允許將這個底層類型“隱式轉換”(implicit cast)為枚舉類型本身。我們用 EOCPermittedDirection 來演示一下,假設按 NS_ENUM 方式將其展開:
1 typedef enum EOCPermittedDirection : int EOCPermittedDirection; 2 3 enum EOCPermittedDirection : int { 4 EOCPermittedDirectionUP = 1 << 0, 5 EOCPermittedDirectionDown = 1 << 1, 6 EOCPermittedDirectionLeft = 1 << 2, 7 EOCPermittedDirectionRight = 1 << 3, 8 };
然后考慮下列代碼:
1 EOCPermittedDirection permittedDirections = EOCPermittedDirectionLeft | EOCPermittedDirectionUP;
若編譯器按 C++ 模式編譯(也可能是按 Objective-C 模式編譯),則會給出下列錯誤信息:
1 error: cannot initialize a variable of type 2 'EOCPermittedDirection' with an rvalue of type 'int'
如果想編譯這行代碼,就要將按位或操作的結果顯示轉換(explicit cast)為 EOCPermittedDirection。所以,在C++ 模式下應該用另一種方式定義 NS_OPTIONS 宏,以便省去類型轉換操作。鑒於此,凡是需要以按位或操作來組合的枚舉都應該使用 NS_OPTIONS 定義。若是枚舉不需要互相組合,則應使用 NS_ENUM 來定義。
能夠用到枚舉的情況還有很多。前面已經提到,枚舉可以表示選項與狀態,然而還有許多東西也能用枚舉表示。比如狀態碼就是個好例子。可以把邏輯含義相似的一組狀態碼放入同一個枚舉集里,而不要用 #define 預處理指令或常量來定義。以枚舉來表示樣式(style)也很合宜。假設創建某個 UI 元素時可以使用不同的樣式,那么在這種情況下就最應該把樣式聲明為枚舉類型了。
最后再講一種枚舉的用法,就是在 switch 語句里,有時可以這樣定義:
1 typedef NS_ENUM(NSUInteger, EOCConnectionState) { 2 EOCConnectionStateDisconnected, 3 EOCConnectionStateConnecting, 4 EOCConnectionStateConnected, 5 }; 6 7 8 switch (_currentState) { 9 case EOCConnectionStateDisconnected: 10 { 11 // Handle disconnected state 12 } 13 break; 14 case EOCConnectionStateConnecting: 15 { 16 // handle connecting state 17 } 18 break; 19 case EOCConnectionStateConnected: 20 { 21 // handle connected state 22 } 23 break; 24 }
我們總是習慣在 switch 語句中加上 default 分支。然而,若是用枚舉來定義狀態機(state machine),則最好不要有 default 分支。這樣的話,如果稍后又加了一種狀態,那么編譯器就會發出警告信息,提示新加入的狀態並未在 switch 分支中處理。假如寫上了 default 分支,那么它就會處理這個新狀態,從而導致編譯器不發出警告信息。用 NS_ENUM 定義其他枚舉類型時也要注意此問題。例如,在定義代表 UI 元素的枚舉時,通常要確保 switch 語句能正確處理所有樣式。
END