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

.NET 中的 BitFields 和 BitStrings

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.81/5 (27投票s)

2016年10月8日

CPOL

6分钟阅读

viewsIcon

45041

downloadIcon

581

用于 BitFields 和 BitStrings 的 .NET 库

引言

本文将介绍一个 .NET 库,它实现了表示 BitStringBitField 的类。 BitString 是由 1 和 0 组成的字符串,在内部表示为 32 位无符号整数数组。 随着 BitString 的增长,它从左侧(最高有效位)到右侧(最低有效位)填充每个数组元素,并流入下一个数组元素。

例如,一个包含 33 个 1 的 BitString 在内部由两个 32 位无符号整数表示,如下所示

BitStrings 对于位操作非常有用。

BitField 只是一个带有命名字段/区域的 BitString。 这是通过一个内部字典实现的,该字典将字段/区域的名称与其在 BitString 中的偏移量和长度相关联。 这允许我们按名称访问 BitString 的任何字段/区域。

背景

最近,在将一些 C++ 代码转换为 C# 时,我遇到了打包结构和位字段的使用,并且惊讶地发现 C# 不提供相同的功能。 我不得不将原始 C++ 代码中的位字段使用转换为 C# 中不直观的掩码和位移操作。

我决定编写一个 C# 库,使任何此类代码转换都更轻松、更直观。 结果是一个包含 2 个主要类的库:BitString 和 BitField。

快速预览

在描述这两个类之前,让我们快速预览一下 BitField 类可以做什么。 BitField 类的一个用例如下

假设您需要创建和读取一个自定义格式的二进制文件。 假设该文件包含 12 条记录,每条记录由 5 个字节组成。 因此,每条记录长 40 位。 假设每条记录具有以下布局

二进制文件记录布局
字段名 偏移量(从0开始) 位长度
Light1 0 1
Light2 1 1
Fan1 2 1
Fan2 3 1
ErrorCode 4 12
StatusCode 16 4
保留 20 9
计数器 29 11

我们可以将文件记录定义为 BitField。 设置记录布局的代码使用 BitField 如下所示

            // Define a BitField structure of 40-bits (5 Bytes)
            BitField bf = new BitField();
            bf.AddField("Light1", 1);
            bf.AddField("Light2", 1);
            bf.AddField("Fan1", 1);
            bf.AddField("Fan2", 1);
            bf.AddField("ErrorCode", 12);
            bf.AddField("StatusCode", 4);
            bf.AddField("Reserved", 9);
            bf.AddField("Counter", 11);

            // After defining structure, we must initialize it.  This
            // call will initialize the 40-bit BitString to all 0's
            bf.InitBitField();

我们可以通过以下代码轻松设置 BitField 中任何字段/区域的值

            // Initialize some fields to some value.  If we don't
            // explicitly set a value, the field is all 0's from the
            // earlier call to InitBitField().  Note we can use
            // strings or integers for assignment.
            bf["Light1"] = 1;
            bf["Fan2"] = "1";
            bf["ErrorCode"] = "111100001111";
            bf["StatusCode"] = 2;

现在,我们可以通过以下代码将记录写入文件

            // Create the binary file name
            string fn = Path.GetTempFileName();

            // Write out 12 records where each record is represented by
            // a BitField
            using (FileStream fs = new FileStream(fn, FileMode.Append))
            {
                for (int i = 0; i < 12; i++)
                {
                    // Convert the BitField into a byte array
                    byte[] temp = bf.GetBytes();

                    for (int j = 0; j < temp.Length; j++)
                    {
                        // Write out the BitField/record
                        fs.WriteByte(temp[j]);
                    }

                    // Increment the BitField's Counter value by 1
                    // using the BitString's increment operator
                    ++bf["Counter"];
                }
            }

