第5條:用枚舉表示狀態、選項、狀態碼


  本條要點:(作者總結)

  • 應該用枚舉來表示狀態機的狀態、傳遞給方法的選項以及狀態碼等值,給這些值起個易懂的名字。
  • 如果把傳遞給某個方法的選項表示為枚舉類型,而多個選項又可同時使用,那么就將各選項定義為 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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM