使用C#的StructLayout和FieldOffset表示联合位字段

5

我理解在C#中表示联合需要使用StructLayout[LayoutKind.Explicit)]和[FieldOffset(x)]属性来指定联合内的字节偏移量。然而,我有一个要表示的联合,而FieldOffset属性只能按照一个字节的大小进行偏移。

union _myUnion
{
     unsigned int info;
     struct
     {
          unsigned int flag1:1 // bit 0
          unsigned int flag2:1 // bit 1
          unsigned int flag3:1 // bit 2
          unsigned int flag4:1 // bit 3
          unsigned int flag5:1 // bit 4
          unsigned int flag6:1 // bit 5
          .
          .
          .
          unsigned int flag31:1 // bit 31
     }
}

正如您在联合体的内部结构中所看到的那样,我无法使用FieldOffset,因为我需要一些可以按位偏移的东西。是否有解决方法?我正在尝试调用DLL函数,其中一个数据结构被定义为这样,我已经没有更好地表示这个联合体结构的想法了。

2
BitVector32类型是专门用来处理这种情况的。 - Hans Passant
3个回答

4

这里不需要使用联合体;一个用于数据的字段和属性,8个执行按位“移位”操作的属性,例如:

public uint Value {get;set;}

public uint Flag2 {
   get { return Value >> 2; }
}

等等,我认为你想在这里使用布尔值?

通常我会说:不要创建可变的结构体。PInvoke可能是一个有效的场景(我不确定),所以我会忽略它 :)

如果该值确实使用了超过32位,请考虑将后备字段切换为ulong。


p/invoke 给每个规则都添加了一个例外 =) - Tergiver

3
是的,您可以这样做。您正在走正确的道路,答案在使用BitVector32和FieldOffset以及StructLayout属性中。但是,在执行此操作时,有一些事项需要注意:
  1. 您需要明确指定将包含所讨论数据的变量的大小。首先要非常重视这一点。例如,在您上面的问题中,您将info指定为unsigned int。unsigned int的大小是多少?32位?64位?这取决于运行此特定版本的.NET的操作系统的版本(可能是.NET Core、Mono或Win32/Win64)。
  2. 这是什么“尾数”或位顺序?同样,我们可能正在运行任何类型的硬件(考虑Mobile/Xamarin,而不仅仅是笔记本电脑或平板电脑)--因此,您不能假设Intel位顺序。
  3. 我们将希望避免任何与语言相关的内存管理,或者用C/C++术语来说,是POD(plain old data)类型。这将意味着只使用值类型。
我会根据您的问题和标志0-31的规范做出以下假设:sizeof(int) == 32。
然后的技巧是确保以下内容:
  1. 所有数据都是按字节对齐的。
  2. 位字段和info字段在相同的字节边界上对齐。
以下是如何实现这一点的方法:
[StructLayout(LayoutKind.Explicit, Size = 1, CharSet = CharSet.Ansi)]
public struct MyUnion
{
    #region Lifetime

    /// <summary>
    /// Ctor
    /// </summary>
    /// <param name="foo"></param>
    public MyUnion(int foo)
    {
        // allocate the bitfield
        info = new BitVector32(0);

        // initialize bitfield sections
        flag1 = BitVector32.CreateSection(1);
        flag2 = BitVector32.CreateSection(1, flag1);
        flag3 = BitVector32.CreateSection(1, flag2);
    }

    #endregion

    #region Bifield

    // Creates and initializes a BitVector32.
    [FieldOffset(0)]
    private BitVector32 info;

    #endregion

    #region Bitfield sections

    /// <summary>
    /// Section - Flag 1
    /// </summary>
    private static BitVector32.Section flag1;

    /// <summary>
    /// Section - Flag 2
    /// </summary>
    private static BitVector32.Section flag2;

    /// <summary>
    /// Section - Flag 3
    /// </summary>
    private static BitVector32.Section flag3;

    #endregion

    #region Properties

    /// <summary>
    /// Flag 1
    /// </summary>
    public bool Flag1
    {
        get { return info[flag1] != 0; }
        set { info[flag1] = value ? 1 : 0; }
    }

    /// <summary>
    /// Flag 2
    /// </summary>
    public bool Flag2
    {
        get { return info[flag2] != 0; }
        set { info[flag2] = value ? 1 : 0; }
    }

    /// <summary>
    /// Flag 1
    /// </summary>
    public bool Flag3
    {
        get { return info[flag3] != 0; }
        set { info[flag3] = value ? 1 : 0; }
    }

    #endregion

    #region ToString

    /// <summary>
    /// Allows us to represent this in human readable form
    /// </summary>
    /// <returns></returns>
    public override string ToString()
    {
        return $"Name: {nameof(MyUnion)}{Environment.NewLine}Flag1: {Flag1}: Flag2: {Flag2} Flag3: {Flag3}  {Environment.NewLine}BitVector32: {info}{Environment.NewLine}";
    }

    #endregion
}

