程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> C# >> 關於C# >> 從模擬字符串型的枚舉說起

從模擬字符串型的枚舉說起

編輯:關於C#

1. 有字符串型的枚舉嗎?

UK 在《關於枚舉的種種》中提到這樣一個問題:

枚舉的成員類型都是數值型的,如果想做一個字符型的枚舉有什麼辦法?

enum colors : string{
    red='#ff0000',

  }

在展開討論之前,我認為有必要搞清楚另一個問題,上面代碼中的 '#ff0000' 不是字符而是字符串,應改成 "#ff0000",於是,UK 的問題也順利成章地改成“想做一個字符串型的枚舉有什麼辦法”了。

很坦白地說,.NET 並不支持這樣的字符串型枚舉。另外,也不是所有的數值類型都能作為枚舉的底層類型,枚舉的底層類型只能是整數類型,這意味著某天你想定義一個底層類型為 double 的枚舉,你將會收到編譯器的警告信。

UK 的代碼確實提出了這樣一種需求,colors 就像一個枚舉,但其成員的值是一個字符串,於是,我們很容易產生這樣一個疑問:是否能夠模擬出這樣一個枚舉呢?

2. 有辦法模擬出來嗎?

在寫任何代碼之前,讓我們首先思考一下,假如真有這樣一個 Color 類,我們會怎樣使用它?以下是我想到的兩個用法:

1// Code #01
2
3Color c1 = Color.Red;
4string s1 = (string)c1;
5Debug.Assert(s1 == "#FF0000");
6
7Color c2 = (Color)"#00FF00";
8Debug.Assert(c2 == Color.Green);

首先,枚舉是通過其成員而不是構造函數來初始化的,正如上面代碼的第三行。於是,我們應該把構造函數設為私有,同時模擬一些枚舉成員並暴露出來:

// Code #02

public class Color
{
  private Color(string value)
  {
    m_Value = value;
  }

  private string m_Value;

  public static readonly Color Red = new Color("#FF0000");
  public static readonly Color Green = new Color("#00FF00");
  public static readonly Color Blue = new Color("#0000FF");
}

這樣,Code #01 的第三行就可以工作了。值得提醒的是,Red、Green 和 Blue 等成員的值都是不變的,為了不讓客戶代碼修改它們的值,我把它們設成 readonly(注意:它們不可以設為 const,你知道為什麼嗎?)。另外,它們應該屬於 Color 類而不是某個實例的,於是把它們設成 static。

接著,Code #01 的第四行說明了 Color 的實例可以強制轉換成字符串,這可以通過重載轉換運算符做到:

// Code #03

public static explicit operator string(Color value)
{
  return value.m_Value;
}
與 Code #01 的第四行對應的是 Code #01 的第七行,它說明了字符串可以強制轉換成 Color 實例,這也是通過重載轉換運算符做到的:

// Code #04

public static explicit operator Color(string value)
{
  switch (value)
  {
    case "#FF0000":
      return Red;
    case "#00FF00":
      return Green;
    case "#0000FF":
      return Blue;
    default:
      throw new InvalidCastException();
  }
}

很明顯,並不是所有的字符串都可以轉換成 Color 實例,所以,當客戶代碼試圖把一個含有非預期值的字符串轉換成 Color 實例時就應該拋出 InvalidCastException 了。

誠然,有些人會對 Code #04 很反感,因為它裡面有一個 switch!當我們使用枚舉時,我們並不只是用它來進行一些值的轉換或者獲取其字面值,我們更希望用它來標識不同的情況或者類別,並根據枚舉實例的值來判斷屬於哪一情況或類別。換句話說,當我們決定使用枚舉時,我們就注定與條件語句結下不解之緣了。

最後,這裡給出 Color 的完整代碼:

// Code #05

Color#region Color

public class Color
{
  private Color(string value)
  {
    m_Value = value;
  }

  public static explicit operator Color(string value)
  {
    switch (value)
    {
      case "#FF0000":
        return Red;
      case "#00FF00":
        return Green;
      case "#0000FF":
        return Blue;
      default:
        throw new InvalidCastException();
    }
  }

  public static explicit operator string(Color value)
  {
    return value.m_Value;
  }

  public override string ToString()
  {
    return m_Value;
  }

  public static readonly Color Red = new Color("#FF0000");
  public static readonly Color Green = new Color("#00FF00");
  public static readonly Color Blue = new Color("#0000FF");

  private string m_Value;
}

#endregion

