C# - 编写通用方法处理位操作

3
我正在开发一个工具,需要符合一个规范,这个规范要求在字节边界上将大量数据压缩到比特中。例如:2个字节编码2个字段,10位值、6位公差。其他字段可能跨越2-4个字节,并被分成更多的字段。
与其在C#中使用位域结构体(如C++中),我认为另一种选择是在发送/接收数据之前创建通用的位打包/解包函数,并使用标准类型(如byte、short、int、long等)在C#中处理所有数据。
我是C#新手,不确定最好的方法是什么。从我所阅读的资料来看,使用unsafe和指针是不被推荐的,但我的尝试使用通用类型失败了。
private static bool GetBitsFromByte<T,U>(T input, byte count, out U output, byte start = 0) where T:struct where U:struct
{
    if (input == default(T))
        return false;

    if( (start + count) > Marshal.SizeOf(input))
        return false;

    if(count > Marshal.SizeOf(output))
        return false;

    // I'd like to setup the correct output container based on the
    // number of bits that are needed
    if(count <= 8)
        output = new byte();
    else if (count <= 16)
        output = new UInt16();
    else if (count <= 32)
        output = new UInt32();
    else if (count <= 64)
        output = new UInt64();
    else
        return false;

    output = 0; // Init output

    // Copy bits out in order
    for (int i = start; i < count; i++)
    {
        output |= (input & (1 << i));  // This is not possible with generic types from my understanding
    }
    return true; 
}

我会使用类似以下的方法从 data_in 中提取10个最低有效位(LSB)到 data_out 中,然后提取接下来的6个位到 next_data_out 中。

Uint32 data_in = 0xdeadbeef;
Uint16 data_out;
byte next_data_out;
if(GetBitsFromByte<Uint32,Uint16>(data_in, 10, out data_out, 0))
{
    // data_out should now = 0x2EF
    if(GetBitsFromByte<Uint32,byte>(data_in, 6, out next_data_out, data_out.Length))
    {
        // next_data_out should now = 0x2F
    }
}

我不想为所有可能的 byte, ushort, uint, ulong 组合编写函数,尽管我猜那是一种替代方法。
我已经查看了 BitConverter 类,但那是用于操作字节数组而不是位。我也明白我不能做类似这样的事情:where T : INumericwhere T : System.ValueType,所以我愿意听取建议。
谢谢!

你考虑过调用非托管的C/C++代码吗?这样可能会更容易些。 - Jay
@Jay 我本来希望能全部用C#完成,但如果其他方法都失败了,那就只能退而求其次了。 - sbtkd85
首先为什么需要泛型?为什么不能使用单独的方法或方法重载? - Sriram Sakthivel
1
这似乎可以在64位中进行所有计算,然后向下转换以存储回结构体。甚至可能运行得更快...(或者T比64位还大吗?) - user645280
不安全的操作和指针使用是不被鼓励的,除非你有充分的理由。但如果确实需要,那么你可能已经有了充分的理由。如果你愿意使用 BitVectors 或 &、| 运算符,那么这将起作用;如果你想使用指针,那就去做吧。在这里没有必要使用 C 代码,C# 可以完成这些转换。 - Alan
显示剩余4条评论
3个回答

3

正如您所知,您无法使用where T : INumeric,因此您编写的任何内容可能都需要有几个变化以支持不同的数字类型。

