
この記事は、インタビューから始まります。
小さなプロジェクト(My SQLを使用していると仮定)のデータベースを設計する際に、ユーザーテーブル(User)にユーザーのロールを格納するフィールド(Roles)を追加するとしたら、どのようなタイプのフィールドを設定しますか?ヒント:バックエンド開発ではロールを列挙する必要があり、ユーザーが複数のロールを持つ可能性があることを考慮してください。
映入你脑海的第一个答案可能是:varchar 类型,用分隔符的方式来存储多个角色,比如用 1|2|3 或 1,2,3 来表示用户拥有多个角色。当然如果角色数量可能超过个位数,考虑到数据库的查询方便(比如用 INSTR 或 POSITION 来判断用户是否包含某个角色),角色的值至少要从数字 10 开始。方案是可行的,可是不是太简单了,有没有更好的方案?
より良い答えは整数(int、bigintなど)でなければなりません。利点は、SQLクエリ条件を書く方が便利で、パフォーマンスとスペースがvarcharよりも優れていることです。しかし、整数は結局のところ、単一の数字ですが、どのように複数の役割を表すのでしょうか?この時点でバイナリビット操作を考えると、心の中にはすでに答えがあるはずです。
そして、あなたの心の答えを保持し、この記事を読んだ後、実際のアプリケーションでは問題の連続があるかもしれないので、あなたは驚きを得るかもしれません。この問題をよりよく説明するために、列挙の基本を見てみましょう。
列挙の基本
列挙型の役割は、変数を限られたオプションからの値に制限することです。これらのオプション(列挙型のメンバー)は、デフォルトで0から始まり、それに応じて増加する数値に対応します。例えば:
public enum Days
{
Sunday, Monday, Tuesday, // ...
}
Sun dayは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、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()); // "成功"
}
これは私たちが日常的に使う列挙の知識のほとんどをカバーしていると思います。ここでは、記事の冒頭で述べたユーザーロールストレージの問題に戻ります。
ユーザーロールのストレージの問題
まず、2つのユーザーロールを表す列挙型を定義します。
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 & ~foo;
これは、データベースや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#プログラミングのベストプラクティスの1つだと思います。列挙を定義するときにFlags機能を追加するようにします。
列挙値の競合を解決する2の累乗
ここまでは、列挙型Rolesは何の問題もないように見えますが、Manangerというロールを追加するとどうなりますか?数値増加のルールに従って、Managerの値は3に設定します。
[Flags]
public enum Roles
{
Admin = 1,
Member = 2,
Manager = 3
}
マネージャーの値を3に設定できますか?AdminとMemberはビットまたは論理演算を行うため、明らかにできません(例:Admin)。|Member)の値も3であり、両方のロールを持つことを示しており、Managerと競合します。どうすれば対立を避けることができるか?
バイナリ論理演算“or”はメンバー値と競合するので、論理演算orの法則を使用して解決します。OR演算の論理は両側に1が現れると1になります例えば1です|1.1 1|結果はすべて1で、0だけです。|0の場合は結果が0になる。同じ位置に2つの値があるのを避ける必要があります。2進法の完全な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
}
編集値を増やし続けるだけで、読書体験も良く、簡単には編集できません。2つの方法は等価であり、定数シフトの計算はコンパイル時に行われるため、追加のオーバーヘッドはありません。
まとめまとめまとめ
この記事では、小さなインタビューの質問を通じて列挙についての思考の連鎖を引き起こす。小規模なシステムでは、ユーザロールをユーザテーブルに直接格納することは非常に一般的ですが、この時点では、ロールフィールドを整数(intなど)に設定するのがより良い設計スキームです。しかし、より良いデバッグとログ出力を助けるためにフラグ機能を使用するなど、いくつかのベストプラクティスも考慮する必要があります。また、複数の列挙値の実行や(?)など、実際の開発におけるさまざまな潜在的な問題も考慮してください。|Essbase演算がメンバー値と競合する問題。