或許有人會提出這樣一個問題:我們經常會對枚舉進行判等運算,但為什麼 Code #05 中既沒有重載 Object.Equals 方法,也沒有提供 == 和 != 運算符呢?細心觀察 Color 的代碼,你會發現獲取 Color 的實例只有兩種途徑:通過靜態只讀字段和通過強制轉換運算符,但無論你是如何得到 Color 的實例,這些實例最終都是源自 Color 內部的靜態只讀字段。換言之,通過這兩種途徑分別獲得的兩個 Color 實例其實是同一個實例。

3. 模擬方案有什麼問題嗎?

我相信,Color 在一定程度上能夠滿足 UK 的需求了,但是,我認為 Color 並不一定能用到實際的應用中去。回顧 Code #05,Color 既是一個枚舉也是一個數據載體。說它是枚舉,是因為我們刻意把它模擬成枚舉;說它是一個數據載體,是因為我們並不僅僅以枚舉的方式來用它,我們更需要的是成員背後所代表的值。這意味著 Color 承擔的責任多於一個,違反了單一責任原則(SRP)。

我懷疑,UK 之所以對 Color 有這樣的期望,或多或少是受到了《關於枚舉的種種》中枚舉成員的值那部分內容的影響,尤其是“為什麼需要手動指定枚舉成員的值?”這個問題的答案。如果真是這樣,我必須就該文產生誤導在此向你道歉。現實的情況往往不會像該文的 Code #13 那樣簡單,不但同一類型的顧客(例如白金會員)所能享受到的折扣隨時會發生變化,而且同一的商品在不同的時期(例如促銷期)的折扣也有可能不同,於是顧客最終所能享受到的折扣可能是一個經過復雜運算的綜合折扣。任何對變化因素的硬編碼都會導致系統的僵化!我建議在閱讀該文這部分內容時應以研究枚舉的這方面特性為目的。

另一方面,Color 作為一個數據載體,它確實弱得可憐。目前它僅包含 R、G、B 三個通道的數據,如果我想加入 alpha 通道的數據呢?這會對它的代碼產生多大的沖擊?如果我希望它能分別為我提取 A、R、G、B 四個通道的數據呢?如果我希望實現 RGB 和 CMYK 之間的數據轉換呢?我相信這些問題已經足夠讓你頭痛一周了,但當你知道人的肉眼能夠識別的顏色約有一千六百萬種,而這些顏色都可以通過 RGB 值來描述並作為顯示器輸出的依據,你的鼠標會不會馬上指向浏覽器右上角的交叉呢?

很明顯,這裡所給出的 Color 是經不起時間的考驗的,於是我不禁在想,.NET Framework 究竟如何表達 Color 呢?它又是如何滿足這些讓人苦悶的需求呢?

4. .NET Framework 又如何表達 Color 呢?

在 .NET Framework 中,和這裡所提到的需求相關的東西有3個:Color 結構、KnownColor 枚舉和 ColorTranslator 類,它們都位於 System.Drawing 命名空間中。

首先說說 KnownColor 枚舉,它的成員可以分成兩類:一類是有名字的顏色,與它們對應的 RGB 值是不變的,如 Indigo 是靛藍;另一類是外觀項目的顏色,與它們對應的 RGB 值會隨著用戶的設置而改變,如 InfoText 是工具提示文本的顏色。

其次是 Color 結構了,它有很多靜態屬性,這些屬性都是獲取與 KnownColor 枚舉中第一類成員對應的 Color 實例。它還提供 FromKnownColor 和 ToKnownColor 方法用於實現它的實例和 KnownColor 枚舉之間的轉換。當然,由於它是一個數據載體,它包含了 A、R、G、B 四個通道的數據,對它的實例的判等就相當於對這些數據進行判等,於是它也重載了 == 和 != 運算符。

說了這麼多,好像還沒有提到如何從 Color 實例得到 HTML 顏色值,好吧,現在是時候讓 ColorTranslator 類登場了。該類提供 ToHtml 和 FromHtml 方法用於實現 Color 實例和 HTML 顏色值的字符串之間的轉換。

這三個東西的責任都很明晰,當你需要判斷某個地方用的是否某種顏色時,沒有必要去比較它們的 A、R、G、B 四個通道的數據,你真正需要的只是一種快而准的標識對比,KnownColor 枚舉恰恰就能滿足這方面的要求;當你需要操作某種顏色的數據時,你其實並不希望操作一個字符串,Color 結構能讓你輕易提取所需的數據;當你在顏色數據和 HTML 顏色值的字符串之間進行轉換時,你其實並不太需要一個枚舉來做標識。然而,你也有可能結合它們三個一起使用,下面給出一個例子作為本文的收尾:

// Code #06

string desktopColorValue = ColorTranslator.ToHtml(Color.FromKnownColor(KnownColor.Desktop));

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved