如何在C#中将流保存到文件?

870

我有一个StreamReader对象,它使用流进行了初始化,现在我想将此流保存到磁盘上(该流可能是.gif.jpg.pdf文件)。

现有代码:

StreamReader sr = new StreamReader(myOtherObject.InputStream);
  1. 我需要将这个保存到磁盘(我有文件名)。
  2. 将来我可能想将其存储到SQL Server中。

我也有编码类型,如果我要将其存储到SQL Server中,我将需要它,对吗?


5
我的其他对象是什么? - anhtv13
10个回答

1110

正如Tilendor在Jon Skeet的回答中所强调的那样,自从.NET 4以来,流(Streams)就拥有了一个CopyTo方法。

var fileStream = File.Create("C:\\Path\\To\\File");
myOtherObject.InputStream.Seek(0, SeekOrigin.Begin);
myOtherObject.InputStream.CopyTo(fileStream);
fileStream.Close();

或者使用using语法:

using (var fileStream = File.Create("C:\\Path\\To\\File"))
{
    myOtherObject.InputStream.Seek(0, SeekOrigin.Begin);
    myOtherObject.InputStream.CopyTo(fileStream);
}

如果你不想复制整个流,请调用 Seek,以确保你已经在开头。


5
如果这个输入流是从HTTP连接获取的,那么它会缓存并下载所有来自源的字节,然后将这些字节全部写入吗? - Deepak Bhatia
2
我创建了一个PDF查看器,其中使用了流。一旦我绑定了流并使用相同的流保存PDF文件,如果不使用“Seek(0,SeekOrigin.Begin)”,则无法保存正确的文档。因此,对于提到“Seek(0,SeekOrigin.Begin)”,我给予加1的评价。 - user2463514
@sulhadin,这意味着您没有在fileStream上写入的权限。 - Antoine Leclair
1
在这种情况下,为什么要使用.Seek(0, SeekOrigin.Begin)而不是.Position = 0?因为两者似乎都可以做同样的事情。 - Martin Schneider
@MartinSchneider 很好的问题。我现在不太使用C#,所以我不能确定。根据您提供的链接,似乎您是正确的,我们可以使用.Position = 0,我也更喜欢这种语法。 - Antoine Leclair
显示剩余2条评论

590

绝不能使用StreamReader来处理二进制文件(如gif或jpg)。StreamReader专用于处理文本数据,如果用于任意二进制数据,则几乎肯定会丢失数据。(如果使用Encoding.GetEncoding(28591),那么可能会没问题,但这样做有何意义呢?)

为什么需要使用StreamReader呢?直接将二进制数据保持在二进制状态并将其作为二进制数据写回磁盘(或SQL)即可,不需要进行转换。

编辑:由于这似乎是人们想看到的内容...如果您只想要将一个流复制到另一个流(例如文件),请使用以下代码:

/// <summary>
/// Copies the contents of input to output. Doesn't close either stream.
/// </summary>
public static void CopyStream(Stream input, Stream output)
{
    byte[] buffer = new byte[8 * 1024];
    int len;
    while ( (len = input.Read(buffer, 0, buffer.Length)) > 0)
    {
        output.Write(buffer, 0, len);
    }    
}

例如,要将流转储到文件中,请使用以下命令:

using (Stream file = File.Create(filename))
{
    CopyStream(input, file);
}

请注意,.NET4中引入了Stream.CopyTo,基本上具有相同的功能。

8
这似乎是一个很常见的情况,我很惊讶它没有出现在.NET中。我看到有人创建了整个文件大小的字节数组,这可能会对大文件造成问题。 - Tilendor
84
在.NET 4中,它以扩展方法的形式出现。(CopyTo) - Jon Skeet
35
我认为这不是扩展方法,而是Stream类中的新内容。 - Kugel
9
@Kugel:你说得对,抱歉。之前我把它作为一个扩展方法放在一个工具库中,但现在它已经在Stream自身中了,我的扩展方法就不会被调用了。 - Jon Skeet
4
@Florian:这是相当任意的——选择一个足够小的值以避免占用过多内存,同时又足够大以便一次传输合理数量的数据块。16K或32K都可以,但要小心不要让其成为大对象堆的一部分。 - Jon Skeet
显示剩余3条评论

103
public void CopyStream(Stream stream, string destPath)
{
  using (var fileStream = new FileStream(destPath, FileMode.Create, FileAccess.Write))
  {
    stream.CopyTo(fileStream);
  }
}