我可能会使用BitArray并编写必要的转换方法以进行其他数据类型的转换。然后,您最多只需要从每种数字类型中获取一个方法,而不是每种类型组合一个方法。(在C#中有8左右的整数类型,因此最坏情况大约为8+8=16,而不是8×8=64)

如果您不喜欢手动复制/粘贴和更新操作,则可以使用T4 Templates生成8左右的整数类型的方法。

uint data_in = 0xdeadbeef;
ushort data_out;
byte next_data_out;
// pay attention to BitConverter.IsLittleEndian here!
// you might need to write your own conversion methods,
// or do a Reverse() or find a better library
var bits = new BitArray(BitConverter.GetBytes(data_in));
if (bits.TryConvertToUInt16(out data_out, 10))
{
    Console.WriteLine(data_out.ToString("X")); // 2EF
    if (bits.TryConvertToByte(out next_data_out, 6, 10))
    {
        Console.WriteLine(next_data_out.ToString("X")); // 2F
    }
}


private static bool Validate(BitArray bits, int len, int start, int size)
{
    return len < size * 8 && bits.Count > start + len;
}
public static bool TryConvertToUInt16(this BitArray bits, out ushort output, int len, int start = 0)
{
    output = 0;
    if (!Validate(bits, len, start, sizeof(ushort)))
        return false;
    for (int i = start; i < len + start; i++)
    {
        output |= (ushort)(bits[i] ? 1 << (i - start) : 0);
    }
    return true;
}
public static bool TryConvertToByte(this BitArray bits, out byte output, int len, int start = 0)
{
    output = 0;
    if (!Validate(bits, len, start, sizeof(byte)))
        return false;
    for (int i = start; i < len + start; i++)
    {
        output |= (byte)(bits[i] ? 1 << (i - start) : 0);
    }
    return true;
}

我有点困惑,relevant_bits 最终会保存一个 boolIEnumerable,如何高效地将其转换为 ushort - user645280
你可能会像问题中的代码一样使用带有 |= 的循环。 - Tim S.
我更喜欢使用 byte []BitConverter 来完成这个任务。感觉会更快... - user645280
@ebyrob 使用 byte[] 可能会更快,但这可能不重要。BitArray 在内部是一个 int 数组,我认为 BitConverter 不会处理他需要的索引。如果你喜欢使用 byte[] 而不是 BitArray 的语义,你可以这样做。 - Tim S.
我正在比较 IEnumerable<bool>。是的,如果使用正确,BitArray 可能相当快。 - user645280
@TimS。我之前不知道BitArray,所以这对我可能有用。我会试着使用这个类和你建议的方法来得到一个解决方案。谢谢! - sbtkd85

1
这里有几件事情正在发生:
  1. 当你有一个输出参数时,你需要在函数中的某个地方为它赋值。像这样的语句是无效的:

    if( (start + count) > Marshal.SizeOf(input))
        return false; // 没有给输出参数赋值!
    
  2. 同样地,你有很多对输出参数的赋值操作。不要这样做,你在声明中指定了输出参数的类型为 U 类型

    // 不要这样做
    if(count <= 8)
         output = new byte();
    if (...) //等等 
    // 这样做
    output = new U();
    
  3. 即使纠正了上述两点,我仍然不确定你能达到多远。你不能从泛型类型推断任何操作,我认为你也无法为它们赋值。

    // 从 "struct" 参数约束中无法推断出
    output = 0; // 初始化输出参数
    
因此,您可能可以使用硬编码输出的版本(使U成为硬编码类型),但从我的角度来看,尝试使用out通用类型似乎几乎不可能。
编辑:想一想,我不确定您是否能够在通用结构上执行位运算。

0
如果您想在任意结构上使其工作,这有点像序列化问题。请参阅此线程以获取有关此问题的一些信息: 如何在C#中将结构转换为字节数组? 以下是对上述概念进行了一些修改以使其通用的小改动:
class GenericSerializer <T>
{
    public BitArray ToBitArray(T input, int start, int len)
    {
        int structSize = Marshal.SizeOf(input);
        BitArray ret = new BitArray(len);
        int byteStart = start / 8;
        int byteEnd = (start + len) / 8 + 1;
        byte[] buffer = new byte[byteEnd - byteStart];

        IntPtr ptr = Marshal.AllocHGlobal(structSize);
        Marshal.StructureToPtr(input, ptr, false);
        Marshal.Copy(ptr, buffer, byteStart, buffer.Length);
        Marshal.FreeHGlobal(ptr);

        int destBit = 0;
        int sourceBit = start % 8;
        int sourceEnd = sourceBit + len;
        while (sourceBit < sourceEnd)
        {
            ret[destBit] = 0 != (buffer[sourceBit / 8] 
                & (1 << (sourceBit % 8)));
            ++sourceBit;
            ++destBit;
        }

        return ret;
    }

    public T FromBytes(byte[] arr)
    {
        IntPtr ptr = Marshal.AllocHGlobal(arr.Length);
        Marshal.Copy(arr, 0, ptr, arr.Length);

        T output = (T)Marshal.PtrToStructure(ptr, typeof(T));
        Marshal.FreeHGlobal(ptr);

        return output;
    }
}

注意:我只对阅读进行了BitArray,并使用byte []进行了写入。(这里的缺点是每次操作都要完全复制结构体两次,因此性能不会很高)
使用BitConverter或一组函数将转换为/从一些已知类型(例如Int32、Int16、Int64等)可能会运行得更快。

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