C#中的位域

86

我有一个需要填充并写入磁盘(实际上有几个)的结构。

例如:

byte-6    
bit0 - original_or_copy  
bit1 - copyright  
bit2 - data_alignment_indicator  
bit3 - PES_priority  
bit4-bit5 - PES_scrambling control.  
bit6-bit7 - reserved  

在C语言中,我可能会做以下的事情:

struct PESHeader  {
    unsigned reserved:2;
    unsigned scrambling_control:2;
    unsigned priority:1;
    unsigned data_alignment_indicator:1;
    unsigned copyright:1;
    unsigned original_or_copy:1;
};

有没有办法在C#中实现这一点,使我能够使用结构体解引用点运算符访问位?

对于一些结构,我可以只是做一些位移再包装在访问器函数中。

我有很多这种处理结构的需求,所以我正在寻找更易于阅读和编写的方法。

12个回答

59

我可能会使用属性来组合一些东西,然后再使用转换类将合适的属性结构转换为位域原语。就像这样...

using System;

namespace BitfieldTest
{
    [global::System.AttributeUsage(AttributeTargets.Field, AllowMultiple = false)]
    sealed class BitfieldLengthAttribute : Attribute
    {
        uint length;

        public BitfieldLengthAttribute(uint length)
        {
            this.length = length;
        }

        public uint Length { get { return length; } }
    }

    static class PrimitiveConversion
    {
        public static long ToLong<T>(T t) where T : struct
        {
            long r = 0;
            int offset = 0;

            // For every field suitably attributed with a BitfieldLength
            foreach (System.Reflection.FieldInfo f in t.GetType().GetFields())
            {
                object[] attrs = f.GetCustomAttributes(typeof(BitfieldLengthAttribute), false);
                if (attrs.Length == 1)
                {
                    uint fieldLength  = ((BitfieldLengthAttribute)attrs[0]).Length;

                    // Calculate a bitmask of the desired length
                    long mask = 0;
                    for (int i = 0; i < fieldLength; i++)
                        mask |= 1 << i;

                    r |= ((UInt32)f.GetValue(t) & mask) << offset;

                    offset += (int)fieldLength;
                }
            }

            return r;
        }
    }

    struct PESHeader
    {
        [BitfieldLength(2)]
        public uint reserved;
        [BitfieldLength(2)]
        public uint scrambling_control;
        [BitfieldLength(1)]
        public uint priority;
        [BitfieldLength(1)]
        public uint data_alignment_indicator;
        [BitfieldLength(1)]
        public uint copyright;
        [BitfieldLength(1)]
        public uint original_or_copy;
    };

    public class MainClass
    {
        public static void Main(string[] args)
        {
            PESHeader p = new PESHeader();

            p.reserved = 3;
            p.scrambling_control = 2;
            p.data_alignment_indicator = 1;

            long l = PrimitiveConversion.ToLong(p);


            for (int i = 63; i >= 0; i--)
            {
                Console.Write( ((l & (1l << i)) > 0) ? "1" : "0");
            }

            Console.WriteLine();

            return;
        }
    }
}

这段代码生成了预期的二进制数000101011。当然,它需要更多的错误检查和稍微更合理的输入,但是我认为这个概念是可靠的、可重用的,并且可以轻松地创建易于维护的结构。

adamw


10
根据 MSDN 的说明,“GetFields”方法不会按照特定顺序返回字段,例如按字母顺序或声明顺序。因此,您的代码不能依赖于字段返回的顺序,因为这个顺序是变化的。这在这里会引起问题吗? - Kevin P. Rice
1
如果您创建一个“标记”接口IBitfield(没有成员),则可以将PrimitiveConversion类转换为任何实现IBitfield的结构的扩展方法。例如:public static long ToLong(this IBitfield obj) {}。然后,ToLong()方法将出现在Intellisense中,适用于IBitfield对象。 - Kevin P. Rice
你能通过使用“f.SetValue(t,someValue)”来反转这个过程吗?我正在使用它将数据包类转换为消息缓冲区以进行套接字传输。效果很好,但是由于某种原因,我无法使用f.SetValue()从流中读取数据返回到结构体中。没有错误,只是不起作用。 - buzzard51
GetFields 可能会因为反射缓存而导致重排序,但通过按 'MetadataToken' 进行排序应该可以解决这个问题(您可以通过混合获取单个字段和获取所有字段来复现此问题)。 - firda

26

通过使用枚举,您可以做到这一点,但会看起来很笨拙。

[Flags]
public enum PESHeaderFlags
{
    IsCopy = 1, // implied that if not present, then it is an original
    IsCopyrighted = 2,
    IsDataAligned = 4,
    Priority = 8,
    ScramblingControlType1 = 0,
    ScramblingControlType2 = 16,
    ScramblingControlType3 = 32,
    ScramblingControlType4 = 16+32,
    ScramblingControlFlags = ScramblingControlType1 | ScramblingControlType2 | ... ype4
    etc.
}

24
您想使用StructLayoutAttribute属性,详情请查看此处
[StructLayout(LayoutKind.Explicit, Size=1, CharSet=CharSet.Ansi)]
public struct Foo 
{ [FieldOffset(0)]public byte original_or_copy; 
  [FieldOffset(0)]public byte copyright;
  [FieldOffset(0)]public byte data_alignment_indicator; 
  [FieldOffset(0)]public byte PES_priority; 
  [FieldOffset(0)]public byte PES_scrambling_control; 
  [FieldOffset(0)]public byte reserved; 
}

这实际上是一个联合体,但你可以把它用作位域 - 你只需要意识到每个字段的位在字节中应该在哪里。使用实用函数和/或常量进行按位与运算可以帮助。