33
你可能不应该将 stream 对象放入 using(){} 括号中。你的方法没有创建该流对象,因此也不应该对其进行释放。 - LarsTech
2
你需要使用 FileStream 而不是 using,否则它会一直保持打开状态,直到被垃圾回收。 - Pavel Chikulaev
3
这段代码本来能够运行,但是输出的文件大小为0 KB。正确的做法是用这个代码:File.WriteAllBytes(destinationFilePath, input.ToArray());。在我的情况中,input 是从 ZipArchive 中获取的一个 MemoryStream - SNag
如果 stream 可能不在开头,请将 stream.Position = 0; 作为此方法的第一行。 - ToolmakerSteve

34
private void SaveFileStream(String path, Stream stream)
{
    var fileStream = new FileStream(path, FileMode.Create, FileAccess.Write);
    stream.CopyTo(fileStream);
    fileStream.Dispose();
}

1
这个程序运行得很好,但是输出为0 KB。相反,我必须使用以下代码才能获得正确的输出:File.WriteAllBytes(destinationFilePath, input.ToArray());。在我的情况下,input是来自于ZipArchive中的MemoryStream - SNag
4
这帮助我找出了我的错误。然而,不要忘记移动到流的开头:stream.Seek(0, SeekOrigin.Begin); - Nathan Bills
stream.Position = 0; 是移动到流的开头的另一种语法形式。 - ToolmakerSteve

11

使用CopyTo时可能无法获得全部答案,因为使用该应用程序的系统可能尚未升级到.NET 4.0+。我知道有些人喜欢强制升级,但兼容性也很重要。

另外一件事情,我不明白为什么要使用流从另一个流中复制。为什么不直接这样做:

byte[] bytes = myOtherObject.InputStream.ToArray();

一旦你获得了字节,你可以轻松地将它们写入文件:

