C#枚舉的高級(jí)應(yīng)用
文章開(kāi)頭先給大家出一道面試題:
在設(shè)計(jì)某小型項(xiàng)目的數(shù)據(jù)庫(kù)(假設(shè)用的是 MySQL)時(shí),如果給用戶表(User)添加一個(gè)字段(Roles)用來(lái)存儲(chǔ)用戶的角色,你會(huì)給這個(gè)字段設(shè)置什么類型?提示:要考慮到角色在后端開(kāi)發(fā)時(shí)需要用枚舉表示,且一個(gè)用戶可能會(huì)擁有多個(gè)角色。
映入你腦海的第一個(gè)答案可能是:varchar 類型,用分隔符的方式來(lái)存儲(chǔ)多個(gè)角色,比如用 1|2|3
或 1,2,3
來(lái)表示用戶擁有多個(gè)角色。當(dāng)然如果角色數(shù)量可能超過(guò)個(gè)位數(shù),考慮到數(shù)據(jù)庫(kù)的查詢方便(比如用 INSTR 或 POSITION 來(lái)判斷用戶是否包含某個(gè)角色),角色的值至少要從數(shù)字 10 開(kāi)始。方案是可行的,可是不是太簡(jiǎn)單了,有沒(méi)有更好的方案?更好的回答應(yīng)是整型(int、bigint 等),優(yōu)點(diǎn)是寫(xiě) SQL 查詢條件更方便,性能、空間上都優(yōu)于 varchar。但整型畢竟只是一個(gè)數(shù)字,怎么表示多個(gè)角色呢?此時(shí)想到了二進(jìn)制位操作的你,心中應(yīng)該早有了答案。且保留你心中的答案,接著看完本文,或許你會(huì)有意外的收獲,因?yàn)閷?shí)際應(yīng)用中可能還會(huì)遇到一連串的問(wèn)題。為了更好的說(shuō)明后面的問(wèn)題,我們先來(lái)回顧一下枚舉的基礎(chǔ)知識(shí)。
枚舉基礎(chǔ)
枚舉類型的作用是限制其變量只能從有限的選項(xiàng)中取值,這些選項(xiàng)(枚舉類型的成員)各自對(duì)應(yīng)于一個(gè)數(shù)字,數(shù)字默認(rèn)從 0 開(kāi)始,并以此遞增。例如:
public enum Days { Sunday, Monday, Tuesday, // ... }
其中 Sunday 的值是 0,Monday 是 1,以此類推。為了一眼能看出每個(gè)成員代表的值,一般推薦顯示地將成員值寫(xiě)出來(lái),不要省略:
public enum Days { Sunday = 0, Monday = 1, Tuesday = 2, // ... }
C# 枚舉成員的類型默認(rèn)是 int 類型,通過(guò)繼承可以聲明枚舉成員為其它類型,比如:
public enum Days : byte { Monday = 1, Tuesday = 2, Wednesday = 3, Thursday = 4, Friday = 5, Saturday = 6, Sunday = 7 }
枚舉類型一定是繼承自 byte、sbyte、short、ushort、int、uint、long 和 ulong 中的一種,不能是其它類型。下面是幾個(gè)枚舉的常見(jiàn)用法(以上面的 Days 枚舉為例):
// 枚舉轉(zhuǎn)字符串 string foo = Days.Saturday.ToString(); // "Saturday" string foo = Enum.GetName(typeof(Days), 6); // "Saturday" // 字符串轉(zhuǎn)枚舉 Enum.TryParse("Tuesday", out Days bar); // true, bar = Days.Tuesday (Days)Enum.Parse(typeof(Days), "Tuesday"); // Days.Tuesday // 枚舉轉(zhuǎn)數(shù)字 byte foo = (byte)Days.Monday; // 1 // 數(shù)字轉(zhuǎn)枚舉 Days foo = (Days)2; // Days.Tuesday // 獲取枚舉所屬的數(shù)字類型 Type foo = Enum.GetUnderlyingType(typeof(Days))); // System.Byte // 獲取所有的枚舉成員 Array foo = Enum.GetValues(typeof(MyEnum); // 獲取所有枚舉成員的字段名 string[] foo = Enum.GetNames(typeof(Days));
另外,值得注意的是,枚舉可能會(huì)得到非預(yù)期的值(值沒(méi)有對(duì)應(yīng)的成員)。比如:
Days d = (Days)21; // 不會(huì)報(bào)錯(cuò) Enum.IsDefined(typeof(Days), d); // false
即使枚舉沒(méi)有值為 0 的成員,它的默認(rèn)值永遠(yuǎn)都是 0。
var z = default(Days); // 0
枚舉可以通過(guò) Description、Display 等特性來(lái)為成員添加有用的輔助信息,比如:
public enum ApiStatus { [Description("成功")] OK = 0, [Description("資源未找到")] NotFound = 2, [Description("拒絕訪問(wèn)")] AccessDenied = 3 } static class EnumExtensions { public static string GetDescription(this Enum val) { var field = val.GetType().GetField(val.ToString()); var customAttribute = Attribute.GetCustomAttribute(field, typeof(DescriptionAttribute)); if (customAttribute == null) { return val.ToString(); } else { return ((DescriptionAttribute)customAttribute).Description; } } } static void Main(string[] args) { Console.WriteLine(ApiStatus.Ok.GetDescription()); // "成功" }
上面這些我認(rèn)為已經(jīng)包含了大部分我們?nèi)粘S玫降拿杜e知識(shí)了。下面我們繼續(xù)回到文章開(kāi)頭說(shuō)的用戶角色存儲(chǔ)問(wèn)題。
用戶角色存儲(chǔ)問(wèn)題
我們先定義一個(gè)枚舉類型來(lái)表示兩種用戶角色:
public enum Roles { Admin = 1, Member = 2 }
這樣,如果某個(gè)用戶同時(shí)擁有 Admin 和 Member 兩種角色,那么 User 表的 Roles 字段就應(yīng)該存 3。那問(wèn)題來(lái)了,此時(shí)若查詢所有擁有 Admin 角色的用戶的 SQL 該怎么寫(xiě)呢?對(duì)于有基礎(chǔ)的程序員來(lái)說(shuō),這個(gè)問(wèn)題很簡(jiǎn)單,只要用位操作符邏輯與(‘&’)來(lái)查詢即可。
SELECT * FROM `User` WHERE `Roles` & 1 = 1;
同理,查詢同時(shí)擁有這兩種角色的用戶,SQL 語(yǔ)句應(yīng)該這么寫(xiě):
SELECT * FROM `User` WHERE `Roles` & 3 = 3;
對(duì)這條 SQL 語(yǔ)句用 C# 來(lái)實(shí)現(xiàn)查詢是這樣的(為了簡(jiǎn)單,這里使用了 Dapper):
public class User { public int Id { get; set; } public Roles Roles { get; set; } } connection.Query<User>( "SELECT * FROM `User` WHERE `Roles` & @roles = @roles;", new { roles = Roles.Admin | Roles.Member });
對(duì)應(yīng)的,在 C# 中要判斷用戶是否擁有某個(gè)角色,可以這么判斷:
// 方式一 if ((user.Roles & Roles.Admin) == Roles.Admin) { // 做管理員可以做的事情 } // 方式二 if (user.Roles.HasFlag(Roles.Admin)) { // 做管理員可以做的事情 }
同理,在 C# 中你可以對(duì)枚舉進(jìn)行任意位邏輯運(yùn)算,比如要把角色從某個(gè)枚舉變量中移除:
var foo = Roles.Admin | Roles.Member; var bar = foo & ~Roles.Admin;
這就解決了文章前面提到的用整型來(lái)存儲(chǔ)多角色的問(wèn)題,不論數(shù)據(jù)庫(kù)還是 C# 語(yǔ)言,操作上都是可行的,而且也很方便靈活。
枚舉的 Flags 特性
下面我們提供一個(gè)通過(guò)角色來(lái)查詢用戶的方法,并演示如何調(diào)用,如下:
public IEnumerable<User> GetUsersInRoles(Roles roles) { _logger.LogDebug(roles.ToString()); _connection.Query<User>( "SELECT * FROM `User` WHERE `Roles` & @roles = @roles;", new { roles }); } // 調(diào)用 _repository.GetUsersInRoles(Roles.Admin | Roles.Member);
Roles.Admin | Roles.Member
的值是 3,由于 Roles 枚舉類型中并沒(méi)有定義一個(gè)值為 3 的字段,所以在方法內(nèi) roles 參數(shù)顯示的是 3。3 這個(gè)信息對(duì)于我們調(diào)試或打印日志很不友好。在方法內(nèi),我們并不知道這個(gè) 3 代表的是什么。為了解決這個(gè)問(wèn)題,C# 枚舉有個(gè)很有用的特性:FlagsAtrribute。
[Flags] public enum Roles { Admin = 1, Member = 2 }
加上這個(gè) Flags 特性后,我們?cè)賮?lái)調(diào)試 GetUsersInRoles(Roles roles)
方法時(shí),roles 參數(shù)的值就會(huì)顯示為 Admin|Member
了。簡(jiǎn)單來(lái)說(shuō),加不加 Flags 的區(qū)別是:
var roles = Roles.Admin | Roles.Member; Console.WriteLing(roles.ToString()); // "3",沒(méi)有 Flags 特性 Console.WriteLing(roles.ToString()); // "Admin, Member",有 Flags 特性
給枚舉加上 Flags 特性,我覺(jué)得應(yīng)當(dāng)視為 C# 編程的一種最佳實(shí)踐,在定義枚舉時(shí)盡量加上 Flags 特性。
解決枚舉值沖突:2 的冪
到這,枚舉類型 Roles 一切看上去沒(méi)什么問(wèn)題,但如果現(xiàn)在要增加一個(gè)角色:Mananger,會(huì)發(fā)生什么情況?按照數(shù)字值遞增的規(guī)則,Manager 的值應(yīng)當(dāng)設(shè)為 3。
[Flags] public enum Roles { Admin = 1, Member = 2, Manager = 3 }
能不能把 Manager 的值設(shè)為 3?顯然不能,因?yàn)?Admin 和 Member 進(jìn)行位的或邏輯運(yùn)算(即:Admin | Member) 的值也是 3,表示同時(shí)擁有這兩種角色,這和 Manager 沖突了。那怎樣設(shè)值才能避免沖突呢?既然是二進(jìn)制邏輯運(yùn)算“或”會(huì)和成員值產(chǎn)生沖突,那就利用邏輯運(yùn)算或的規(guī)律來(lái)解決。我們知道“或”運(yùn)算的邏輯是兩邊只要出現(xiàn)一個(gè) 1 結(jié)果就會(huì) 1,比如 1|1、1|0 結(jié)果都是 1,只有 0|0 的情況結(jié)果才是 0。那么我們就要避免任意兩個(gè)值在相同的位置上出現(xiàn) 1。根據(jù)二進(jìn)制滿 2 進(jìn) 1 的特點(diǎn),只要保證枚舉的各項(xiàng)值都是 2 的冪即可。比如:
1: 00000001 2: 00000010 4: 00000100 8: 00001000
再往后增加的話就是 16、32、64...,其中各值不論怎么相加都不會(huì)和成員的任一值沖突。這樣問(wèn)題就解決了,所以我們要這樣定義 Roles 枚舉的值:
[Flags] public enum Roles { Admin = 1, Member = 2, Manager = 4, Operator = 8 }
不過(guò)在定義值的時(shí)候要在心中小小計(jì)算一下,如果你想懶一點(diǎn),可以用下面這種“位移”的方法來(lái)定義:
[Flags] public enum Roles { Admin = 1 << 0, Member = 1 << 1, Manager = 1 << 2, Operator = 1 << 3 }
一直往下遞增編值即可,閱讀體驗(yàn)好,也不容易編錯(cuò)。兩種方式是等效的,常量位移的計(jì)算是在編譯的時(shí)候進(jìn)行的,所以相比不會(huì)有額外的開(kāi)銷。
總結(jié)
本文通過(guò)一道小小的面試題引發(fā)一連串對(duì)枚舉的思考。在小型系統(tǒng)中,把用戶角色直接存儲(chǔ)在用戶表是很常見(jiàn)的做法,此時(shí)把角色字段設(shè)為整型(比如 int)是比較好的設(shè)計(jì)方案。但與此同時(shí),也要考慮到一些最佳實(shí)踐,比如使用 Flags 特性來(lái)幫助更好的調(diào)試和日志輸出。也要考慮到實(shí)際開(kāi)發(fā)中的各種潛在問(wèn)題,比如多個(gè)枚舉值進(jìn)行或(‘|’)運(yùn)算與成員值發(fā)生沖突的問(wèn)題。
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
.Net行為型設(shè)計(jì)模式之狀態(tài)模式(State)
這篇文章介紹了.Net行為型設(shè)計(jì)模式之狀態(tài)模式(State),文中通過(guò)示例代碼介紹的非常詳細(xì)。對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-05-05asp.net Ajax之無(wú)刷新評(píng)論介紹
asp.net Ajax之無(wú)刷新評(píng)論介紹;需要的朋友可以參考下2012-11-11asp.net下SQLite(輕量級(jí)最佳數(shù)據(jù)庫(kù)) 原理分析和開(kāi)發(fā)應(yīng)用
SQLite是一個(gè)開(kāi)源的嵌入式關(guān)系數(shù)據(jù)庫(kù),它在2000年由D. Richard Hipp發(fā)布,它的減少應(yīng)用程序管理數(shù)據(jù)的開(kāi)銷,SQLite可移植性好,很容易使用,很小,高效而且可靠2011-10-10ASP.NET系統(tǒng)關(guān)鍵字及保留字列表整理
ASP.NET系統(tǒng)關(guān)鍵字及保留字列表,大家在寫(xiě)程序的時(shí)候一定要避免使用,免得引起不需要的麻煩2012-10-10如何在WebForm中使用javascript防止連打(雙擊)
如何在WebForm中使用javascript防止連打(雙擊)...2007-01-01.net?6精簡(jiǎn)版webapi教程及熱重載、代碼自動(dòng)反編譯演示
這篇文章介紹了.net?6精簡(jiǎn)版webapi教程及熱重載、代碼自動(dòng)反編譯演示,對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2021-12-12.Net結(jié)構(gòu)型設(shè)計(jì)模式之組合模式(Composite)
這篇文章介紹了.Net結(jié)構(gòu)型設(shè)計(jì)模式之組合模式(Composite),文中通過(guò)示例代碼介紹的非常詳細(xì)。對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-05-05