使用.NET中的位字段实现掩码和标志






4.84/5 (37投票s)
2004年4月21日
9分钟阅读

249058

3736
位字段在 Windows 窗体中作为标志的简单用法。
目录
引言
本文演示了在Windows窗体中使用位字段作为标志的一个简单应用。位字段允许将数据打包到简单的结构中,当对带宽、内存或数据存储有严格要求时,它们尤其有用。虽然在现代设备或日常应用程序中这可能不是一个主要问题,但与布尔值等其他值类型相比,使用位字段可以节省多达16倍的内存和存储空间。
背景
存储
考虑.NET中的布尔值
bool bVal;
布尔值数据类型存储为16位(2字节)数字,只能是true
或false
。考虑一个无符号16位数字,其范围从0到65535。
Decimal Hexidecimal Binary
0 0x0000 0000000000000000
65535 0xffff 1111111111111111
当数值数据类型转换为布尔值时,0
变为false
,所有其他值都变为true
。当布尔值转换为数值类型时,false
变为0
,true
变为-1
(使用有符号数)。
如果我们想使用一个布尔值来表示程序中一个标志或设置的两个状态(true <=> 打开,false <=> 关闭),那么这将存储为一个16位数字。
考虑使用一个二进制数字来表示相同的双状态值:(1 <=> 打开,0 <=> 关闭)。
Decimal Hexidecimal Binary
0 0x0000 0000000000000000
1 0x0001 0000000000000001
我们可以使用这个相同的16位数字来表示16个不同的设置,只需检查每一位是1/打开还是0/关闭。
Decimal Hexidecimal Binary
1 0x0001 0000000000000001
2 0x0002 0000000000000010
4 0x0004 0000000000000100
8 0x0008 0000000000001000
16 0x0010 0000000000010000
32 0x0020 0000000000100000
64 0x0040 0000000001000000
128 0x0080 0000000010000000
256 0x0100 0000000100000000
512 0x0200 0000001000000000
1024 0x0400 0000010000000000
2048 0x0800 0000100000000000
4096 0x1000 0001000000000000
8192 0x2000 0010000000000000
16384 0x4000 0100000000000000
32768 0x8000 1000000000000000
只有16个设置时,这可能看起来不是问题,人们更有可能将设置存储为布尔值。然而,存储这些设置的历史记录会很快累积,而节省16倍的空间可能会产生显著影响。你是否曾尝试将一个100MB的文件加载到内存中进行操作?那一个1.6GB的文件呢?
理解位移 <<
f1 = 0x01 // 0x01 1 00000001
f2 = f1 << 1, // 0x02 2 00000010
f3 = f2 << 1, // 0x04 4 00000100
f4 = f3 << 1, // 0x08 8 00001000
向左或向右移动。
有两个运算符:
<<
用于将指定数量的位向左移动(朝向“高位”位)。>>
用于向右移动。
如果移位操作导致某些位超出底层数据类型,则这些位将被丢弃。移位操作产生的空位在左移操作和正数右移操作中始终用0填充。如果请求的右移位数为负数(例如 f2 >> -2
),则空出的位位置将被1填充。
理解位运算
位运算用于操作位字段,并确定是否设置了指定的标志。以下真值表说明了一些操作的真值:
Mask OR Flag Mask AND NOT Flag Mask XOR Flag
0 | 0 = 0 0 & ~0 = 0 0 ^ 0 = 0
1 | 0 = 1 1 & ~0 = 1 1 ^ 0 = 1
0 | 1 = 1 0 & ~1 = 0 0 ^ 1 = 1
1 | 1 = 1 1 & ~1 = 0 1 ^ 1 = 0
代码使用
BitField
类/结构使用枚举来定义位字段中的标志。该字段可以使用64位无符号 ulong
值类型存储多达64个唯一的标志。标志可以有任何名称,但请小心对待Clear
标志,它有一个特殊值,用于清除和填充整个位字段。
[FlagsAttribute]
public enum Flag : ulong
{
Clear = 0x00,
f1 = 0x01,
f2 = f1 << 1,
. . .
}
每个Flag
枚举都是一个数字,其二进制形式中只有一个位设置为1
,其余设置为0
。通过此枚举,有64个不同的值,每个值只有一个1,并且有2^64(18446744073709551616)种可能的这些64个标志的组合。
关于 [FlagsAttribute]
的几点说明:
- 枚举被视为一个位字段;即,由一组标志组成的掩码。
- 位运算的结果也是位字段。
- 位字段通常用于可能组合出现的元素列表,而枚举常量通常用于互斥的元素列表。因此,位字段旨在组合以生成未命名值,而枚举常量则不然。
位字段存储在变量_Mask
中,并通过public
属性 get
和 set
提供外部访问。
private ulong _Mask;
public ulong Mask
{
get
{
return _Mask;
}
set
{
_Mask = value;
}
}
方法
SetField
方法在掩码中设置指定的flag
,并关闭所有其他flag
。
- 在
flag
中设置为1
的位将在掩码中设置为1。 - 在
flag
中设置为0
的位将在掩码中设置为0。
private void SetField(Flag flg)
{
Mask = (ulong)flg;
}
这对于使用Flag.Clear
标志设置所有位关闭(SetField(Flag.Clear)
)特别有用,
Mask = Flag.Clear
<=> Mask = 0000000000000000000000000000000000000000000000000000000000000000
或者使用Flag.Clear
标志的否定设置所有位开启(SetField(~Flag.Clear)
)。
Mask = ~Flag.Clear
<=> Mask = ~0000000000000000000000000000000000000000000000000000000000000000
<=> Mask = 1111111111111111111111111111111111111111111111111111111111111111
SetOn
方法在掩码中设置指定的标志,并保持所有其他标志不变(使用二进制按位包含的OR
运算符)。
- 在
flag
中设置为1
的位将在掩码中设置为1。 - 在
flag
中设置为0
的位在掩码中将保持不变。
public void SetOn(Flag flg)
{
Mask |= (ulong)flg;
}
由于一个标志正好有一个位值为1
,其余位为0
,因此这会保持掩码不变,除了有值1
的位置。
考虑对一个随机的16位Mask
进行此操作:
1101001000011001
OR F2
-------------------------
<=> 1101001000011001
OR 0000000000000010
-------------------------
<=> 1101001000011011
这与在Mask
的适当位置放置数字1
以将flag
设置为开启的效果相同。
SetOff
方法在掩码中将指定的标志关闭,并保持所有其他标志不变(使用一元按位补码NOT,然后是二元按位AND运算符)。
- 在
flag
中设置为1
的位将在掩码中设置为零。 - 在
flag
中设置为0
的位在掩码中将保持不变。
public void SetOff(Flag flg)
{
Mask &= ~(ulong)flg;
}
由于一个flag
正好有一个位值为1
,其余位为0
,因此这会保持掩码不变,除了有值1
的位置。
考虑对一个随机的16位Mask进行此操作:
1101001000011001
AND ~F1
--------------------------
<=> 1101001000011001
AND ~0000000000000001
--------------------------
<=> 1101001000011001
AND 1111111111111110
--------------------------
<=> 1101001000011000
这与在Mask
的适当位置放置数字0
以将flag
设置为关闭的效果相同。
SetToggle
方法切换指定的标志,并保持所有其他位不变(使用二元按位异或,XOR运算符)。
- 在
flag
中设置为1
的位将在掩码中被切换。 - 在
flag
中设置为0
的位在掩码中将保持不变。
public void SetToggle(Flag flg)
{
Mask ^= (ulong)flg;
}
由于一个标志正好有一个位值为1
,其余位为0
,因此这会保持掩码不变,除了有值1
的位置。
考虑对一个随机的16位Mask进行此操作:
1101001000011001
XOR F1
-------------------------
<=> 1101001000011001
XOR 0000000000000001
-------------------------
<=> 1101001000011000
这与在Mask
的适当位置放置相反的数字以切换flag
的效果相同。使用这个flag
,我们不必记住flag
的先前状态。
AnyOn
方法检查掩码中是否设置了任何指定的标志。它隔离相应的位并返回true
(如果任何位非零),否则返回false
。
public bool AnyOn (Flag flg)
{
return (Mask & (ulong)flg) != 0;
}
AllOn
方法检查掩码中是否所有指定的标志都已设置。它隔离相应的位并返回true
(如果它们全部非零),否则返回false
。
public bool AllOn (Flag flg)
{
return (Mask & (ulong)flg) == (ulong)flg;
}
IsEqual
方法检查掩码中所有指定的标志是否与掩码相同。它隔离相应的位并返回true
(如果它们全部相同),否则返回false
。
public bool IsEqual (Flag flg)
{
return Mask == (ulong)flg;
}
DecimalToFlag
方法将十进制值转换为Flag FlagsAttribute
值。输入可以是介于0
和64
之间的索引,输出将是对应于该索引的Flag
枚举。它所做的就是获取索引,然后移动该位相应的位数。
public static Flag DecimalToFlag(decimal dec)
{
Flag flg = Flag.Clear;
ulong tMsk = 0;
byte shift;
try
{
shift = (byte)dec;
if (shift > 0 && shift <= 64)
{
tMsk = (ulong) 0x01 << (shift - 1);
}
flg = (Flag)tMsk;
}
catch (OverflowException e) //Byte cast operation
{
Console.WriteLine("Exception caught in DecimalToFlag: {0}",e.ToString());
}
return flg;
}
ToStringDec
、ToStringHex
、ToStringBin
这三个方法分别以十进制、十六进制和二进制表示法返回掩码的string
表示。
使用BitField类/结构
实例化对象非常直接。
BitField bitField = new BitField();
使用new
运算符创建struct
对象时,它会被创建并调用相应的构造函数。与类不同,struct
可以在不使用new
运算符的情况下实例化,因此如果您不使用new
,字段将保持未分配状态,并且在所有字段都初始化之前无法使用该对象。
这将创建一个新的位字段并将所有flag
设置为关闭。要将所有flag
设置为开启,您可以调用该方法:
bitField.FillField();
要设置一个flag
,请使用:
bitField.SetOff(BitField.Flag.F1); //Flag F1 off
bitField.SetOn(BitField.Flag.F1); //Flag F1 on
bitField.SetToggle(BitField.Flag.F1); //Flag F1 off
bitField.SetToggle(BitField.Flag.F1); //Flag F1 on
要检查一个Flag
是否开启,请使用:
if (bitField.IsOn(BitField.Flag.f1))
{
Console.WriteLine("Flag F1 is On");
}
有趣的点
掩码值是一个64位数字,可以存储、检索或传递给支持64位数字的其他进程和应用程序。然后,可以使用BitField
类/结构来检索和操作掩码。我没有找到任何解释.NET中位运算如何实现以及运算是否高效实现的文章。可能存在优化代码的方法,但我还没有找到一个好的资源。
同样重要的是要注意,创建类对象会产生一些开销,因为类是引用类型,而结构是值类型。如果这是一个问题,那么可以直接在代码中实现位字段操作,但这会违背面向对象模型的初衷。
另一个可以考虑的选项是使用结构而不是类。
当以下情况时,结构可能更可取:
- 您的数据量很小,少于16字节或128位。
- 您对每个实例执行大量操作,并且堆管理会带来性能下降。
- 您不需要继承该结构,也不需要对其实例的功能进行专门化。
- 您不对结构进行装箱和拆箱。
- 您正在托管/非托管边界之间传递可直接复制(blittable)的数据。
当以下情况时,类更可取:
- 您需要使用继承和多态性。
- 您需要在创建时初始化一个或多个成员。
- 您需要提供一个无参数的构造函数。
- 您需要无限的事件处理支持。
源代码和演示项目包含了一个类似的位字段类和一个位字段结构。根据用途,使用其中一个而不是另一个可能更可取。通过计时并比较实际结果,可以有趣地了解在何种情况下系统可以更有效地处理结构,以及效率提升的程度。
历史
- 2004年4月21日:初始版本
未进行任何更改或改进。
许可证
本文没有明确的许可证附加,但可能在文章文本或下载文件中包含使用条款。如有疑问,请通过下面的讨论板联系作者。您可以在此处找到作者可能使用的许可证列表。