昨天在.NET中的枚舉值(一)中我提到,如果將該文中的實現進一步架構,提煉出一個抽象類別作為自訂枚舉類型的基類的話,肯定會對後續開發有很好的協助。今天我們就來繼續探討一下!
需要補充的是,在第一集中我就說過,這種使用類或結構來替代枚舉類型的方案並不能替代所有的內建枚舉值,而是在必要的時候用用即可。鑽牛角尖的朋友們要小心在意了!
這個方案貌似是可以重構的。但是在上一集中我們應該留意到,代碼中重寫了很多運算子,運算子是靜態成員,而靜態成員是無法被繼承的,還有Parse等協助性成員也是靜態,這就意味著,想要以抽象類別完全重構包含必要的靜態成員的類是不可能的事情。遇到這類問題,我們就無法從代碼上直接控制約束了,只能從口頭上(或者文檔上)約定好如何去做。
也就是說,由於老陳的一時興奮,犯下了一個思維定勢的嚴重錯誤,如果要重構,也只能部分重構,不夠給力。既然如此,今天我們就深入研究一下這種方案,看看它的利害。
首先我們看看這種方案的最精簡模式:
1 [Serializable] // 不是必要的
2 public sealed class UserOperates
3 {
4 public static readonly UserOperates None = new UserOperates(0);
5 public static readonly UserOperates Read = new UserOperates(1);
6 public static readonly UserOperates Write = new UserOperates(2);
7 public static readonly UserOperates Delete = new UserOperates(4);
8
9 private UserOperates(int value) { this.Value = value; }
10
11 public int Value { get; private set; }
12 }
這裡有幾個要素:
- 枚舉值欄位必須是唯讀;
- 建構函式必須不能從外部改變,因此標記為私人的;
- 類被聲明為密封的,限定了它的多態性;
- 為了能夠從外部擷取枚舉值欄位的原始值,我們定義了一個Value成員,從這個角度來講,本樣本還不算是最精簡的;
把這個類修改一下,再來看看:
1 [Serializable] // 不是必要的
2 public class UserOperates
3 {
4 public static readonly UserOperates None = new UserOperates(0);
5 public static readonly UserOperates Read = new UserOperates(1);
6 public static readonly UserOperates Write = new UserOperates(2);
7 public static readonly UserOperates Delete = new UserOperates(4);
8
9 public static readonly List<UserOperates> EnumFields = new List<UserOperates> {
10 None,
11 Read,
12 Write,
13 };
14
15 protected UserOperates(int value) { this.Value = value; }
16
17 public int Value { get; private set; }
18
19 public static UserOperates operator |(UserOperates p1, UserOperates p2) { return Parse(p1.Value | p2.Value); }
20
21 public static UserOperates operator ^(UserOperates p1, UserOperates p2) { return Parse(p1.Value ^ p2.Value); }
22
23 public static UserOperates operator &(UserOperates p1, UserOperates p2) { return Parse(p1.Value & p2.Value); }
24
25 public bool HasFlag(UserOperates flag) { return (this.Value & flag.Value) == flag.Value; }
26
27 public static UserOperates Parse(int value)
28 {
29 // 注意:這裡沒有考慮位網域作業
30 return EnumFields.FirstOrDefault(item => item.Value == value) ?? None;
31 }
32 }
我們增加了一些操作符,但是我們來看看如下代碼,您認為它們分別輸出什嗎?
1 var o1 = UserOperates.Write | UserOperates.Delete;
2 var o2 = UserOperates.Read | UserOperates.Write | UserOperates.Delete;
3 var o3 = (UserOperates.Read | UserOperates.Write | UserOperates.Delete) ^ UserOperates.Read;
4 var o4 = (UserOperates.Read | UserOperates.Write | UserOperates.Delete) & UserOperates.Read;
5 var o5 = (UserOperates.Read | UserOperates.Write | UserOperates.Delete).HasFlag(UserOperates.Write);
6
7 Trace.WriteLine(o1.Value);
8 Trace.WriteLine(o2.Value);
9 Trace.WriteLine(o3.Value);
10 Trace.WriteLine(o4.Value);
11 Trace.WriteLine(o5);
答案是:
1 0
2 0
3 2
4 0
5 False
沒有一個是對的!怎麼會這樣呢?
這是我們代碼中的bug,我們在將各個枚舉值的原始值分別計算之後,試圖在字典中尋找與之匹配的項,結果都是無法找到,因此都返回為預設欄位None,它的值是0。彌補方法之一:
1 public static readonly UserOperates None = new UserOperates(0);
2 public static readonly UserOperates Read = new UserOperates(1);
3 public static readonly UserOperates Write = new UserOperates(2);
4 public static readonly UserOperates ReadWrite = new UserOperates(3);
5 public static readonly UserOperates Delete = new UserOperates(4);
6 public static readonly UserOperates ReadDelete = new UserOperates(5);
7 public static readonly UserOperates WriteDelete = new UserOperates(6);
8 public static readonly UserOperates ReadWriteDelete = new UserOperates(7);
9
10 public static readonly List<UserOperates> EnumFields = new List<UserOperates> {
11 None,
12 Read,
13 Write,
14 ReadWrite,
15 Delete,
16 ReadDelete,
17 WriteDelete,
18 ReadWriteDelete
19 };
再來運行:
1 6
2 7
3 5
4 1
5 True
輸出完全正確!其實,這正是“使用類來替代某些枚舉值”的一種缺陷,我們不得不將任何可能出現的組合都列出來才能保證運行正常。鑒於這個事實,老陳建議:對於需要位網域作業的枚舉類型,尤其是那些較為複雜的位域枚舉類型,千萬不要使用此方案,這可能會加重我們的編碼負擔!
還有一個潛在的bug,就是EnumFields欄位的值在外部是可更改的。也需要規避掉。很簡單,將它的修飾符public修改為protected即可。
此外還發現,昨天的ToString()方法寫的也有問題,應當修改為:
1 public override string ToString() { return this.Name; }
在整個重構過程中還遇到了其他的困難,比如運算子無法使用泛型,這意味著如果要使用多態來重構這個代碼的話,運算子就需要在每個類重寫,而且還會牽扯到各種類型轉換問題,其細節非常繁雜。
總而言之,昨天所提到的使用抽象類別重構的夢想已經基本破滅,因為這種重構帶來的痛苦比喜悅要多的多,不如不重構。這種替代枚舉類型的方案用途很有限,不要濫用!
對於我自身而言,以後寫文章應當更加謹慎,做過所有測試之後再來顯擺,狂妄自大終歸會自食其果的!歡迎大家繼續批評指導!
預報:明天將完成第三集,討論一下如何使用Attribute特性來實現枚舉值的多常量綁定,同時將會放出我自己使用的兩個枚舉類型封裝類。