共用方式為


CLR 列舉型別

從 Managed Extensions for C++ 升級為 Visual C++ 之後,列舉的宣告和行為已變更。

Managed Extensions 列舉宣告之前必須搭配 __value 關鍵字。 此處的要點是能分辨原生 (Native) 列舉和衍生自 System::ValueType 的 CLR 列舉,同時也建議類似的功能。 例如:

__value enum e1 { fail, pass };
public __value enum e2 : unsigned short  { 
   not_ok = 1024, 
   maybe, ok = 2048 
};

新語法解決原生和 CLR 列舉之間分辨問題的方法,是強調後者的類別本質,而非實值型別根項目。 因此會捨棄 __value 關鍵字,改用一組間隔關鍵字 enum class 取代。 這種做法會提供一組關鍵字對稱給參考、實值和介面類別的宣告:

enum class ec;
value class vc;
ref class rc;
interface class ic;

在新語法中,列舉型別組 e1 和 e2 轉譯如下所示:

enum class e1 { fail, pass };
public enum class e2 : unsigned short { 
   not_ok = 1024,
   maybe, ok = 2048 
};

除了這個小小的語法變更以外,CLR 列舉型別的行為在不同方面也有所變更:

  • 不再支援 CLR 列舉的向前宣告。 沒有對應, 只是將它標示為編譯時期錯誤。
__value enum status; // Managed Extensions: ok
enum class status;   // new syntax: error
  • 內建算術型別和 Object 類別階層之間的多載解析,已於兩種語言版本之間回復! 副作用是,CLR 列舉無法再隱含地轉換成算術型別。

  • 在新語法中,CLR 列舉會維護本身的範圍,但在 Managed Extensions 則不是如此。 以前,在列舉的包含範圍內看得到列舉值。 現在,列舉值則封裝於列舉範圍內。

CLR 列舉是一種物件

請考慮下列程式碼片段:

__value enum status { fail, pass };

void f( Object* ){ Console::WriteLine("f(Object)\n"); }
void f( int ){ Console::WriteLine("f(int)\n"); }

int main()
{
   status rslt = fail;

   f( rslt ); // which f is invoked?
}

對於原生 C++ 的程式設計人員而言,叫用多載的 f() 執行個體就是 f(int) 的執行個體。 列舉是符號整數常數,而且它會參與在此情況下會優先考慮的標準整數提升。 而且實際上在 Managed Extensions 中,這是呼叫要解析的執行個體。 這樣會製造一些驚喜,不是刻意在原生 C++ 框架 (Frame) 中使用時,而是當需要用來和現有的 BCL (基底類別庫) 架構互動時,其中 Enum 是間接衍生自 Object 的類別。 在 Visual C++ 語言設計中,所叫用之 f() 的執行個體是 f(Object^) 的執行個體。

Visual C++ 選擇用來強制施行的方式,是不支援 CLR 列舉型別和算術型別之間的隱含轉換。 這表示將任何 CLR 列舉型別物件指派至算數型別時,需要明確的轉換 (Cast)。 所以比方說指定

void f( int );

做為非多載方法時,在 Managed Extensions 中可以呼叫

f( rslt ); // ok: Managed Extensions; error: new syntax

,而且 rslt 中內含的值會隱含地轉換為整數值。 在 Visual C++ 中編譯這項呼叫時則會失敗。 若要正確地轉譯,則必須插入轉換運算子:

f( safe_cast<int>( rslt )); // ok: new syntax

CLR 列舉型別的範圍

C 和 C++ 語言之間的其中一項改變,就是在 C++ 中增加結構 (Struct) 機能的範圍。 在 C 中,結構只是缺少介面或相關範圍支援的資料彙總 (Aggregate)。 這在當時是一項很根本的改變,而且對於許多剛從 C 語言改用 C++ 的新使用者而言是個會引起爭議的問題。 原生和 CLR 列舉之間的關係很相似。

在 Managed Extensions 中,會嘗試針對 CLR 列舉的列舉值定義弱式插入名稱,以便在原生列舉中模擬缺少範圍的情況。 這樣做不保證一定能成功。 問題在於這種做法會使列舉值落入全域命名空間,導致很難管理名稱衝突。 在新語法中,則已符合 CLR 列舉的支援範圍中的其他 CLR 語言。

