如何从/向流写入/读取位? (C#)

Posted

技术标签:

【中文标题】如何从/向流写入/读取位? (C#)【英文标题】:How to write/read bits from/to a Stream? (C#) 【发布时间】:2009-08-22 12:02:56 【问题描述】:

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

【问题讨论】:

你能提供更多的上下文吗?你的意思是读/写二进制数据而不是文本数据?您打算读/写什么类型的数据? 你真的要写单个位(不是字节)吗? 问题很清楚,他需要一次向/从流中写入或读取一个位。这对于编写紧凑的流或 Huffman 压缩是典型的。 【参考方案1】:

您可以在 Stream 上创建一个枚举位的扩展方法,如下所示:

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

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

【讨论】:

整洁地使用扩展方法。但是,您可以添加对以相反顺序读取位的支持。 多次调用ReadBits 每次都会从流中读取一个新字节,即使前一个字节中还有未读取的位。我已经在下面发布了一个解决这个问题的答案。 是的,应该有一个字节序标志。 戴夫,这不正确。第一次迭代读取第一个字节并产生第一位。此时,执行在内部 for 循环内暂停。下一次迭代将从该点恢复并从第一个字节产生下一位。只有在内部 for 循环完成后(即 8 次迭代后)才会读取下一个字节。 @TommyCarlier 请参阅dotnetfiddle.net/Syk1kL - 您的解决方案会跳到下一个字节并随后调用ReadBits【参考方案2】:

这对于默认流类是不可能的。 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,我刚刚添加了一个关于如何做到这一点的快速示例 能否添加一个写位的例子,好吗? 流是 IDisposable:任何包装器(如 BitStream)本身应该是 IDisposable,或者(不太理想)明确记录它不处理 Disposal 的事实。 当你用完当前字节中的位时,你需要一种方法来表示需要读取下一个字节......当你的说明性示例开始成为现实时,你不讨厌它吗您永远不会亲自使用的代码。 :-) @Alon -- 这里的人们倾向于对“你会为我编写代码”类型的问题和 cmets 皱眉。鉴于此代码示例,您确实应该能够弄清楚如何编写位。您只需要将它们累积成一个字节,直到字节满,然后写入字节。【参考方案3】:

如果您需要一次检索几位字节流的单独部分,则需要记住该位的位置,以便在调用之间读取下一个。下面的类负责在调用之间缓存当前字节和其中的位位置。

// 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);
...

【讨论】:

这个解决方案有一个问题:它只保留了缓冲区和 bitPos 变量的 1 个静态实例。当您同时在多个 Streams 上调用 ReadBits 时(例如,从多个线程),缓冲区和 bitPos 将无法为这些不同的 Streams 正确工作。此外,如果您在 1 个 Stream 上调用 ReadBits,完成并在新 Stream 上调用 ReadBits,缓冲区和 bitPos 仍将保留前一个 Stream 中的陈旧数据。 是的,扩展方法代码有限制,因为所有字段都必须是静态的,不允许您对同时打开的不同Streams 进行交叉读取。我已经把代码变成了一个常规的包装类。感谢您的反馈!

以上是关于如何从/向流写入/读取位? (C#)的主要内容,如果未能解决你的问题,请参考以下文章

向/从文件写入/读取位域结构

vbscript 从32位应用程序读取和写入64位注册表

如何c语言中如何将print信息写入一个文件

如何读取和写入位到字节数组

如何从C扩展名(PHP源)读取和写入PHP超全局变量? [处于保留状态]

从一个寄存器读取某些位并写入另一个寄存器的某些位