65.9K
CodeProject 正在变化。 阅读更多。
Home

.NET 中的伪位域

starIconstarIconstarIcon
emptyStarIcon
starIcon
emptyStarIcon

3.89/5 (7投票s)

2017年5月2日

CPOL

5分钟阅读

viewsIcon

29118

downloadIcon

148

如何在 .NET 中实现一个伪位域

引言

如果您有 C 或 C++ 背景,然后转向或涉足 .NET,您可能会注意到 .NET 中缺少 `Bitfield`。您可以访问 .NET 的建议站点以获取未来的新增功能,但似乎没有足够的动力来实施它。在本文中,我将展示如何构建一个类似 `Bitfield` 的结构。

背景

那些从未用过 `Bitfield` 的人可能会想,这为什么重要。位域允许您将一堆值打包到一个整数中。许多通信和记录结构使用位域将一组设置组合成一个值;一个很好的例子是 Windows 中用于设置和配置串行端口的 DCB 结构。

通常在 C 中,您可以构建一个如下所示的结构:

//bit field box properties
struct Box{
 unsigned int opaque     :1;
 unsigned int fill_color :4;
 unsigned int something  :4; // remaining bits after this are unused
}box1;

//access the fields like this
box1.fill_color=2;
box1.opaque=1;

这样做的好处是,它让您感觉像是操作一个对象,而不是处理大量常量、掩码和管理设置、翻转和清除所有小位的混乱代码。上面的相同内容在进行位操作(在 VB 中显示)时可能看起来像这样。

Dim box1 as int32
Dim Const OPAQUE as int32 = &b1
Dim Const FILL_COLOR as int32=&b11110
Dim Const SOMETHING as int32=&b111100000

'set the fill color with clearing the bits first
box1=box1 and not FILL_COLOR
box1 = box1 or 2<<1

'set the opaque flag
box1=box1 or OPAQUE

这个例子还不算太糟糕(但仍然如此),但在几百行代码之后,您就开始失去对 OPAQUE 的理解;它与框变量一起使用,还是也适用于圆变量?我设置了那个位吗?我是否将值封装到一个完整的类来管理常量?每个掩码需要移动多少个空格,我是否也为它们创建常量?

这让我无数次地希望有 `Bitfield`,真是太疯狂了。

Using the Code

既然我无法获得内置的位域支持,我必须使用泛型来实现自己的。

<CLSCompliant(False)> Public Structure BitField16_
(Of enumeration As {Structure, IComparable, IConvertible, IFormattable})
    Public Value As UInt16

    Public Sub New(initial As UInt16)
        Value = initial
    End Sub

    'gets or sets the bits depending on the enum passed
    Public Property [field](v As enumeration) As UInt16
        Get
            Dim i As Integer = [Enum].ToObject(GetType(enumeration), v)
            Return (Value And i) >> shifted(i)
        End Get
        Set(newvalue As UInt16)
            Dim i As Integer = [Enum].ToObject(GetType(enumeration), v)
            Value = Value And Not (i) 'clear any old bits
            Value = Value Or (i And (newvalue << shifted(i))) 'set any new bits
        End Set
    End Property

    'finds the first shifed bit and returns the offset that it sits at
    Private Function shifted(mask As Integer) As Byte
        Dim v As Integer = (mask And -mask) 'removes all but the LSB
        Dim c As Integer = 0 'counter
        Do Until v = 1
            v = v >> 1 'keep shifting right until the value is 1
            c += 1
        Loop
        Return c
    End Function

End Structure

我使用了结构而不是类,因为它将保留在堆栈上,并在我完成变量后轻松丢弃。您可能注意到的第二件事是结构声明中的“Of enumeration”(枚举类型),是的,我们将把一个枚举传递给这个结构来定义位域的字段。

<Flags>Public Enum testEnum
    bits1 = &B11
    bits2 = &B11100
    bits3 = &B100000
    bits4 = &B11000000
End Enum

我们不让 `enum` 在内部从 0,1,2,3... 开始,而是使用“`&B`”二进制常量来设置值,这样我们就可以看到每个掩码将处理的位。(总有一天,我希望 VB 允许我写“`&B0000 0011`”来使东西对齐得很漂亮)因此,您可以看到“`bits1`”最多可以占用两个位,最大值为 3,而“`bits3`”只有一个位,只能是 0 或 1。除此之外,您可以根据需要创建自己的 `enum`,只需确保不要在掩码中超过 16 位(或者如果您稍作更改,则为 32 位、64 位)。

“shifted”函数将返回掩码左移的位数,这有助于在对无符号整数进行获取或设置时解析值。用法现在变得相当简单:

Dim t as BitField16(Of testEnum)

t.field(testEnum.bits3) = 5 'can only store 0 or 1 the remaining bits get ignored
t.field(testEnum.bits2 = 5 'can store 3 bits worth so &b101 will fit just fine in that field

是的,它比“C”风格的位掩码稍微啰嗦一些,但它比一堆常量和其他混乱的东西要干净得多。并且内部值可以保存、流式传输或根据需要进行任何操作。

关注点

我选择 `UInt16`(`Ushort`)是因为我日常工作中处理大量的串行端口数据,并且我处理的大多数远程设备只支持最多 16 位数据;因此,请随时将其更改为支持 `UInt32` 或 `UInt64` 类型的数据。如果您想为位域使用任何有符号类型,如 `Int32`,只需确保您的 `Enum` 掩码不使用 MSB,因为 .NET 运行时会出错。

切勿尝试将负值设置到其中一个字段,因为您会丢失数据,位域只能处理正数据。

这段代码目前不够健壮,因为它应该在 `Enum` 中的一个字段的掩码为 0 时抛出异常,或者如果传递给泛型的其他 `valuetype` 不是 `enum`,则会导致问题,但我需要一些轻量级且相当快速的东西;我希望“shifted”函数能够减少周期以使其更精简,但我确信使用更高级的数学会增加更多周期。如果您对“shifted”函数有任何建议,我将很乐意听取。

“`[Enum].ToObject(GetType(enumeration), v)`”转换和反射也有一点性能损失,但我认为为了更清晰的代码而进行的权衡是值得的;因为另一种选择是编写专门的结构来处理我遇到的每种类型的 `bitfield` 并处理所有位操作。.

在示例代码中,我还包含了类型转换运算符,可以直接将 `UInt16`“get/set”到结构中,而无需直接处理 `Value` 字段。

在附加的项目中,还将包括 4 个不同类型测试的速度测试,这些测试在我的 i7 Thinkpad 上以系统滴答方式运行。

  1. 第一个速度测试是泛型(最慢但最灵活,未来维护最少)。
  2. 第二个是具有已知 `Enum` 的非泛型测试,速度更快,但现在每个 `Bitfield` 都需要一个结构。
  3. 第三个是完整的结构,其中每个字段都已写出,速度最快,对客户端代码更清晰,如果需要新字段,未来维护量更大,代码大小最大。
  4. 最后一个速度测试是直接处理 `UInt16` 的基线,这是最快的,因为没有将函数调用推到堆栈上,但也是最脆弱的,因为客户端代码必须处理和维护所有位。

您还可以看到,在这 4 种情况下,所需的大小仍然只有 2 字节。

历史

  • 2017 年 5 月 2 日:初稿
  • 2017 年 5 月 3 日:添加了源代码以比较速度测试
  • 2017 年 5 月 4 日:向 `Enum` 添加了“`Flags`”属性,感谢 BillWoodruff 指出这一点
© . All rights reserved.