也就是說,如果不是以合格方式使用 CLR 列舉的列舉值,新語法就無法辨認此值。 我們來看看實際範例。

// Managed Extensions supporting weak injection
__gc class XDCMake {
public:
   __value enum _recognizerEnum { 
      UNDEFINED,
      OPTION_USAGE, 
      XDC0001_ERR_PATH_DOES_NOT_EXIST = 1,
      XDC0002_ERR_CANNOT_WRITE_TO = 2,
      XDC0003_ERR_INCLUDE_TAGS_NOT_SUPPORTED = 3,
      XDC0004_WRN_XML_LOAD_FAILURE = 4,
      XDC0006_WRN_NONEXISTENT_FILES = 6,
   };

   ListDictionary* optionList;
   ListDictionary* itagList;

   XDCMake() {
      optionList = new ListDictionary;

      // here are the problems …
      optionList->Add(S"?", __box(OPTION_USAGE)); // (1)
      optionList->Add(S"help", __box(OPTION_USAGE)); // (2)

      itagList = new ListDictionary;
      itagList->Add(S"returns", 
         __box(XDC0004_WRN_XML_LOAD_FAILURE)); // (3)
   }
};

對於下列三種資格不符的列舉值名稱用法而言 (亦即 (1)、(2) 和 (3)),每一種都需要在轉譯至新語法的程序中定義為合格,如此才能編譯原始程式碼。 下列是原本的原始程式碼的正確轉譯:

ref class XDCMake {
public:
   enum class _recognizerEnum {
      UNDEFINED, OPTION_USAGE, 
      XDC0001_ERR_PATH_DOES_NOT_EXIST = 1,
      XDC0002_ERR_CANNOT_WRITE_TO = 2,
      XDC0003_ERR_INCLUDE_TAGS_NOT_SUPPORTED = 3,
      XDC0004_WRN_XML_LOAD_FAILURE = 4,
      XDC0006_WRN_NONEXISTENT_FILES = 6
   };

   ListDictionary^ optionList;
   ListDictionary^ itagList;

   XDCMake() {
      optionList = gcnew ListDictionary;
      optionList->Add("?",_recognizerEnum::OPTION_USAGE); // (1)
      optionList->Add("help",_recognizerEnum::OPTION_USAGE); //(2)
      itagList = gcnew ListDictionary;
      itagList->Add( "returns", 
         _recognizerEnum::XDC0004_WRN_XML_LOAD_FAILURE); //(3)
   }
};

這樣會改變原生和 CLR 列舉之間的設計策略。 在 Visual C++ 中使用 CLR 列舉維護相關範圍時,於類別中封裝列舉宣告是不必要也是無效的做法。 這個慣用語是在貝爾實驗室研發 cfront 2.0 時逐漸形成,同時也是用來解決全域名稱干擾問題。

在貝爾實驗室中,由 Jerry Schwarz 所研發的新 iostream 程式庫的原始 Beta 版中,Jerry 並未封裝所有針對實驗室所定義的相關列舉,而且 read、write、append 等一般列舉值讓使用者幾乎不可能編譯現有程式碼。 第一個解決方法是破壞名稱,使其語意不明,例如 io_read、io_write 等等。 第二個解決方法是將範圍加入至列舉以便修改語言,但這在當時行不通。 而居中的解決方法則是封裝類別中的列舉或類別階層,其中列舉的標記名稱和列舉值都會填入 (Populate) 封入類別 (Enclosing Class) 範圍。換句話說,在類別中放置列舉的動機在當初看來不明智,但卻是因應全域命名空間干擾問題的可行做法。

有了 Visual C++ 列舉,在類別中封裝列舉就再也沒有任何值得讚賞的優點了。 事實上,如果查看 System 命名空間,將會看到列舉、類別和介面全都位於相同的宣告空間中。

請參閱

參考

enum class (C++ 元件擴充功能)

概念

實值型別和行為 (C++/CLI)