我们也可以使用同一个 BitField 对象将二进制文件读回。 为此,我们只需从文件中读取一条记录,然后用它初始化 BitField(通过调用 InitBitField())。 现在我们可以轻松地从记录中访问每个单独的字段。

            // Now we want to read in the binary file that we just created!
            List<string> report = new List<string>();

            using (BinaryReader BinReader = new BinaryReader(File.Open(fn, FileMode.Open)))
            {
                for (int i = 0; i < 12; i++)
                {
                    // Read each 5-byte (40-bit) record into an array of bytes
                    byte[] buffer = BinReader.ReadBytes(5);

                    // Initialize the BitField with the record from the binary file
                    bf.InitBitField(buffer);

                    // Extract each field for display in a report
                    string temp = "BitField = " + bf.ToString() + Environment.NewLine;
                    temp = temp + "Light1 = " + bf["Light1"].ToString();
                    temp = temp + ", Light2 = " + bf["Light2"].ToString();
                    temp = temp + ", Fan1 = " + bf["Fan1"].ToString();
                    temp = temp + ", Fan2 = " + bf["Fan2"].ToString() + Environment.NewLine;
                    temp = temp + "ErrorCode = " + bf["ErrorCode"].ToString();
                    temp = temp + ", StatusCode = " + bf["StatusCode"].ToString();
                    temp = temp + ", Reserved = " + bf["Reserved"].ToString();
                    temp = temp + ", Counter = " + bf["Counter"].ToString();
                    temp = temp + Environment.NewLine + Environment.NewLine;

                    report.Add(temp);
                }
            }

如果我们打印报告的内容,它将是

BitField = 1001111100001111001000000000000000000000
Light1 = 1, Light2 = 0, Fan1 = 0, Fan2 = 1
ErrorCode = 111100001111, StatusCode = 0010, Reserved = 000000000, Counter = 00000000000

BitField = 1001111100001111001000000000000000000001
Light1 = 1, Light2 = 0, Fan1 = 0, Fan2 = 1
ErrorCode = 111100001111, StatusCode = 0010, Reserved = 000000000, Counter = 00000000001

BitField = 1001111100001111001000000000000000000010
Light1 = 1, Light2 = 0, Fan1 = 0, Fan2 = 1
ErrorCode = 111100001111, StatusCode = 0010, Reserved = 000000000, Counter = 00000000010

BitField = 1001111100001111001000000000000000000011
Light1 = 1, Light2 = 0, Fan1 = 0, Fan2 = 1
ErrorCode = 111100001111, StatusCode = 0010, Reserved = 000000000, Counter = 00000000011

BitField = 1001111100001111001000000000000000000100
Light1 = 1, Light2 = 0, Fan1 = 0, Fan2 = 1
ErrorCode = 111100001111, StatusCode = 0010, Reserved = 000000000, Counter = 00000000100

BitField = 1001111100001111001000000000000000000101
Light1 = 1, Light2 = 0, Fan1 = 0, Fan2 = 1
ErrorCode = 111100001111, StatusCode = 0010, Reserved = 000000000, Counter = 00000000101

BitField = 1001111100001111001000000000000000000110
Light1 = 1, Light2 = 0, Fan1 = 0, Fan2 = 1
ErrorCode = 111100001111, StatusCode = 0010, Reserved = 000000000, Counter = 00000000110

BitField = 1001111100001111001000000000000000000111
Light1 = 1, Light2 = 0, Fan1 = 0, Fan2 = 1
ErrorCode = 111100001111, StatusCode = 0010, Reserved = 000000000, Counter = 00000000111

BitField = 1001111100001111001000000000000000001000
Light1 = 1, Light2 = 0, Fan1 = 0, Fan2 = 1
ErrorCode = 111100001111, StatusCode = 0010, Reserved = 000000000, Counter = 00000001000

BitField = 1001111100001111001000000000000000001001
Light1 = 1, Light2 = 0, Fan1 = 0, Fan2 = 1
ErrorCode = 111100001111, StatusCode = 0010, Reserved = 000000000, Counter = 00000001001

