从MemoryStream写入并读取数据

70

我正在使用 DataContractJsonSerializer,它喜欢将输出写入流中。我想在序列化器的输出前后添加一些内容,所以我使用了一个 StreamWriter 来交替写入我需要的额外内容。

var ser = new DataContractJsonSerializer(typeof (TValue));

using (var stream = new MemoryStream())
{   
    using (var sw = new StreamWriter(stream))
    {
        sw.Write("{");

        foreach (var kvp in keysAndValues)
        {
            sw.Write("'{0}':", kvp.Key);
            ser.WriteObject(stream, kvp.Value);
        }

        sw.Write("}");
    }

    using (var streamReader = new StreamReader(stream))
    {
        return streamReader.ReadToEnd();
    }
}
当我这样做时,我会得到一个“参数异常”,提示为“流不可读”。 我可能在多个地方做错了,因此欢迎所有答案。谢谢。
4个回答

143

有三件事情:

  • 不要关闭 StreamWriter。那会关闭MemoryStream。不过你需要刷新 writer。
  • 在读取之前重置流的位置。
  • 如果你要直接向流写入内容,你需要先刷新 writer。

所以:

using (var stream = new MemoryStream())
{
    var sw = new StreamWriter(stream);
    sw.Write("{");

    foreach (var kvp in keysAndValues)
    {
        sw.Write("'{0}':", kvp.Key);
        sw.Flush();
        ser.WriteObject(stream, kvp.Value);
    }    
    sw.Write("}");            
    sw.Flush();
    stream.Position = 0;

    using (var streamReader = new StreamReader(stream))
    {
        return streamReader.ReadToEnd();
    }
}

不过还有一个更简单的选择。当您进行读取操作时,您所做的只是将流转换为字符串。您可以更简单地完成这个过程:

return Encoding.UTF8.GetString(stream.GetBuffer(), 0, (int) stream.Length);

不幸的是,如果流已关闭,MemoryStream.Length会抛出异常,因此您可能希望调用不关闭基础流的StreamWriter构造函数,或者只需不关闭StreamWriter

我对您直接写入流感到担忧- ser是什么?它是XML序列化器还是二进制序列化器?如果是二进制的话,您的模型有些缺陷-在不非常小心的情况下,不应混合二进制和文本数据。如果是XML,您可能会发现字符串中间会有字节顺序标记,这可能会引起问题。


2
没想到直接获取缓冲区,不错。 - ShuggyCoUk
1
这听起来好像你没有正确地进行刷新。你是否在Jon的示例中添加了所有的刷新调用? - ShuggyCoUk
3
sw.Flush(); 感到庆幸。我疯狂地尝试弄清楚为什么 MemoryStream 写入数据却没有任何数据! - Karthic Raghupathi
3
这是唯一让我成功的方法。我无法使用Encoding.UTF8.GetString()方法,因为它提示该闭合流无法使用。 - wbinford
2
在 .net 4.5 中,有一个重载的 StreamWriter 构造函数,它配置了 StreamWriter 在写入器被处理时保持流处于打开状态。这将允许您保留使用块并避免刷新调用。https://msdn.microsoft.com/zh-cn/library/gg712853(v=VS.110,d=hv.2).aspx - Simon Giles
显示剩余15条评论

8

将内存流的位置设置为开头可能有所帮助。

 stream.Position = 0; 

但是核心问题是,在关闭时StreamWriter会关闭您的内存流。

只需在您结束使用块时刷新该流,并且在读取内存流数据后仅处理它将为您解决此问题。

您还可以考虑使用StringWriter代替...

using (var writer = new StringWriter())
{
    using (var sw = new StreamWriter(stream))
    {
        sw.Write("{");

        foreach (var kvp in keysAndValues)
        {
            sw.Write("'{0}':", kvp.Key);
            ser.WriteObject(writer, kvp.Value);
        }
        sw.Write("}");
    }

    return writer.ToString();
}

这将需要您的序列化WriteObject调用能够接受TextWriter而不是Stream。

是的,抱歉我现在已经添加了ser的声明。它只接受XmlWriter,这并不真正帮助我,因为我不能添加'{'和其他东西。我猜它会在底层使用XmlSerializer,但我现在不想考虑那个。设置位置没有任何效果。 - Gaz

6

在一个MemoryStream对象被关闭后,可以使用ToArray()GetBuffer()方法访问其内容。下面的代码演示了如何将内存缓冲区的内容作为UTF8编码的字符串获取。

byte[] buff = stream.ToArray(); 
return Encoding.UTF8.GetString(buff,0,buff.Length);

注意:ToArray()GetBuffer()更简单易用,因为ToArray()返回的是流的确切长度,而不是缓冲区大小(可能大于流内容)。ToArray()会复制字节。
注意:GetBuffer()ToArray()更高效,因为它不会复制字节。您需要注意可能在缓冲区末尾存在未定义的尾随字节,考虑流长度而不是缓冲区大小。如果流大小超过80000字节,则强烈建议使用GetBuffer(),因为ToArray复制将分配在大对象堆上,其生命周期可能会有问题。
还可以通过以下方式克隆原始MemoryStream,以便通过StreamReader访问它。
using (MemoryStream readStream = new MemoryStream(stream.ToArray()))
{
...
}

如果可能的话,最理想的解决方案是在内存流关闭之前访问原始内存流。


2
注意:ToArray()比GetBuffer()更好,因为ToArray()返回流的确切长度,而不是缓冲区大小(可能大于流内容)。 但是你为什么要在意呢?将过大的数组传递给Encoding.GetString没有任何副作用-我们知道要告诉它解码的字节数。GetBuffer不创建副本正是你使用它的原因-它避免了ToArray创建的无意义的副本。 - Jon Skeet
好观点 @JonSkeet 我会更新我的答案以反映性能方面的考虑。 如果有人感兴趣,这里有一个关于MemoryStream.GetBuffer优点的堆栈交换问题 https://dev59.com/YWcs5IYBdhLWcg3wMxFF - Simon Giles

0
只是一个猜测:也许你需要刷新StreamWriter?可能系统看到有“待处理”的写入。通过刷新,你可以确定流包含所有已写入的字符并且可读取。

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