const byte _original_or_copy = 1;
const byte _copyright        = 2;

//bool ooo = foo.original_or_copy();
static bool original_or_copy(this Foo foo) 
{ return  (foo.original_or_copy & _original_or_copy)  == original_or_copy;
}    

还有LayoutKind.Sequential的方式可以让您以C方式执行。


21

正如Christophe Lambrechts所建议的,BitVector32提供了一种解决方案。Jitted性能应该是足够的,但不确定。 这里是说明此解决方案的代码:

public struct rcSpan
{
    //C# Spec 10.4.5.1: The static field variable initializers of a class correspond to a sequence of assignments that are executed in the textual order in which they appear in the class declaration.
    internal static readonly BitVector32.Section sminSection = BitVector32.CreateSection(0x1FFF);
    internal static readonly BitVector32.Section smaxSection = BitVector32.CreateSection(0x1FFF, sminSection);
    internal static readonly BitVector32.Section areaSection = BitVector32.CreateSection(0x3F, smaxSection);

    internal BitVector32 data;

    //public uint smin : 13; 
    public uint smin
    {
        get { return (uint)data[sminSection]; }
        set { data[sminSection] = (int)value; }
    }

    //public uint smax : 13; 
    public uint smax
    {
        get { return (uint)data[smaxSection]; }
        set { data[smaxSection] = (int)value; }
    }

    //public uint area : 6; 
    public uint area
    {
        get { return (uint)data[areaSection]; }
        set { data[areaSection] = (int)value; }
    }
}

通过这种方式,你可以做很多事情。如果为每个字段提供手动访问器,甚至可以更好地完成而无需使用BitVector32:

public struct rcSpan2
{
    internal uint data;

    //public uint smin : 13; 
    public uint smin
    {
        get { return data & 0x1FFF; }
        set { data = (data & ~0x1FFFu ) | (value & 0x1FFF); }
    }

    //public uint smax : 13; 
    public uint smax
    {
        get { return (data >> 13) & 0x1FFF; }
        set { data = (data & ~(0x1FFFu << 13)) | (value & 0x1FFF) << 13; }
    }

    //public uint area : 6; 
    public uint area
    {
        get { return (data >> 26) & 0x3F; }
        set { data = (data & ~(0x3F << 26)) | (value & 0x3F) << 26; }
    }
}

令人惊讶的是,这个手工制作的解决方案似乎是最方便、最简单和最短的。当然,这只是我的个人偏好。


12

基于Zbyl的答案再多说一点。这一个对我来说更容易改 - 我只需要调整sz0,sz1等并确保Set/Get块中mask#和loc#是正确的。

从性能上来看,它应该与原始代码一样,因为它们都解析为38个MSIL语句。(常量在编译时解析)

public struct MyStruct
{
    internal uint raw;

    const int sz0 = 4, loc0 = 0,          mask0 = ((1 << sz0) - 1) << loc0;
    const int sz1 = 4, loc1 = loc0 + sz0, mask1 = ((1 << sz1) - 1) << loc1;
    const int sz2 = 4, loc2 = loc1 + sz1, mask2 = ((1 << sz2) - 1) << loc2;
    const int sz3 = 4, loc3 = loc2 + sz2, mask3 = ((1 << sz3) - 1) << loc3;

    public uint Item0
    {
        get { return (uint)(raw & mask0) >> loc0; }
        set { raw = (uint)(raw & ~mask0 | (value << loc0) & mask0); }
    }

    public uint Item1
    {
        get { return (uint)(raw & mask1) >> loc1; }
        set { raw = (uint)(raw & ~mask1 | (value << loc1) & mask1); }
    }

    public uint Item2
    {
        get { return (uint)(raw & mask2) >> loc2; }
        set { raw = (uint)(raw & ~mask2 | (value << loc2) & mask2); }
    }

    public uint Item3
    {
        get { return (uint)((raw & mask3) >> loc3); }
        set { raw = (uint)(raw & ~mask3 | (value << loc3) & mask3); }
    }
}

1
非常棒的设置。乐在其中地被重复使用 ;)。我发现当位域“满”时(例如当 raw=uint.MaxValue 时),我确实必须稍微更改最后一项。或者,这只涉及最后一个属性。不确定。因此,对于上面的示例,ItemX 属性的 getter 看起来像这样:get { return (uint)((Raw & Mask3) >> Loc3); }。setter 看起来像这样:set { Raw = (uint)(Raw & ~Mask3 | (value << Loc3) & Mask3); } 如果没有这个更改,最后一个属性的转换将失败。 - Spiralis
1
@Spiralis:感谢您注意到这一点。我按照您的建议进行了更新,现在它的工作效果更好了。 - SunsetQuest

7

6

我发现自己对这些辅助函数非常熟悉:

uint SetBits(uint word, uint value, int pos, int size)
{
    uint mask = ((((uint)1) << size) - 1) << pos;
    word &= ~mask; //resettiamo le posizioni
    word |= (value << pos) & mask;
    return word;
}

uint ReadBits(uint word, int pos, int size)
{
    uint mask = ((((uint)1) << size) - 1) << pos;
    return (word & mask) >> pos;
}

那么:

uint the_word;

public uint Itemx
{
    get { return ReadBits(the_word, 5, 2); }
    set { the_word = SetBits(the_word, value, 5, 2) }
}

简单明了的解决方案! - Salman Saleh

5

虽然它是一个类,但使用BitArray似乎是最少重复造轮子的方法。除非你真的需要高性能,否则这是最简单的选择。(可以使用[]运算符引用索引。)


3

我认为,如果你将其设置为字节枚举,则标志枚举也可以起作用:

[Flags] enum PesHeaders : byte { /* ... */ }

3

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