public static void WriteFile(string fileName, byte[] bytes)
{
    string path = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
    if (!path.EndsWith(@"\")) path += @"\";

    if (File.Exists(Path.Combine(path, fileName)))
        File.Delete(Path.Combine(path, fileName));

    using (FileStream fs = new FileStream(Path.Combine(path, fileName), FileMode.CreateNew, FileAccess.Write))
    {
        fs.Write(bytes, 0, (int)bytes.Length);
        //fs.Close();
    }
}

这段代码我已经测试过,可以使用 .jpg 文件,不过我只用过小文件(小于1MB)。一个流,没有在流之间复制,也不需要编码,只需写入字节!如果你已经有一个流,可以直接使用 .ToArray() 将其转换为 bytes,就不需要使用 StreamReader 进行过度处理了!唯一的潜在缺点是,如果你有一个大文件,并且使用 .CopyTo() 或相似的方法,则 FileStream 可以通过流式传输而不是使用一个字节数组逐个读取字节。因此,这种方式可能会更慢。但它不会崩溃,因为 FileStream.Write() 方法处理写入字节,而且它只是一次写一个字节,所以不会占用内存,除非你必须有足够的内存来将流作为 byte[] 对象保存。在我使用它的情况下,我得到了一个 OracleBlob,我必须转换为 byte[],它很小,而且除此之外,对我来说没有可用的流,所以我只是把我的字节发送给了上面的函数。
另一种选择是使用流,可以使用 Jon Skeet 的另一篇文章中的 CopyStream 函数 - 这只是使用 FileStream 接收输入流并直接从中创建文件。它不像他所做的那样使用 File.Create(最初对我来说似乎有问题,但后来发现可能只是一个 VS 错误...)。
/// <summary>
/// Copies the contents of input to output. Doesn't close either stream.
/// </summary>
public static void CopyStream(Stream input, Stream output)
{
    byte[] buffer = new byte[8 * 1024];
    int len;
    while ( (len = input.Read(buffer, 0, buffer.Length)) > 0)
    {
        output.Write(buffer, 0, len);
    }    
}

public static void WriteFile(string fileName, Stream inputStream)
{
    string path = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
    if (!path.EndsWith(@"\")) path += @"\";

    if (File.Exists(Path.Combine(path, fileName)))
        File.Delete(Path.Combine(path, fileName));

    using (FileStream fs = new FileStream(Path.Combine(path, fileName), FileMode.CreateNew, FileAccess.Write)
    {
        CopyStream(inputStream, fs);
    }

    inputStream.Close();
    inputStream.Flush();
}

1
不需要调用Close,因为使用了using() - Alex78191
如果你在谈论 inputStream.Close(),请再看一遍 - inputStream 是作为变量发送的。using 应该是在 path+filename 的输出流上。如果你在谈论 using 中间的 fs.Close(),那么抱歉,你是正确的,我已经将其删除。 - vapcguy
2
在关闭之前应该刷新。虽然关闭也应该执行刷新。 - Andrew
@Andrew 我认为这就是我按照特定顺序完成它们的原因 - 因为我认为你不能在已经被刷新的流上执行 .Close(),因为 .Flush() 也会关闭它,并且我想要同时执行这两个命令。 - vapcguy
没有Stream.ToArray()方法,那是从哪里来的?至于为什么要使用Stream而不是直接使用byte[],我会问为什么在你已经有一个Stream的情况下还要引入一个(中间)byte[]?你提到了将整个Stream缓冲到byte[]的缺点——内存影响——但好处是什么?此外,这个答案一开始就质疑了Stream-to-Stream复制的有用性,但随后又建议使用CopyStream(),它的实现方式与CopyTo()相同。 - Lance U. Matthews
@LanceU.Matthews 谁说要执行 Stream.ToArray() 了?看代码。它在那之前就有 InputStream!它是基于该方法,而不是原始流。字节对象可以更容易地进行操作并且可以被存储。这就是好处所在。流必须先被写入,这样才能对底层数据进行操作。所以如果你必须这样做,为什么还要使用它呢?只需使用/复制它作为一个字节对象即可。而且,CopyStream() 是将其写入到一个 byte[] 中,而不是另一个流!所以,不,它不像 CopyTo() 那样简单。你完全没有理解这些内容,是吗? - vapcguy

8

这是一个使用正确的“using”和实现IDisposable接口的示例:

static void WriteToFile(string sourceFile, string destinationfile, bool append = true, int bufferSize = 4096)
{
    using (var sourceFileStream = new FileStream(sourceFile, FileMode.OpenOrCreate))
    {
        using (var destinationFileStream = new FileStream(destinationfile, FileMode.OpenOrCreate))
        {
            while (sourceFileStream.Position < sourceFileStream.Length)
            {
                destinationFileStream.WriteByte((byte)sourceFileStream.ReadByte());
            }
        }
    }
}

还有这个

    public static void WriteToFile(Stream stream, string destinationFile, int bufferSize = 4096, FileMode mode = FileMode.OpenOrCreate, FileAccess access = FileAccess.ReadWrite, FileShare share = FileShare.ReadWrite)
    {
        using (var destinationFileStream = new FileStream(destinationFile, mode, access, share))
        {
            while (stream.Position < stream.Length) 
            {
                destinationFileStream.WriteByte((byte)stream.ReadByte());
            }
        }
    }

重要的是了解使用 (应在实现 idisposable 的对象的实例化中实施,如上所示),并且对流属性的工作原理有一个很好的想法。Position 实际上是流中的索引(从 0 开始),每当使用 readbyte 方法读取每个字节时都会跟随它。在这种情况下,我基本上将其用作 for 循环变量的替代,并让它一直跟随到长度,该长度实际上是整个流(以字节为单位)的结尾。忽略字节,因为它几乎相同,你将拥有像这样简单优雅的东西,可以干净地解决所有问题。
请记住,ReadByte 方法只是在过程中将字节转换为 int,并且可以轻松转换回来。
我将添加另一个实现,最近编写了一种动态缓冲区,以确保顺序数据写入,以防止大规模负载。
private void StreamBuffer(Stream stream, int buffer)
{
    using (var memoryStream = new MemoryStream())
    {
        stream.CopyTo(memoryStream);
        var memoryBuffer = memoryStream.GetBuffer();

        for (int i = 0; i < memoryBuffer.Length;)
        {
            var networkBuffer = new byte[buffer];
            for (int j = 0; j < networkBuffer.Length && i < memoryBuffer.Length; j++)
            {
                networkBuffer[j] = memoryBuffer[i];
                i++;
            }
            //Assuming destination file
            destinationFileStream.Write(networkBuffer, 0, networkBuffer.Length);
        }
    }
}

这个解释相当简单:我们知道需要记住整个数据集,但只想写入一定数量的数据,所以我们希望第一个循环的最后一个参数为空(与while相同)。接下来,我们初始化一个字节数组缓冲区,其大小设置为传递的大小,并使用第二个循环将j与缓冲区和原始数组的大小进行比较,如果它大于原始字节数组的大小,则结束运行。


1
值得一提的是:Jon Skeet 展示了一种更高效的方式来执行第二个代码片段,使用读/写方法来处理一定长度的数据(而不是逐字节处理)。第三个代码片段过于复杂 - 它创建了一个内存流来保存所有数据 - 对于大量数据来说并不实用。再次参考 Jon Skeet 的第二个代码片段。它具有相同的特点,即每次写入一块数据。它可以在不将所有数据拉入内存的情况下完成此操作,并且代码更简单。 - ToolmakerSteve
嗯,说得也对。这是我第一次使用C#时的情况,所以不用担心更正。而且我同意“块”比“字节”更好。 - user10555044

7

为什么不使用FileStream对象呢?

public void SaveStreamToFile(string fileFullPath, Stream stream)
{
    if (stream.Length == 0) return;

    // Create a FileStream object to write a stream to a file
    using (FileStream fileStream = System.IO.File.Create(fileFullPath, (int)stream.Length))
    {
        // Fill the bytes[] array with the stream data
        byte[] bytesInStream = new byte[stream.Length];
        stream.Read(bytesInStream, 0, (int)bytesInStream.Length);

        // Use FileStream object to write to the specified file
        fileStream.Write(bytesInStream, 0, bytesInStream.Length);
     }
}

48
如果输入流达到1GB长,这段代码将尝试分配1GB的缓冲区 :) - Buthrakaur
1
这个无法使用ResponseStream工作,因为它的长度未知。 - Tomas Kubes
虽然需要为byte[]分配足够的内存,但我认为将1 GB以上的blob流式传输到文件中是很少见的...除非你有一个保留DVD种子的网站...此外,现在大多数计算机都至少有2 GB的可用RAM...警告是有效的,但我认为这是一个“对于大多数工作来说足够好”的情况。 - vapcguy
除非网站一次只有一个活跃用户,否则Web服务器对这种情况容忍度极低。 - NateTheGreatt

6
//If you don't have .Net 4.0  :)

public void SaveStreamToFile(Stream stream, string filename)
{  
   using(Stream destination = File.Create(filename))
      Write(stream, destination);
}

//Typically I implement this Write method as a Stream extension method. 
//The framework handles buffering.

public void Write(Stream from, Stream to)
{
   for(int a = from.ReadByte(); a != -1; a = from.ReadByte())
      to.WriteByte( (byte) a );
}

/*
Note, StreamReader is an IEnumerable<Char> while Stream is an IEnumbable<byte>.
The distinction is significant such as in multiple byte character encodings 
like Unicode used in .Net where Char is one or more bytes (byte[n]). Also, the
resulting translation from IEnumerable<byte> to IEnumerable<Char> can loose bytes
or insert them (for example, "\n" vs. "\r\n") depending on the StreamReader instance
CurrentEncoding.
*/

17
使用 ReadByte/WriteByte 按字节复制流会比使用 Read(byte[], int, int)/Write(byte[], int,int) 按缓冲区复制慢得多。 - Kevin

6
另一个选择是将流转换为byte[]并使用File.WriteAllBytes。代码如下:
using (var stream = new MemoryStream())
{
    input.CopyTo(stream);
    File.WriteAllBytes(file, stream.ToArray());
}

将其封装在扩展方法中,可以提高其命名的清晰度:

public void WriteTo(this Stream input, string file)
{
    //your fav write method:

    using (var stream = File.Create(file))
    {
        input.CopyTo(stream);
    }

    //or

    using (var stream = new MemoryStream())
    {
        input.CopyTo(stream);
        File.WriteAllBytes(file, stream.ToArray());
    }

    //whatever that fits.
}

4
如果输入内容过大,你将会收到一个内存溢出异常。从输入流复制内容到文件流的选项要更好一些。 - Ykok

4
public void testdownload(stream input)
{
    byte[] buffer = new byte[16345];
    using (FileStream fs = new FileStream(this.FullLocalFilePath,
                        FileMode.Create, FileAccess.Write, FileShare.None))
    {
        int read;
        while ((read = input.Read(buffer, 0, buffer.Length)) > 0)
        {
             fs.Write(buffer, 0, read);
        }
    }
}

直接将缓冲输入流提供给 FileStream - 很好! - vapcguy
这基本上就是 Jon Skeet 在2009年展示的内容。他只是将其重构为两个部分,以便可以将流复制部分与任何类型的目标流一起重用,而不仅仅是文件。 - ToolmakerSteve

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