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

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.64/5 (24投票s)

2016年4月26日

CPOL

5分钟阅读

viewsIcon

102558

downloadIcon

1364

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());

执行输出

上述代码的输出将如下所示

Output from execution test

 

关注点

在选择类或结构体时需要考虑的一点是,当位字段的实例作为参数传递到另一个方法时,结构体会按值传递,而类会按引用传递。这意味着如果您使用结构体,它将被复制一份,并且在方法返回时,对成员的任何修改都将丢失。

绕过此“问题”的方法是使用装箱。

有关该主题的更多信息,请参阅此 MSDN 文章: 装箱和拆箱 (C# 编程指南)

性能警告

由于此方法使用反射,因此它不是最快的方法。这一点在评论区被 frankazoid 指出。

因此,如果您在重复循环中读取和转换通信协议中的数据,您应该意识到此方法比直接的位掩码操作慢至少 10000 倍。基本上,我们将谈论将结构体与整数值相互转换的几毫秒。但是,设置和获取单个字段不成问题。

如果目的是仅在应用程序设置中读取或写入位字段结构的值,那么便利性可能超过时间开销。

历史

修订 日期 描述
1 2016-04-26 技巧的第一个版本
2 2016-05-11 添加了性能警告

 

 

 

© . All rights reserved.