使用 struct 在 C# 中实现位字段






4.64/5 (24投票s)
C# 的泛型等效于 C 中的位字段 struct
引言
如果您需要将 C 或 C++ 代码转换为 C#,您很可能会迟早遇到位字段。
不幸的是,C# 没有现成的解决方案,因此您会开始抓耳挠腮,并试图找出另一种实现它的最佳方法。(当然,工作量越少越好)
一种实现方法是创建一个类或结构体,为每个字段提供一个属性,并提供一些方法,可以将该结构体与整数值相互转换。但是,如果您有许多位字段结构需要转换,这项工作很快就会变得乏味。
另一种方法是使用结构体(或类)并创建一些通用的扩展方法来完成任何结构体的转换工作。
背景
我需要实现一个模拟位字段的代码,因为我使用了一个返回 `uint` 的旧版函数,该函数代表 C 风格的位字段。为了操作此值并将其发送回,最好不要使用按位运算,而是实现一个包装器类,使其更容易理解地设置单个字段。
由于这并非我第一次需要做这样的事情,我决定尝试寻找一种通用的方法,以避免每次都编写特定的转换方法的工作。
在搜索该主题时,我偶然发现了这个 Stack Overflow 问题,它解决了我和我遇到的同样问题,而接受的答案以及评论启发了我编写本文技巧中描述的类。所以一部分功劳应该归功于 Adam Wright 和 Kevin P Rice。
请点击此链接查看答案: C# 中的位字段
解释代码
基本概念
目标是模仿 C 中的位字段结构的功能,并在 C# 中进行实现。
这是通过编写一些自定义属性来完成的,这些属性可以应用于 C# 结构体,以及一些扩展方法,用于与整数值进行相互转换,还将值转换为其二进制表示形式的字符串。
与其他任何实现一样,可以有很多种不同的方法。我选择了使用
struct
而不是class
,主要是因为结构体是值类型,并且可以用作泛型方法的类型。- 属性而不是字段,因为如果需要或首选,可以添加对分配值的验证。
此外,在我的实现中,我将位数限制为 64 位,内部使用了 UInt64
类型。这似乎是大多数我遇到的情况都可以接受的限制。
自定义属性
此实现需要两个自定义属性。一个是用于结构体本身的,另一个将用于属性。
结构体属性
以下自定义属性指定了整个结构体使用的位数。
/// <summary>
/// Specifies the number of bits in the bit field structure
/// Maximum number of bits are 64
/// </summary>
[AttributeUsage(AttributeTargets.Struct | AttributeTargets.Class, AllowMultiple = false)]
public sealed class BitFieldNumberOfBitsAttribute : Attribute
{
/// <summary>
/// Initializes new instance of BitFieldNumberOfBitsAttribute with the specified number of bits
/// </summary>
/// <param name="bitCount">The number of bits the bit field will contain (Max 64)</param>
public BitFieldNumberOfBitsAttribute(byte bitCount)
{
if ((bitCount < 1) || (bitCount > 64))
throw new ArgumentOutOfRangeException("bitCount", bitCount,
"The number of bits must be between 1 and 64.");
BitCount = bitCount;
}
/// <summary>
/// The number of bits the bit field will contain
/// </summary>
public byte BitCount { get; private set; }
}
属性属性
此自定义属性用于表示位字段中每个成员的每个属性。它指定每个属性可以包含多少位。之所以同时具有偏移量和长度,是因为根据 MSDN 的说法,`Type.GetProperties()` 方法返回的属性顺序是不确定的。
引用GetProperties 方法不会按特定顺序返回属性,例如字母顺序或声明顺序。您的代码不得依赖属性返回的顺序,因为该顺序是可变的。
/// <summary>
/// Specifies the length of each bit field
/// </summary>
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false)]
public sealed class BitFieldInfoAttribute : Attribute
{
/// <summary>
/// Initializes new instance of BitFieldInfoAttribute with the specified field offset and length
/// </summary>
/// <param name="offset">The offset of the bit field</param>
/// <param name="length">The number of bits the bit field occupies</param>
public BitFieldInfoAttribute(byte offset, byte length)
{
Offset = offset;
Length = length;
}
/// <summary>
/// The offset of the bit field
/// </summary>
public byte Offset { get; private set; }
/// <summary>
/// The number of bits the bit field occupies
/// </summary>
public byte Length { get; private set; }
}
标记接口
为了创建结构体或类的通用扩展方法,您需要一个基类或接口,该基类或接口用作传递给扩展方法的实例对象的类型。
在这种情况下,会创建一个空接口。
/// <summary> /// Interface used as a marker in order to create extension methods on a struct /// that is used to emulate bit fields /// </summary> public interface IBitField { }
您声明的 struct
必须实现上述接口。
位字段示例
现在是时候展示一个简单的例子了。
下面是 C 语言中的位字段和 C# 中的等效代码并排显示。
C | C# |
struct example_bit_field
{
unsigned char bit1 : 1;
unsigned char bit2 : 1;
unsigned char two_bits : 2;
unsigned char four_bits : 4;
}
|
[BitFieldNumberOfBitsAttribute(8)]
struct ExampleBitField : IBitField
{
[BitFieldInfo(0, 1)]
public bool Bit1 { get; set; }
[BitFieldInfo(1, 1)]
public byte Bit2 { get; set; }
[BitFieldInfo(2, 2)]
public byte TwoBits { get; set; }
[BitFieldInfo(4, 4)]
public byte FourBits { get; set; }
}
|
偏移量从零开始,新属性的值通过将前一个属性的偏移量和长度相加来计算。
例如
在最后一个属性之后添加一个新属性,其偏移量将是 4 + 4 = 8。
注意:如果添加了新属性,请不要忘记更改 BitFieldNumberOfBitsAttribute
。
扩展方法
ToUInt64
此扩展方法用于将位字段转换为 ulong
。它遍历所有属性并设置相应的位。
/// <summary>
/// Converts the members of the bit field to an integer value.
/// </summary>
/// <param name="obj">An instance of a struct that implements the interface IBitField.</param>
/// <returns>An integer representation of the bit field.</returns>
public static ulong ToUInt64(this IBitField obj)
{
ulong result = 0;
// Loop through all the properties
foreach (PropertyInfo pi in obj.GetType().GetProperties())
{
// Check if the property has an attribute of type BitFieldLengthAttribute
BitFieldInfoAttribute bitField;
bitField = (pi.GetCustomAttribute(typeof(BitFieldInfoAttribute)) as BitFieldInfoAttribute);
if (bitField != null)
{
// Calculate a bitmask using the length of the bit field
ulong mask = 0;
for (byte i = 0; i < bitField.Length; i++)
mask |= 1UL << i;
// This conversion makes it possible to use different types in the bit field
ulong value = Convert.ToUInt64(pi.GetValue(obj));
result |= (value & mask) << bitField.Offset;
}
}
return result;
}
ToBinaryString
此方法对于在 UI 中呈现位字段结构或用于调试目的很有用。
/// <summary>
/// This method converts the struct into a string of binary values.
/// The length of the string will be equal to the number of bits in the struct.
/// The least significant bit will be on the right in the string.
/// </summary>
/// <param name="obj">An instance of a struct that implements the interface IBitField.</param>
/// <returns>A string representing the binary value of tbe bit field.</returns>
public static string ToBinaryString(this IBitField obj)
{
BitFieldNumberOfBitsAttribute bitField;
bitField = (obj.GetType().GetCustomAttribute(typeof(BitFieldNumberOfBitsAttribute)) as BitFieldNumberOfBitsAttribute);
if (bitField == null)
throw new Exception(string.Format(@"The attribute 'BitFieldNumberOfBitsAttribute' has to be
added to the struct '{0}'.", obj.GetType().Name));
StringBuilder sb = new StringBuilder(bitField.BitCount);
ulong bitFieldValue = obj.ToUInt64();
for (int i = bitField.BitCount - 1; i >= 0; i--)
{
sb.Append(((bitFieldValue & (1UL << i)) > 0) ? "1" : "0");
}
return sb.ToString();
}
创建者方法
这不是一个扩展方法,而是一个通用的静态方法,可用于使用默认值初始化位字段。
/// <summary>
/// Creates a new instance of the provided struct.
/// </summary>
/// <typeparam name="T">The type of the struct that is to be created.</typeparam>
/// <param name="value">The initial value of the struct.</param>
/// <returns>The instance of the new struct.</returns>
public static T CreateBitField<T>(ulong value) where T : struct
{
// The created struct has to be boxed, otherwise PropertyInfo.SetValue
// will work on a copy instead of the actual object
object boxedValue = new T();
// Loop through the properties and set a value to each one
foreach (PropertyInfo pi in boxedValue.GetType().GetProperties())
{
BitFieldInfoAttribute bitField;
bitField = (pi.GetCustomAttribute(typeof(BitFieldInfoAttribute)) as BitFieldInfoAttribute);
if (bitField != null)
{
ulong mask = (ulong)Math.Pow(2, bitField.Length) - 1;
object setVal = Convert.ChangeType((value >> bitField.Offset) & mask, pi.PropertyType);
pi.SetValue(boxedValue, setVal);
}
}
// Unboxing the object
return (T)boxedValue;
}
使用代码
声明一个结构体并使用扩展方法非常直接。
结构体已在上面创建,您可以在下面看到如何使用该代码。
Console.WriteLine("First bit field ...");
ExampleBitField bitField1 = new ExampleBitField();
bitField1.Bit1 = true;
bitField1.Bit2 = 0;
bitField1.TwoBits = 0x2; // 10
bitField1.FourBits = 0x7; // 0111
ulong bits = bitField1.ToUInt64();
Console.WriteLine("ulong: 0x{0:X2}", bits);
string s = bitField1.ToBinaryString();
Console.WriteLine("string: {0}", s);
Console.WriteLine();
Console.WriteLine("Second bit field ...");
ExampleBitField bitField2 = BitFieldExtensions.CreateBitField<ExampleBitField>(0xA3);
Console.WriteLine("ulong: 0x{0:X2}", bitField2.ToUInt64());
Console.WriteLine("string: {0}", bitField2.ToBinaryString());
执行输出
上述代码的输出将如下所示

关注点
在选择类或结构体时需要考虑的一点是,当位字段的实例作为参数传递到另一个方法时,结构体会按值传递,而类会按引用传递。这意味着如果您使用结构体,它将被复制一份,并且在方法返回时,对成员的任何修改都将丢失。
绕过此“问题”的方法是使用装箱。
有关该主题的更多信息,请参阅此 MSDN 文章: 装箱和拆箱 (C# 编程指南)
性能警告
由于此方法使用反射,因此它不是最快的方法。这一点在评论区被 frankazoid 指出。
因此,如果您在重复循环中读取和转换通信协议中的数据,您应该意识到此方法比直接的位掩码操作慢至少 10000 倍。基本上,我们将谈论将结构体与整数值相互转换的几毫秒。但是,设置和获取单个字段不成问题。
如果目的是仅在应用程序设置中读取或写入位字段结构的值,那么便利性可能超过时间开销。
历史
修订 | 日期 | 描述 |
---|---|---|
1 | 2016-04-26 | 技巧的第一个版本 |
2 | 2016-05-11 | 添加了性能警告 |