请特别注意构造函数。按照定义,在C#中您无法为结构体定义默认构造函数。但是,我们需要一种方法来确保在使用之前正确地初始化BitVector32对象及其部分。我们通过需要一个带有虚整型参数的构造函数来实现这一点,并像这样初始化对象:

    /// <summary>
    /// Main entry point
    /// </summary>
    /// <param name="args"></param>
    static void Main(string[] args)
    {
        // brew up one of these...
        var myUnion = new MyUnion(0)
        {
            Flag2 = true
        };

顺便提一下,您并不受单个位域的限制——您可以定义任何大小的位域。例如,如果我将您的示例更改为:

union _myUnion
{
    unsigned int info;
    struct
    {
        unsigned int flag1 : 3 // bit 0-2
        unsigned int flag2 : 1 // bit 3
        unsigned int flag3 : 4 // bit 4-7
            .
            .
            .
        unsigned int flag31 : 1 // bit 31
    }
}

我只需要改变我的类为:

[StructLayout(LayoutKind.Explicit, Size = 1, CharSet = CharSet.Ansi)]
public struct MyUnion2
{
    #region Lifetime

    /// <summary>
    /// Ctor
    /// </summary>
    /// <param name="foo"></param>
    public MyUnion2(int foo)
    {
        // allocate the bitfield
        info = new BitVector32(0);

        // initialize bitfield sections
        flag1 = BitVector32.CreateSection(0x07);
        flag2 = BitVector32.CreateSection(1, flag1);
        flag3 = BitVector32.CreateSection(0x0f, flag2);
    }

    #endregion

    #region Bifield

    // Creates and initializes a BitVector32.
    [FieldOffset(0)]
    private BitVector32 info;

    #endregion

    #region Bitfield sections

    /// <summary>
    /// Section - Flag1
    /// </summary>
    private static BitVector32.Section flag1;

    /// <summary>
    /// Section - Flag2
    /// </summary>
    private static BitVector32.Section flag2;

    /// <summary>
    /// Section - Flag3
    /// </summary>
    private static BitVector32.Section flag3;

    #endregion

    #region Properties

    /// <summary>
    /// Flag 1
    /// </summary>
    public int Flag1
    {
        get { return info[flag1]; }
        set { info[flag1] = value; }
    }

    /// <summary>
    /// Flag 2
    /// </summary>
    public bool Flag2
    {
        get { return info[flag2] != 0; }
        set { info[flag2] = value ? 1 : 0; }
    }

    /// <summary>
    /// Flag 1
    /// </summary>
    public int Flag3
    {
        get { return info[flag3]; }
        set { info[flag3] = value; }
    }

    #endregion

    #region ToString

    /// <summary>
    /// Allows us to represent this in human readable form
    /// </summary>
    /// <returns></returns>
    public override string ToString()
    {
        return $"Name: {nameof(MyUnion2)}{Environment.NewLine}Flag1: {Flag1}: Flag2: {Flag2} Flag3: {Flag3}  {Environment.NewLine}BitVector32: {info}{Environment.NewLine}";
    }

    #endregion
}

关于这个话题的最后一句话... 如果你绝对需要这样做,那么显然应该这样做。很明显,这需要你对操作系统环境、你运行的语言、你的调用约定以及其他许多脆弱的要求有专门的知识。
在任何其他情况下,这里有很多代码味道,这显然表明它不可移植。但从你的问题的背景中,我推测整个重点是你需要紧密地接近硬件,并且需要这种精度。
买家自负!

为什么你不把 BitVector32.Section 设为静态的?有必要把它们作为结构体实例的一部分吗? - Scott Chamberlain
如果您是通过info.CreateSection(1);进行初始化,我会同意您的观点。但是,它是由BitVector32.CreateSection(1);声明的,它没有引用您实例化的BitVetcor32。从源代码来看,它没有引用存储在其中的向量,只有偏移量。我非常确定您可以将这些字段设置为静态,并且每个标志的结构体大小可以减少4个字节(对于32个标志而言,这可能会累加)。 - Scott Chamberlain
1
不要把BitVector看作是数据库表,而应该将其视为应用了“视图”的“模型”。这些部分只是应用于同一类型模型的不同视图,视图并不关心您传递给它哪个具体模型,它只是在您提供的任何实例上应用视图过滤器。我认为您的答案是最好的,但需要去掉有关将过滤器作为结构实例的一部分的错误建议。 - Scott Chamberlain
1
好的,我认为我们需要一些澄清...
  1. CreateSection 的 API 声明如下: public static BitVector32.Section CreateSection(short maxValue)
因此,无论我将其称为 info.CreateSection(...) 还是 BitVector32.Create(...),最终结果都是相同的,它会创建一个 Section 结构体对象。
  1. 它创建的对象是一个实例对象,因此必须被实例化...你不希望它是静态的。生命周期在这里很重要。
  2. 也许,与其使用数据库隐喻,使用模式可能更合适。
- Stacy Dudovitz
需要记住的重要事情是,BitVector32 分配了两个东西... 1. 一组位数组 2. 一个或多个部分对象,其中包含对该数组的偏移量以及可用于操作这些位的位掩码。 - Stacy Dudovitz
显示剩余4条评论

2

最优雅的解决方法是使用标志枚举

最好使用uint而不是int

[Flags]
public enum MyEnum
    : uint
{
    None=0,
    Flag1=1,
    Flag2=1<<1,
    Flag3=1<<2,
    // etc
    Flag32=1<<31
}

在此之后,您可以将int用作枚举和uint。

MyEnum value=MyEnum.Flag1|MyEnum.Flag2;
uint uintValue=(uint)value;
// uintValue=3

这通常会由PInvoke进行序列化。

这并没有解决能够将一个值存储在标志之一中,然后在信息字段中查看该值的问题。您如何同时看到两个字段的“联合”? - Stacy Dudovitz
我仍然认为重点被忽略了...... 如果您必须进行第二次赋值,那么您将有两个不占用相同物理地址空间的独立内存位置。我们想要的是两个独立的变量(info 和 flags结构体),它们占用相同的物理内存地址空间。我们应该只需要执行一次赋值。然后,我们可以检查任何一个变量并查看结果。这个答案并没有解决这个要求。 - Stacy Dudovitz
不,C#中的枚举是像uint一样的值类型。实际上,枚举在内存中被表示为一个数字。当您使用赋值时,它会覆盖值,而不是引用。 - Alexei Shcherbakov

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接