如何在C#中从/向流中读取/写入位?

7

如何在C#中将位写入流(System.IO.Stream)或读取流?谢谢。


你能提供更多的上下文吗?你是指读/写二进制数据而不是文本数据吗?你打算读/写什么类型的数据? - Eric Smith
你真的想要编写单个位(而不是字节)吗? - H H
2
问题非常明确,他需要逐位地从流中写入或读取数据。这在编写紧凑的流或哈夫曼压缩时很常见。 - Cecil Has a Name
4个回答

14

您可以在Stream上创建一个扩展方法,用于枚举位(bits),就像这样:

public static class StreamExtensions
{
    public static IEnumerable<bool> ReadBits(this Stream input)
    {
        if (input == null) throw new ArgumentNullException("input");
        if (!input.CanRead) throw new ArgumentException("Cannot read from input", "input");
        return ReadBitsCore(input);
    }

    private static IEnumerable<bool> ReadBitsCore(Stream input)
    {
        int readByte;
        while((readByte = input.ReadByte()) >= 0)
        {
            for(int i = 7; i >= 0; i--)
                yield return ((readByte >> i) & 1) == 1;
        }
    }
}

使用这个扩展方法很容易:

foreach(bool bit in stream.ReadBits())
{
    // do something with the bit
}

注意:不应该在同一个流上多次调用ReadBits方法,否则后续的调用将忘记当前位的位置,直接开始读取下一个字节。


1
使用扩展方法非常整洁。但是,您可以添加支持以相反顺序读取位的功能。 - Cecil Has a Name
多次调用ReadBits将每次从流中读取一个新字节,即使有未读位于上一个字节中。我在下面发布了一个解决此问题的答案。 - Dave R.
是的,应该有一个字节序标志。 - JAlex
Dave,那不正确。第一次迭代读取第一个字节并产生第一个位。此时,在内部for循环中暂停执行。下一次迭代将从该点恢复,并从第一个字节产生下一个位。只有在内部for循环完成后(即经过8次迭代后),才会读取下一个字节。 - Tommy Carlier
@Dave 对不起,我误解了你的陈述。现在我明白了。但是为什么你要多次调用ReadBits呢?即使你想支持这种情况,如果你同时在多个流(例如来自多个线程)上调用ReadBits,你的解决方案也会有问题。我会在你的解决方案中添加更多细节的注释。 - Tommy Carlier
显示剩余2条评论

4

使用默认的流类是不可能实现的。C#(BCL)Stream类在最底层以字节为粒度进行操作。您可以编写一个包装器类,该类读取字节并将其分割成位。

例如:

class BitStream : IDisposable {
  private Stream m__stream;
  private byte? m_current;
  private int m_index;
  public byte ReadNextBit() { 
    if ( !m_current.HasValue ) {
      m_current = ReadNextByte();
      m_index = 0;
    }
    var value = (m_byte.Value >> m_index) & 0x1;
    m_index++;
    if (m_index == 8) {
      m_current = null;
    }
    return value;
  }
  private byte ReadNextByte() {
    ...
  }
  // Dispose implementation omitted
}

注意:这将以从右到左的方式读取位,这可能与您的意图相符或不符。

@Alon,我刚刚添加了一个关于如何执行此操作的快速示例。 - JaredPar
请问您能否添加一个写位操作的例子? - Alon Gubkin
流(Streams)是可处置(IDisposable)的:任何包装器例如BitStream应该要么本身是IDisposable,要么(不太好的选择)明确记录它不处理处置(Disposal)。 - Eamon Nerbonne
当你在当前字节中用尽了位时,你需要一种信号来表示需要读取下一个字节...你是否讨厌当你的说明性示例开始成为你个人永远不会使用的真正代码。 :-) - tvanfosson
@Alon -- 这里的人们倾向于对“你能为我写代码吗”这类问题和评论表示不满。你真的应该能够根据这个代码示例来编写代码。你只需要将它们累积到一个字节中,直到字节满了,然后再写入字节。 - tvanfosson

0
如果您需要逐位检索字节流的不同部分,您需要在调用之间记住下一个要读取的位的位置。以下类负责在调用之间缓存当前字节和其中的位位置。
// Binary MSB-first bit enumeration.
public class BitStream
{
    private Stream wrapped;
    private int bitPos = -1;
    private int buffer;

    public BitStream(Stream stream) => this.wrapped = stream;

    public IEnumerable<bool> ReadBits()
    {
        do
        {
            while (bitPos >= 0)
            {
                yield return (buffer & (1 << bitPos--)) > 0;
            }
            buffer = wrapped.ReadByte();
            bitPos = 7;  
        } while (buffer > -1);
    }
}

调用方式如下:

var bStream = new BitStream(<existing Stream>);
var firstBits = bStream.ReadBits().Take(2);
var nextBits = bStream.ReadBits().Take(3);
...

这个解决方案存在一个问题:它只保留了缓冲区和位点变量的1个静态实例。当您同时从多个流(例如,从多个线程)调用ReadBits时,缓冲区和位点将无法正确地为这些不同的流工作。此外,如果您在1个流上调用ReadBits,完成后再在新流上调用ReadBits,则缓冲区和位点仍将保留来自先前流的陈旧数据。 - Tommy Carlier
是的,扩展方法代码有限制,因为所有字段都必须是静态的,这样就不允许您交错读取同时打开的不同“Stream”。我已经将代码转换为常规包装类。感谢您的反馈! - Dave R.

0

针对您的需求,我编写了一个易于使用、快速且开源(MIT许可证)的库,名为“BitStream”,可在github上获取(https://github.com/martinweihrauch/BitStream)。

在此示例中,您可以看到如何将5个无符号整数(均小于值63)用每个6位写入流中,然后再读回。请注意,该库采用long或ulong值进行输入和输出以方便使用,因此请先将您的int、uint等转换为long/ulong。

using SharpBitStream;

uint[] testDataUnsigned = { 5, 62, 17, 50, 33 };
var ms = new MemoryStream();
var bs = new BitStream(ms);
Console.WriteLine("Test1: \r\nFirst testing writing and reading small numbers of a max of 6 bits.");

Console.WriteLine("There are 5 unsigned ints , which shall be written into 6 bits each as they are all small than 64: 5, 62, 17, 50, 33");
foreach(var bits in testDataUnsigned)
{
    bs.WriteUnsigned(6, (ulong)bits);
}

Console.WriteLine("The original data are of the size: " + testDataUnsigned.Length + " bytes. The size of the stream is now: " + ms.Length + " bytes\r\nand the bytes in it are: ");

ms.Position = 0;

Console.WriteLine("The resulting bytes in the stream look like this: ");
for (int i = 0; i < ms.Length; i++)
{
    uint bits = (uint)ms.ReadByte();
    Console.WriteLine("Byte #" + Convert.ToString(i).PadLeft(4, '0') + ": " + Convert.ToString(bits, 2).PadLeft(8, '0'));
}

Console.WriteLine("\r\nNow reading the bits back:");
ms.Position = 0;
bs.SetPosition(0, 0);

foreach (var bits in testDataUnsigned)
{
    ulong number = (uint)bs.ReadUnsigned(6);
    Console.WriteLine("Number read: " + number);
}

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