BitField = 1001111100001111001000000000000000001010
Light1 = 1, Light2 = 0, Fan1 = 0, Fan2 = 1
ErrorCode = 111100001111, StatusCode = 0010, Reserved = 000000000, Counter = 00000001010

BitField = 1001111100001111001000000000000000001011
Light1 = 1, Light2 = 0, Fan1 = 0, Fan2 = 1
ErrorCode = 111100001111, StatusCode = 0010, Reserved = 000000000, Counter = 0000000101

BitString 要求

BitString 类是库中两个类的工作核心。 如前所述,BitStrings 是由 1 和 0 组成的字符串。 例如,下面的 BitString 有 4 位

1011

在设计过程中,我希望 BitString 类支持特定的功能。 我希望 BitString 类支持大多数人期望能够对 BitStrings 执行的常规操作。

我希望能够连接两个 BitStrings

0101 + 111100 = 0101111100

我希望能够对两个 BitStrings 执行常规的按位操作

0101 AND 1111 = 0101

0101 OR 1111 = 1111

0101 XOR 1111 = 1010

如果 BitStrings 的长度不相等,则较短的字符串将在右侧填充 1 或 0,以便结果 BitString 具有所需的位,其中两个 BitStrings 匹配,其余位匹配较长的 BitString

010 AND 101111101 = 000111101

010 OR 101111101 = 111111101

010 XOR 101111101 = 111111101

我希望能够翻转 BitString 中的所有位

0101 => 1010 (Flip)

我希望能够反转 BitStrings 中的位

111001 => 100111 (Reverse)

我希望能够像位代表无符号整数一样递增/递减 BitStrings

0111 => 1000 (Increment)

1000 => 0111 (Decrement)

我希望能够将 BitString 转换为其带符号/无符号整数表示

1000 => 8 (Unsigned integer)

