《Effective Objective-C 2.0》- 5:用列舉來表示狀態、選項、狀態碼
由於 Objective-C 基於 C 語言,所以 C 語言有的功能它都有。其中之一就是列舉型別:enum。系統框架中頻繁使用此型別,然而開發者容易忽視它。在以一系列常量來表示錯誤狀態碼或可組合的選項時,極易使用列舉為其命名。
列舉只是一種常量命名方式。某個物件所經歷的各種狀態就可以定義一個簡單的列舉集(enumeration set)。比如說,可以用下列列舉表示“套接字連線”(socket connection)的狀態:
enum EOCConnectionState {
EOCConnectionStateDisconnected,
EOCConnectionStateConnecting,
EOCConnectionStateConnected,
};
由於每種狀態都用一個便於理解的值來表示,所以這樣寫出來的程式碼更易讀懂。編譯器會為列舉分配一個獨有的編號,從 0 開始,每個列舉值遞增 1 。實現列舉所用的資料型別取決於編譯器,不過其二進位制位(bit)的個數必須能完全表示下列舉編號才行。在前例中,由於最大編號是 2,所以使用 1 個位元組的 char 型別即可。
然而定義列舉變數的方式卻不太簡潔,要依如下語法編寫:
enum EOCConnectionState state = EOCConnectionStateDisconnected;
若是每次不用敲入 enum 而只需要寫 EOCConnectionState 就好了。要想這樣,則需要使用 typedef 關鍵字重新定義列舉型別:
enum EOCConnectionState {
EOCConnectionStateDisconnected,
EOCConnectionStateConnecting,
EOCConnectionStateConnected,
};
typedef enum EOCConnectionState EOCConnectionState;
現在可以用簡寫的 EOCConnectionState 來代替完整的 enum EOCConnectionState 了:
EOCConnectionState state = EOCConnectionStateDisconnected;
C++11 標準修訂了列舉的某些特性。其中一項改動是:可以指明用何種“底層資料型別”(underlying type)來儲存列舉型別的變數。這樣的好處是,可以向前宣告列舉變數了。若不指定底層資料型別,則無法向前宣告列舉型別,因為編譯器不清楚底層資料型別的大小,所以在用到此列舉型別時,也就不知道究竟該給變數分配多少空間。
指定底層資料型別所用的語法是:
enum EOCConnectionStateConnectionState : NSInteger { /* ... */ };
上面這行程式碼確保列舉的底層資料型別是 NSInteger。也可以在向前宣告時指定底層資料型別:
enum EOCConnectionStateConnectionState: NSInteger;
還可以不使用編譯器所分配的序號,而是手工指定某個列舉成員所對應的值。語法如下:
enum EOCConnectionState {
EOCConnectionStateDisconnected = 1,
EOCConnectionStateConnecting,
EOCConnectionStateConnected,
};
上述程式碼把 EOCConnectionStateDisconnected 的值設為 1 ,而不使用編譯器所分配的 0 。如前所述,接下來幾個列舉的值都會在上一個的基礎上遞增 1 。比如說,EOCConnectionStateConnected 的值就是 3。
還有一種情況應該使用列舉型別,那就是定義選項的時候。若這些選項可以彼此組合,則更應如此。只要列舉定義得對,各選項之間就可以通過“按位或操作符”(bitwise OR operator)來組合。例如,iOS UI 框架中有如下列舉型別,用來表示某個檢視應該如何在水平或垂直方向上調整大小:
typedef NS_OPTIONS(NSUInteger, UIViewAutoresizing) {
UIViewAutoresizingNone = 0,
UIViewAutoresizingFlexibleLeftMargin = 1 << 0,
UIViewAutoresizingFlexibleWidth = 1 << 1,
UIViewAutoresizingFlexibleRightMargin = 1 << 2,
UIViewAutoresizingFlexibleTopMargin = 1 << 3,
UIViewAutoresizingFlexibleHeight = 1 << 4,
UIViewAutoresizingFlexibleBottomMargin = 1 << 5
};
每個選項均可啟用或禁用,使用上述方式來定義列舉值即可保證這一點,因為在每個列舉值(UIViewAutoresizingNone 除外,它點值是 0,對應的二進位制值是 0,其中沒有值為 1 的二進位制位)所對應的二進位制表示中,只有一個二進位制位的值是 1。用“按位或操作符”可組合多個選項,例如: UIViewAutoResizingFlexibleWidth | UIViewAutoresizingFlexibleHeight。圖列出了每個列舉成員的二進位制值,並演示了剛才那兩個列舉組合之後的值。用“按位與操作符”(bitwise AND operator)即可判斷出是否已啟用某個選項:
enum UIViewAutoresizing resizing = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
if (resizing & UIViewAutoresizingFlexibleWidth) {
// UIViewAutoresizingFlexibleWidth is set
}
UIViewAutoresizingFlexibleLeftMargin 000001
UIViewAutoresizingFlexibleWidth 000010
UIViewAutoresizingFlexibleRightMargin 000100
UIViewAutoresizingFlexibleTopMargin 001000
UIViewAutoresizingFlexibleHeight 010000
UIViewAutoresizingFlexibleBottomMargin 100000
UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight 010010
每個列舉值的二進位制表示,以及對其中兩個列舉值執行按位或操作之後對二進位制值。
系統庫中頻繁使用這個方法。iOS UI 框架中的 UIKit 裡面還有個例子,用列舉值告訴系統檢視所支援的裝置顯示方向。這個列舉型別叫做 UIInterfaceOrientationMask,開發者需要實現一個名為 supportedInterfaceOrientations 的方法,將檢視所支援的顯示方向高速系統:
- (UIInterfaceOrientationMask)supportedInterfaceOrientations {
return UIInterfaceOrientationMaskPortrait | UIInterfaceOrientationMaskLandscapeLeft;
}
Foundation 框架中定義了一些輔助的巨集,用這些巨集來定義列舉型別時,也可以指定用於儲存列舉值的底層資料型別。這些巨集具備向後相容(backward compatibility)能力,如果目標平臺的編譯器支援新標準,那就使用新式語法,否則改用舊式語法。這些巨集是用 #define 預處理指令來定義的,其中一個用於定義像 EOCConnectionState 這種普通的列舉型別,另一個用於定義像 UIViewAutoresizing 這種包含一系列選項的列舉型別,其用法如下:
typedef NS_ENUM(NSUInteger, EOCConnectionState) {
EOCConnectionStateDisconnected,
EOCConnectionStateConnecting,
EOCConnectionStateConnected,
};
typedef NS_OPTIONS(NSUInteger, EOCPermittedDirection) {
EOCPermittedDirectionUP = 1 << 0,
EOCPermittedDirectionDown = 1 << 1,
EOCPermittedDirectionLeft = 1 << 2,
EOCPermittedDirectionRight = 1 << 3,
};
這些巨集的定義如下:
#define NS_ENUM(...) CF_ENUM(__VA_ARGS__)
#define NS_OPTIONS(_type, _name) CF_OPTIONS(_type, _name)
由於需要分別處理不同的情況,所以上述程式碼用很多種方式來定義這兩個巨集。第一個 #if 用於判斷編譯器是否支援新式列舉。其中所用的布林邏輯看上去相當複雜,不過其意思就是想判斷編譯器是否支援新的列舉特性。如果不支援,那麼就用老式語法來定義列舉。
如果支援新特性,那麼用 NS_ENUM 巨集所定義的列舉型別展開之後就是:
typedef enum EOCConnectionState : NSUInteger EOCConnectionState;
enum EOCConnectionState : NSUInteger {
EOCConnectionStateDisconnected,
EOCConnectionStateConnecting,
EOCConnectionStateConnected,
};
根據是否要將程式碼按 C++ 模式編譯,NS_OPTIONS 巨集的定義方式有所不同。如果不按 C++ 編譯,那麼其展開方式就和 NS_ENUM 相同。若按 C++ 編譯,則展開後的程式碼略有不同。原因在於,用按位或運算來操作兩個列舉值時,C++ 編譯模式的處理辦法與非 C++ 模式不一樣。而上面已經提到了,作為選項的列舉值經常需要用按位或運算來組合。在用或運算操作兩個列舉值時,C++ 認為運算結果的資料型別應該是列舉的底層資料型別,也就是NSUInteger。而且 C++ 不允許將這個底層型別“隱式轉換”(implicit cast)為列舉型別本身。我們用 EOCPermittedDirection 來演示一下,假設按 NS_ENUM 方式將其展開:
typedef enum EOCPermittedDirection : int EOCPermittedDirection;
enum EOCPermittedDirection : int {
EOCPermittedDirectionUP = 1 << 0,
EOCPermittedDirectionDown = 1 << 1,
EOCPermittedDirectionLeft = 1 << 2,
EOCPermittedDirectionRight = 1 << 3,
};
然後考慮下列程式碼:
EOCPermittedDirection permittedDirections = EOCPermittedDirectionLeft | EOCPermittedDirectionUP;
若編譯器按 C++ 模式編譯(也可能是按 Objective-C 模式編譯),則會給出下列錯誤資訊:
error: cannot initialize a variable of type
'EOCPermittedDirection' with an rvalue of type 'int'
如果想編譯這行程式碼,就要將按位或操作的結果顯示轉換(explicit cast)為 EOCPermittedDirection。所以,在C++ 模式下應該用另一種方式定義 NS_OPTIONS 巨集,以便省去型別轉換操作。鑑於此,凡是需要以按位或操作來組合的列舉都應該使用 NS_OPTIONS 定義。若是列舉不需要互相組合,則應使用 NS_ENUM 來定義。
能夠用到列舉的情況還有很多。前面已經提到,列舉可以表示選項與狀態,然而還有許多東西也能用列舉表示。比如狀態碼就是個好例子。可以把邏輯含義相似的一組狀態碼放入同一個列舉集裡,而不要用 #define 預處理指令或常量來定義。以列舉來表示樣式(style)也很合宜。假設建立某個 UI 元素時可以使用不同的樣式,那麼在這種情況下就最應該把樣式宣告為列舉型別了。
最後再講一種列舉的用法,就是在 switch 語句裡,有時可以這樣定義:
typedef NS_ENUM(NSUInteger, EOCConnectionState) {
EOCConnectionStateDisconnected,
EOCConnectionStateConnecting,
EOCConnectionStateConnected,
};
switch (_currentState) {
case EOCConnectionStateDisconnected:
{
// Handle disconnected state
}
break;
case EOCConnectionStateConnecting:
{
// handle connecting state
}
break;
case EOCConnectionStateConnected:
{
// handle connected state
}
break;
}
我們總是習慣在 switch 語句中加上 default 分支。然而,若是用列舉來定義狀態機(state machine),則最好不要有 default 分支。這樣的話,如果稍後又加了一種狀態,那麼編譯器就會發出警告資訊,提示新加入的狀態並未在 switch 分支中處理。假如寫上了 default 分支,那麼它就會處理這個新狀態,從而導致編譯器不發出警告資訊。用 NS_ENUM 定義其他列舉型別時也要注意此問題。例如,在定義代表 UI 元素的列舉時,通常要確保 switch 語句能正確處理所有樣式。
總結:
- 應該用列舉來表示狀態機的狀態、傳遞給方法的選項以及狀態碼等值,給這些值起個易懂的名字。
- 如果把傳遞給某個方法的選項表示為列舉型別,而多個選項又可同時使用,那麼就將各選項定義為 2 的冪,以便通過按位或操作將其組合起來。
- 用 NS_ENUM 與 NS_OPTIONS 巨集來定義列舉型別,並指明其底層資料型別。這樣做可以確保列舉是用開發者所選的底層資料型別實現出來的,而不會採用編譯器所選的型別。
- 在處理列舉型別的 switch 語句中不要實現 default 分支。這樣的話,加入新列舉之後,編譯器就會提示開發者:switch 語句並未處理所有列舉。
相關文章
- 05@多用列舉表示狀態、選項、狀態碼
- HTTP狀態碼列舉(PHP)HTTPPHP
- 使用列舉實現狀態機來優雅你的狀態變更邏輯
- 狀態列
- PyQt5 之狀態列QT
- 狀態碼
- 沉浸式狀態列
- 狀態列相關
- Android 狀態列透明Android
- 直播app原始碼,狀態列和導航欄設定成透明狀態APP原始碼
- http狀態碼HTTP
- http 狀態碼HTTP
- 【譯】Effective TensorFlow Chapter2——理解靜態和動態形狀APT
- [hyperf]分享記錄一下常用http狀態碼的列舉類HTTP
- java狀態模式例項解析Java模式
- Http狀態碼整理HTTP
- HTTP狀態碼:415HTTP
- Android獲取狀態列高度Android
- React Native 中的狀態列React Native
- react-native android狀態列ReactAndroid
- 狀態列Theme相關配置
- Android全屏與透明狀態列Android
- iOS狀態列相關操作iOS
- Flutter改變狀態列字型、狀態列背景顏色、Appbar背景顏色的方式FlutterAPP
- 前端狀態管理與有限狀態機前端
- C/C++ Qt StatusBar 底部狀態列應用C++QT
- 使用 ATX 判斷單選框選中狀態、開關狀態、圖示型別型別
- 常用的HTTP狀態碼HTTP
- 最全的 http 狀態碼HTTP
- HTTP方法及狀態碼HTTP
- 伺服器狀態碼伺服器
- HTTP 響應狀態碼HTTP
- 後端的狀態碼後端
- 常見的狀態碼
- HTTP狀態碼詳解HTTP
- http狀態碼(搬運)HTTP
- HTTP狀態碼的理解HTTP
- win10怎麼把狀態列變透明_win10狀態列變透明方法Win10