網站首頁 編程語言 正文
文章開頭先給大家出一道面試題:
在設計某小型項目的數據庫(假設用的是 MySQL)時,如果給用戶表(User)添加一個字段(Roles)用來存儲用戶的角色,你會給這個字段設置什么類型?提示:要考慮到角色在后端開發時需要用枚舉表示,且一個用戶可能會擁有多個角色。
映入你腦海的第一個答案可能是:varchar 類型,用分隔符的方式來存儲多個角色,比如用?1|2|3
?或?1,2,3
?來表示用戶擁有多個角色。當然如果角色數量可能超過個位數,考慮到數據庫的查詢方便(比如用 INSTR 或 POSITION 來判斷用戶是否包含某個角色),角色的值至少要從數字 10 開始。方案是可行的,可是不是太簡單了,有沒有更好的方案?更好的回答應是整型(int、bigint 等),優點是寫 SQL 查詢條件更方便,性能、空間上都優于 varchar。但整型畢竟只是一個數字,怎么表示多個角色呢?此時想到了二進制位操作的你,心中應該早有了答案。且保留你心中的答案,接著看完本文,或許你會有意外的收獲,因為實際應用中可能還會遇到一連串的問題。為了更好的說明后面的問題,我們先來回顧一下枚舉的基礎知識。
枚舉基礎
枚舉類型的作用是限制其變量只能從有限的選項中取值,這些選項(枚舉類型的成員)各自對應于一個數字,數字默認從 0 開始,并以此遞增。例如:
public enum Days { Sunday, Monday, Tuesday, // ... }
其中 Sunday 的值是 0,Monday 是 1,以此類推。為了一眼能看出每個成員代表的值,一般推薦顯示地將成員值寫出來,不要省略:
public enum Days { Sunday = 0, Monday = 1, Tuesday = 2, // ... }
C# 枚舉成員的類型默認是 int 類型,通過繼承可以聲明枚舉成員為其它類型,比如:
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 中的一種,不能是其它類型。下面是幾個枚舉的常見用法(以上面的 Days 枚舉為例):
// 枚舉轉字符串 string foo = Days.Saturday.ToString(); // "Saturday" string foo = Enum.GetName(typeof(Days), 6); // "Saturday" // 字符串轉枚舉 Enum.TryParse("Tuesday", out Days bar); // true, bar = Days.Tuesday (Days)Enum.Parse(typeof(Days), "Tuesday"); // Days.Tuesday // 枚舉轉數字 byte foo = (byte)Days.Monday; // 1 // 數字轉枚舉 Days foo = (Days)2; // Days.Tuesday // 獲取枚舉所屬的數字類型 Type foo = Enum.GetUnderlyingType(typeof(Days))); // System.Byte // 獲取所有的枚舉成員 Array foo = Enum.GetValues(typeof(MyEnum); // 獲取所有枚舉成員的字段名 string[] foo = Enum.GetNames(typeof(Days));
另外,值得注意的是,枚舉可能會得到非預期的值(值沒有對應的成員)。比如:
Days d = (Days)21; // 不會報錯 Enum.IsDefined(typeof(Days), d); // false
即使枚舉沒有值為 0 的成員,它的默認值永遠都是 0。
var z = default(Days); // 0
枚舉可以通過 Description、Display 等特性來為成員添加有用的輔助信息,比如:
public enum ApiStatus { [Description("成功")] OK = 0, [Description("資源未找到")] NotFound = 2, [Description("拒絕訪問")] 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()); // "成功" }
上面這些我認為已經包含了大部分我們日常用到的枚舉知識了。下面我們繼續回到文章開頭說的用戶角色存儲問題。
用戶角色存儲問題
我們先定義一個枚舉類型來表示兩種用戶角色:
public enum Roles { Admin = 1, Member = 2 }
這樣,如果某個用戶同時擁有 Admin 和 Member 兩種角色,那么 User 表的 Roles 字段就應該存 3。那問題來了,此時若查詢所有擁有 Admin 角色的用戶的 SQL 該怎么寫呢?對于有基礎的程序員來說,這個問題很簡單,只要用位操作符邏輯與(‘&’)來查詢即可。
SELECT * FROM `User` WHERE `Roles` & 1 = 1;
同理,查詢同時擁有這兩種角色的用戶,SQL 語句應該這么寫:
SELECT * FROM `User` WHERE `Roles` & 3 = 3;
對這條 SQL 語句用 C# 來實現查詢是這樣的(為了簡單,這里使用了 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 });
對應的,在 C# 中要判斷用戶是否擁有某個角色,可以這么判斷:
// 方式一 if ((user.Roles & Roles.Admin) == Roles.Admin) { // 做管理員可以做的事情 } // 方式二 if (user.Roles.HasFlag(Roles.Admin)) { // 做管理員可以做的事情 }
同理,在 C# 中你可以對枚舉進行任意位邏輯運算,比如要把角色從某個枚舉變量中移除:
var foo = Roles.Admin | Roles.Member; var bar = foo & ~Roles.Admin;
這就解決了文章前面提到的用整型來存儲多角色的問題,不論數據庫還是 C# 語言,操作上都是可行的,而且也很方便靈活。
枚舉的 Flags 特性
下面我們提供一個通過角色來查詢用戶的方法,并演示如何調用,如下:
public IEnumerable<User> GetUsersInRoles(Roles roles) { _logger.LogDebug(roles.ToString()); _connection.Query<User>( "SELECT * FROM `User` WHERE `Roles` & @roles = @roles;", new { roles }); } // 調用 _repository.GetUsersInRoles(Roles.Admin | Roles.Member);
Roles.Admin | Roles.Member
?的值是 3,由于 Roles 枚舉類型中并沒有定義一個值為 3 的字段,所以在方法內 roles 參數顯示的是 3。3 這個信息對于我們調試或打印日志很不友好。在方法內,我們并不知道這個 3 代表的是什么。為了解決這個問題,C# 枚舉有個很有用的特性:FlagsAtrribute。
[Flags] public enum Roles { Admin = 1, Member = 2 }
加上這個 Flags 特性后,我們再來調試?GetUsersInRoles(Roles roles)
?方法時,roles 參數的值就會顯示為?Admin|Member
?了。簡單來說,加不加 Flags 的區別是:
var roles = Roles.Admin | Roles.Member; Console.WriteLing(roles.ToString()); // "3",沒有 Flags 特性 Console.WriteLing(roles.ToString()); // "Admin, Member",有 Flags 特性
給枚舉加上 Flags 特性,我覺得應當視為 C# 編程的一種最佳實踐,在定義枚舉時盡量加上 Flags 特性。
解決枚舉值沖突:2 的冪
到這,枚舉類型 Roles 一切看上去沒什么問題,但如果現在要增加一個角色:Mananger,會發生什么情況?按照數字值遞增的規則,Manager 的值應當設為 3。
[Flags] public enum Roles { Admin = 1, Member = 2, Manager = 3 }
能不能把 Manager 的值設為 3?顯然不能,因為 Admin 和 Member 進行位的或邏輯運算(即:Admin | Member) 的值也是 3,表示同時擁有這兩種角色,這和 Manager 沖突了。那怎樣設值才能避免沖突呢?既然是二進制邏輯運算“或”會和成員值產生沖突,那就利用邏輯運算或的規律來解決。我們知道“或”運算的邏輯是兩邊只要出現一個 1 結果就會 1,比如 1|1、1|0 結果都是 1,只有 0|0 的情況結果才是 0。那么我們就要避免任意兩個值在相同的位置上出現 1。根據二進制滿 2 進 1 的特點,只要保證枚舉的各項值都是 2 的冪即可。比如:
1: 00000001 2: 00000010 4: 00000100 8: 00001000
再往后增加的話就是 16、32、64...,其中各值不論怎么相加都不會和成員的任一值沖突。這樣問題就解決了,所以我們要這樣定義 Roles 枚舉的值:
[Flags] public enum Roles { Admin = 1, Member = 2, Manager = 4, Operator = 8 }
不過在定義值的時候要在心中小小計算一下,如果你想懶一點,可以用下面這種“位移”的方法來定義:
[Flags] public enum Roles { Admin = 1 << 0, Member = 1 << 1, Manager = 1 << 2, Operator = 1 << 3 }
一直往下遞增編值即可,閱讀體驗好,也不容易編錯。兩種方式是等效的,常量位移的計算是在編譯的時候進行的,所以相比不會有額外的開銷。
總結
本文通過一道小小的面試題引發一連串對枚舉的思考。在小型系統中,把用戶角色直接存儲在用戶表是很常見的做法,此時把角色字段設為整型(比如 int)是比較好的設計方案。但與此同時,也要考慮到一些最佳實踐,比如使用 Flags 特性來幫助更好的調試和日志輸出。也要考慮到實際開發中的各種潛在問題,比如多個枚舉值進行或(‘|’)運算與成員值發生沖突的問題。
原文鏈接:https://www.cnblogs.com/willick/p/csharp-enum-superior-tactics.html
相關推薦
- 2021-10-01 Linux里LVM磁盤擴容詳細步驟_Linux
- 2022-09-24 ASP.NET?MVC實現文件下載_實用技巧
- 2023-01-11 ubuntu20.04虛擬機無法上網的問題及解決_Linux
- 2023-05-15 shell?Bash的數組與關聯數組的實現_linux shell
- 2022-06-29 C語言超詳細講解getchar函數的使用_C 語言
- 2022-05-07 MongoDB連接和創建數據庫的方法講解_MongoDB
- 2023-11-17 python中numpy ndarray 按條件篩選數組,關聯篩選的例子——numpyarray對數
- 2022-11-09 PostgreSQL索引掃描時為什么index?only?scan不返回ctid_PostgreSQ
- 最近更新
-
- window11 系統安裝 yarn
- 超詳細win安裝深度學習環境2025年最新版(
- Linux 中運行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎操作-- 運算符,流程控制 Flo
- 1. Int 和Integer 的區別,Jav
- spring @retryable不生效的一種
- Spring Security之認證信息的處理
- Spring Security之認證過濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權
- redisson分布式鎖中waittime的設
- maven:解決release錯誤:Artif
- restTemplate使用總結
- Spring Security之安全異常處理
- MybatisPlus優雅實現加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務發現-Nac
- Spring Security之基于HttpR
- Redis 底層數據結構-簡單動態字符串(SD
- arthas操作spring被代理目標對象命令
- Spring中的單例模式應用詳解
- 聊聊消息隊列,發送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠程分支