1000 => -8 (2's Complement Signed integer)

我希望能够对 BitString 进行子集操作

111010000 => 010 (Subset a BitString from location 3 to location 5)

我希望能够设置/清除 BitString 中的单个位。

111000 => 101000 (Clear bit 1)

111000 => 111010 (Set bit 4)

最后,我希望能够处理几乎任何长度的 BitStrings。 对于这个要求,我希望 BitString 在内部由无符号 32 位整数数组表示,因此 BitString 101011110101 将在内部存储为

uint[0] = 10101111010100000000000000000000

我希望 BitString 被左对齐填充到每个数组元素中,并且随着 BitString 的增长,它应该溢出到下一个数组元素。 因此,一个包含 33 个全 1 的 BitString 将存储为

uint[0] = 11111111111111111111111111111111

uint[1] = 10000000000000000000000000000000

我希望能够获取内部缓冲区的字节数组表示,因此上面的 uint 数组将作为以下字节数组返回

byte[0] = 11111111

byte[1] = 11111111

byte[2] = 11111111

byte[3] = 11111111

byte[4] = 10000000

字节数组表示使我们能够轻松地将 BitString 的原始位写入流。

BitField 要求

BitField 只是一个带有命名字段/区域的 BitString。 这意味着我希望能够将 BitString 中的一组连续位与一个名称相关联。 BitField 的要求很简单。

  • 我希望有一种方法可以为 BitString 中的一组连续位赋予唯一的名称。
  • 我希望能够通过其唯一名称引用任何此类组。

类设计

库的类图如上所示。 此库中的两个主要类是 BitString 类和 BitField 类。 大部分工作由 BitString 类完成。 BitField 类包含一个 BitString 对象 (m_BitString) 和一个字典 (m_Fields),该字典将字段/区域的名称映射到该区域在 BitString 中的位置和长度。 该字典提供了访问 BitString 中任何命名字段/区域的能力。

使用代码

要在您的代码中使用该库,只需在项目中添加对该库 (BitString.dll) 的引用,并为您要使用的命名空间 "GCore" 添加 using 语句。 现在您应该可以在项目中使用了 BitString 和 BitField 类。

以下是一个简单的程序,展示了如何使用 BitString 类

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using GCore;

namespace DemoConsole
{
    class Program
    {
        static void Main(string[] args)
        {
            // Instantiating a BitString using a regular string of 1's and 0's
            String bits1 = "101010";
            BitString bs1 = new BitString(bits1);

            // bs1 = 101010
            Console.WriteLine("bs1 = " + bs1.ToString());

            // You can also just use assignment with a string value to set the BitString
            bs1 = "11110000";
            Console.WriteLine("bs1 = " + bs1.ToString());

            // Instantiating a BitString using another BitString
            BitString bs2 = new BitString(bs1);

            // bs2 = 11110000
            Console.WriteLine("bs2 = " + bs2.ToString());

            // We can flip all the bits
            bs2.Flip();

            // bs2 = 00001111
            Console.WriteLine("bs2 (flipped) = " + bs2.ToString());

            // You can bitwise XOR two BitStrings
            BitString temp = bs1 ^ bs2;

            // bs1 ^ bs2 = 11111111
            Console.WriteLine("bs1 ^ bs2 = " + temp.ToString());

            // You can concatenate two BitStrings
            BitString bs3 = bs1 + bs2;

            // bs3 (bs1 + bs2) = 1111000000001111
            Console.WriteLine("bs3 (bs1 + bs2) = " + bs3.ToString());

            // Instantiating a BitString using an array of bytes
            byte[] seed = new byte[2];
            seed[0] = 0xFF;
            seed[1] = 0xAA;
            bs3 = new BitString(seed);

            // bs3 = 1111111110101010
            Console.WriteLine("bs3 = " + bs3.ToString());

            // You can reverse a BitString
            bs3.Reverse();

            // bs3 = 0101010111111111
            Console.WriteLine("bs3 (reverse) = " + bs3.ToString());

            // Setting a BitString's value by using a integer
            BitString bs4 = 4;

            // bs4 = 0000000000000000000000000100
            Console.WriteLine("bs4 = " + bs4.ToString());

            // You can subset a BitString
            BitString bs5 = bs4[29, 3];

            // bs5 = 100
            Console.WriteLine("bs5 = " + bs5.ToString());

            // You can increment a BitString
            ++bs5;

            // bs5 = 101
            Console.WriteLine("++bs5 = " + bs5.ToString());

            // Pause and wait for key stroke to end
            Console.ReadKey();
        }
    }
}

Visual Studio 解决方案

我在本文中包含了该库的 Visual Studio 2015 解决方案。 该解决方案包含 4 个项目

  1. BitString 项目 (此项目是 BitString/BitField 库)
  2. BitString 测试项目 (此项目包含库的单元测试)
  3. 演示项目 (此项目包含一个允许您与 BitStrings 交互并查看其内部属性的小型演示)
  4. DemoConsole 项目 (此项目包含前面显示的 BitString 的简单控制台程序)

Demo 项目提供了一个 GUI,允许您玩弄 BitStrings 并查看它们的内部表示。 标有“Binary File Test”的按钮运行了“Quick Preview”部分中显示的程序。 标有“Benchmark”的按钮使用子集运算符将一个 6 位 BitString 在一个 64 位 BitString 中随机位置插入 10,000,000 次。 在我的机器(AMD Phenom II X6 1035T)上,大约需要 1 分 30 秒才能完成。

我希望这个库对某些人有用。 它可以处理非常长的 BitStrings/BitFields,并且 BitStrings 不受字节/字边界的限制。 它可能不够快,无法进行实时位操作,但我希望它能使编写需要进行位操作的 .NET 代码更轻松、更直观。

感谢阅读!

历史

  • 2016/10/19 - 根据读者的优化建议(irneb)更新了 BitString 库。 这将某些操作的速度提高了 25%。 添加了 Randomize() 方法以随机化 BitString 中的位。
  • 2016/10/08 - 初始发布。
© . All rights reserved.