C# and TypeScript – Enum Flags
前言
以前就有提过 Enum Flags,但平日不常用。最近翻 Angular 源码,发现它很多地方用到,而且没有封装语义代码。一堆符号真的看不惯啊...
于是又去复习了一遍,熟悉熟悉。顺便写一篇做记入呗。
这篇 C# 和 TypeScript 会一起讲。
参考
Enum, Flags and bitwise operators (中文版这篇)
Stack Overflow – What does the [Flags] Enum Attribute mean in C#?
W3Schools – JavaScript Bitwise Operations
介绍
Enum 大家都很熟悉了。它就是 number(也有些场景会用 string 啦,但这篇我们 focus number 就好)
enum Status { Status0, Status1, Status2 } console.log(Status.Status0); // 0 console.log(Status.Status1); // 1 console.log(Status.Status2); // 2
那...如果我们要 Enum List 怎么写?
我们当然可以用 Array。
const statusList: Status[] = [Status.Status0, Status.Status1]; if(statusList.includes(Status.Status0)) { // included } if(statusList.some(e => e === Status.Status0)) { // included }
但是有一种性能超级快的方法,可以取代 Enum Array,那就是利用二进制操作符。
而这就是 Enum Flags 的核心。
<< | & ~ ^ 位运算符
我们先来学 5 个和二进制相关的操作符,Enum Flags 就是利用这些原理实现的。
Bitwise left shift 位运算符 <<
Bitwise 是"按位"(按照位置)的意思,Bitwise Shifts 就是按位移动。
下面是 1, 2, 4, 8, 16 的二进制
console.log(Number(1).toString(2).padStart(5, '0')); // 00001 console.log(Number(2).toString(2).padStart(5, '0')); // 00010 console.log(Number(4).toString(2).padStart(5, '0')); // 00100 console.log(Number(8).toString(2).padStart(5, '0')); // 01000 console.log(Number(16).toString(2).padStart(5, '0')); // 10000
注意它们的规律,1 的位置是一条斜线。
这种情况下, 我们可以使用 << 符号来替代自己做乘法
console.log(Number(1 << 0).toString(2).padStart(5, '0')); // 00001 console.log(Number(1 << 1).toString(2).padStart(5, '0')); // 00010 console.log(Number(1 << 2).toString(2).padStart(5, '0')); // 00100 console.log(Number(1 << 3).toString(2).padStart(5, '0')); // 01000 console.log(Number(1 << 4).toString(2).padStart(5, '0')); // 10000
1 << 2 的的过程是这样的
1 的二进制是 1
往左边移动 2 个位置就变成了 00100 也就是十进制的 4,或者你也可以换一个角度理解,<< 2 就是在结尾加 2 个零。
1 << 3 就移动 3 个位置,变成 01000 就是 8
1 << 4 就移动 4 个位置,变成 10000 就是 16
所以,可以这样玩。
1 << 0 === 1; 1 << 1 === 2; 1 << 2 === 4; 1 << 3 === 8; 1 << 4 === 16; 1 << 5 === 32;
Bitwise OR 位运算符 |
console.log(binary(2)); // 00010 console.log(binary(4)); // 00100 console.log(binary(2 | 4)); // 00110
它的逻辑是按位对比,只要其中一个是 1 那么就是 1。
最后一位是 0 0 所以是 0
最后第二位是 1 0 所以是 1
看看更多例子
最后第三位是 0 1 所以是 1
Bitwise AND 位运算符 &
And 和 Or 都是位置对比,区别是
Or 只要求其中一个是 1 那就是 1
And 要求全部是 1 那才是 1
比如
2 | 4 // 00110 2 // 00010 (2 | 4) & 2 // 00010
再来一个
// 2 | 4 | 8 // 01110 // 8 | 16 // 11000 // (2 | 4 | 8) & (8 | 16) // 01000
Bitwise NOT 位运算符 ~
~ 是取反,把 0 变成 1,把 1 变成 0。
例子: ~5
首先 5 的二进制是 101
Console.WriteLine(Convert.ToString(5, 2)); // 101
注: 我这里用 C#。TypeScript 有点不同,下面会讲
把 101 写满 32 位是
Console.WriteLine(Convert.ToString(5, 2).PadLeft(32, '0')); // 00000000000000000000000000000101
手动取反
Console.WriteLine(Convert.ToString(5, 2).PadLeft(32, '0').Replace("0", "z").Replace("1", "0").Replace("z", "1")); // 11111111111111111111111111111010 // 对比 // 00000000000000000000000000000101 // 11111111111111111111111111111010
转换回十进制
var value = Convert.ToString(5, 2).PadLeft(32, '0').Replace("0", "z").Replace("1", "0").Replace("z", "1"); Console.WriteLine(Convert.ToInt32(value, 2)); // -6 Console.WriteLine(~5); // -6
和 ~5 相等。
JavaScript 有点特别,因为它的二进制可以是负数(原理我没有 research, 想懂更多可以看这篇: 知乎 – 还搞不懂负数怎么用二进制表示?看完这一篇就懂了)
// C# Console.WriteLine(Convert.ToString(-6, 2)); // 11111111111111111111111111111010 // JS console.log(Number(-6).toString(2)); // -110
虽然二进制写法不同,但 ~ 的操作结果是一致的,~5 === -6(十进制)
Bitwise XOR 位运算符 ^
XOR 在对比时,如果完全相等返回 0,其中一个不相等返回 1。
比如一列都是 0,或者一列都是 1。就是相等,返回 0。
一列里有 1 又有 0,那就不相等,返回 1。
console.log(1 ^ 1); // 相等 0 console.log(0 ^ 0); // 相等 0 console.log(0 ^ 1); // 不相等 1 console.log(1 ^ 0); // 不相等 1 console.log(4 ^ 8) // 12; console.log(Number(4).toString(2)); // 0100 console.log(Number(8).toString(2)); // 1000 // 对比 // 0100 // 1000 // 1100 结果: 不等, 不等, 相等, 相等 console.log(parseInt('1100', 2)); // 12 证明 4 ^ 8 = 12
Enum Flags 使用
Enum Flags 就是配合上面这些位运算符来使用的。具体原理我就不详细解释了。
有兴趣的可以自己一步一步转换二进制。依据上面位运算符的规则来推演。
定义 Enum Flags
enum StatusFlags { one = 1 << 0, // 1 (00001) two = 1 << 1, // 2 (00010) four = 1 << 2, // 4 (00100) eight = 1 << 3, // 8 (01000) sixteen = 1 << 4 // 16 (10000) }
定义 Enum Flags 有一个规则,号码一定要是 2 的幂次方。
可以直接写 2, 4, 8, 16。
也可以利用 << 位运算符。
甚至像 Angular 源码中,用十六进制也是可以的。(只要是 2 的幂次方就可以了)
Enum List
const statusList1 = StatusFlags.one | StatusFlags.two | StatusFlags.four; // 7 (00111) const statusList2 = StatusFlags.one | StatusFlags.sixteen; // 17 (10001)
利用 Bitwise OR 把 enum 串起来就是 enum list。
Add enum to list
let statusList1 = StatusFlags.one | StatusFlags.two | StatusFlags.four; // 7 (00111) statusList1 |= StatusFlags.sixteen; // add more enum (已经有了会 ignore) statusList1 === (StatusFlags.one | StatusFlags.two | StatusFlags.four | StatusFlags.sixteen); // true
用 Bitwise OR,如果 list 里面已经有了也是可以运行,不会有 duplicated 概念的。
Remove enum from list
statusList1 &= ~StatusFlags.two; // remove enum(本来就没有会 ignore) statusList1 === (StatusFlags.one | StatusFlags.four); // true
用了 Bitwise AND 和 Bitwise NOT。
Toggle enum from List
let statusList1 = StatusFlags.one | StatusFlags.two | StatusFlags.four; // 7 (00111) statusList1 ^= StatusFlags.one; // toggle enum (有就移除) statusList1 === (StatusFlags.two | StatusFlags.four); statusList1 ^= StatusFlags.one; // toggle enum (有就移除) statusList1 === (StatusFlags.one | StatusFlags.two | StatusFlags.four);
用 Bitwise XOR 可以实现 toggle。
Included in list
判断 list2 是否 "全部" 在 list1 里
const statusList1 = StatusFlags.one | StatusFlags.two | StatusFlags.four; // 7 (00111) const statusList2 = StatusFlags.one | StatusFlags.two; // 17 (10001) console.log((statusList1 & statusList2) === statusList2); // true, all list2 in list1
判断 list2 是否 "某一个" 在 list1 里
const statusList1 = StatusFlags.one | StatusFlags.two | StatusFlags.four; // 7 (00111) const statusList2 = StatusFlags.one | StatusFlags.sixteen; // 17 (10001) console.log((statusList1 & statusList2) > 0); // true, some list2 in list1
总结
Enum List 是利用各种位二进制运算符来实现类似 Array 效果的。
它本质上只是数字而已。
Enum Flags (C#)
和 TypeScript 的 Enum Flags 原理是一样的.
[Flags] // 最好添加标签 public enum StatusFlags { One = 1 << 0, Two = 1 << 1, Four = 1 << 2, Eight = 1 << 3, Sixteen = 1 << 4 } public static class Program { public static void Main() { var statusList1 = StatusFlags.One | StatusFlags.Two | StatusFlags.Four; var statusList2 = StatusFlags.One | StatusFlags.Sixteen; statusList1 |= StatusFlags.Sixteen; // add statusList1 &= ~StatusFlags.Sixteen; // remove statusList1 ^= StatusFlags.Sixteen; // toggle var allIn1 = (statusList1 & statusList2) == statusList2; // true var allIn2 = statusList1.HasFlag(statusList2); // HasFlag 和上一行是等价的, list2 完全在 list1 内 var someIn = (statusList2 & statusList1) > 0; // true } }
HasFlag 是一个 build-in 的方法。