【问题标题】:How do I save a stream to a file in C#?如何在 C# 中将流保存到文件中?
【发布时间】:2010-09-29 12:32:07
【问题描述】:

我有一个用流初始化的StreamReader 对象,现在我想将此流保存到磁盘(流可能是.gif.jpg.pdf)。

现有代码:

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

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

【问题讨论】:

  • 什么是 myOtherObject?

标签: c# .net stream


【解决方案1】:

不得对二进制文件(如 gif 或 jpg)使用 StreamReaderStreamReader 用于 文本 数据。如果您将其用于任意二进制数据,您几乎肯定会丢失数据。 (如果你使用 Encoding.GetEncoding(28591) 你可能会没事,但有什么意义呢?)

为什么你需要使用StreamReader?为什么不直接将二进制数据as保留为二进制数据并将其作为二进制数据写回磁盘(或 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);
}

请注意,Stream.CopyTo 是在 .NET 4 中引入的,其用途基本相同。

【讨论】:

  • 这似乎很常见,我很惊讶它不在 .NET 中。我看到人们创建整个文件大小的字节数组,这可能会导致大文件出现问题。
  • @Tilendor:它在 .NET 4 中作为扩展方法存在。(CopyTo)
  • 我认为它不是扩展方法,但它是Stream类中的新方法。
  • @Kugel:你是对的,对不起。 将它作为实用程序库中的扩展方法,但现在它在 Stream 本身中,我的扩展方法不会被调用。
  • @Florian:这是相当随意的 - 一个足够小的值以避免占用太多内存,并且足够大以一次传输一个合理的块。最好是 16K,也许是 32K - 我只是要小心不要最终出现在大型对象堆上。
【解决方案2】:

为什么不使用 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);
     }
}

【讨论】:

  • 如果输入流是 1GB 长怎么办 - 此代码将尝试分配 1GB 缓冲区:)
  • 这不适用于 ResponseStream,因为它的长度未知。
  • 虽然您确实必须拥有可用于byte[] 的内存,但我认为您很少会将 1 GB+ blob 流式传输到文件中...除非您有一个保存 DVD 种子的网站……另外,无论如何,如今大多数计算机至少有 2 GB 的可用内存……警告是有效的,但我认为这对于大多数人来说可能“足够好”工作。
  • 网络服务器根本无法容忍这样的情况,除非网站一次只有一个用户处于活动状态。
【解决方案3】:

正如 Tilendor 在 Jon Skeet 的回答中强调的那样,自 .NET 4 以来,流具有 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);
}

【讨论】:

  • 请注意,如果您还没有开始,则必须致电myOtherObject.InputStream.Seek(0, SeekOrigin.Begin),否则您将不会复制整个流。
  • 如果这个输入流是从http连接得到的,那么它会缓冲并下载然后从源写入所有字节?????
  • 我已经在使用流的地方创建了 PDF 查看器,一旦我绑定流并且当我使用相同的流保存 pdf 文件然后不使用 "Seek(0, SeekOrigin.Begin)" 我不会能够保存正确的文档。所以 +1 提到这个“Seek(0, SeekOrigin.Begin)”
  • myOtherObject.InputStream.CopyTo(fileStream);这一行给出了一个错误:访问被拒绝。
  • 有什么理由使用.Seek(0, SeekOrigin.Begin) 而不是.Position = 0?因为在这种情况下both seem do the same thing
【解决方案4】:
//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.
*/

【讨论】:

  • 逐字节复制流(使用 ReadByte/WriteByte)将比逐缓冲区复制(使用 Read(byte[], int, int)/Write(byte[ ], int,int))。
【解决方案5】:
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 - 很好!
  • 这本质上是Jon Skeet 在 2009 年展示的内容。他只是将其重构为两部分,以便可以将流复制部分重新用于任何类型的目标流,而不仅仅是文件。
【解决方案6】:
public void CopyStream(Stream stream, string destPath)
{
  using (var fileStream = new FileStream(destPath, FileMode.Create, FileAccess.Write))
  {
    stream.CopyTo(fileStream);
  }
}

【讨论】:

  • 您可能不应该将stream 对象放在using(){} 括号中。你的方法没有创建流,所以它不应该处理它。
  • 相反,您需要将FileStream 改为使用,否则它将保持打开状态,直到它被垃圾收集。
  • 这运行良好,但我得到了 0 KB 的输出。相反,我必须这样做才能得到正确的输出:File.WriteAllBytes(destinationFilePath, input.ToArray());。在我的例子中,input 是来自ZipArchiveMemoryStream
  • 如果stream 可能不在开头,请将stream.Position = 0; 作为此方法的第一行。
【解决方案7】:
private void SaveFileStream(String path, Stream stream)
{
    var fileStream = new FileStream(path, FileMode.Create, FileAccess.Write);
    stream.CopyTo(fileStream);
    fileStream.Dispose();
}

【讨论】:

  • 这运行良好,但我得到了 0 KB 的输出。相反,我必须这样做才能得到正确的输出:File.WriteAllBytes(destinationFilePath, input.ToArray());。在我的例子中,input 是来自ZipArchiveMemoryStream
  • 这帮助我弄清楚我做错了什么。但是,不要忘记移动到流的开头:stream.Seek(0, SeekOrigin.Begin);
  • stream.Position = 0; 是移动到流开头的另一种语法。
【解决方案8】:

另一种选择是将流获取到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.
}

【讨论】:

  • 如果输入太大,你会得到一个内存不足的异常。将内容从输入流复制到文件流的选项要好得多
【解决方案9】:

我没有使用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 文件进行测试时有效,但我承认我只将它用于小文件(小于 1 MB)。一个流,流之间无需复制,无需编码,只需写入字节!无需使用StreamReader 使事情过于复杂,如果您已经有一个流,您可以直接使用.ToArray() 转换为bytes

我可以看到这样做的唯一潜在缺点是,如果您有一个大文件,将其作为流并使用.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();
}

【讨论】:

  • 由于using(),无需致电Close
  • @Alex78191 如果您说的是inputStream.Close(),请再看一遍——inputStream 是作为变量发送的。 usingpath+filename 输出流上。如果您在 using 中间谈论 fs.Close(),对不起,您是对的,我删除了它。
  • 应在关闭前冲洗。虽然 close 也应该进行刷新。
  • @Andrew 我认为这就是为什么我按照我的顺序执行它们 - 因为我认为您不能在已被刷新的流上执行.Close(),因为.Flush() 将其关闭,我也想同时执行这两个命令。
【解决方案10】:

这是一个正确使用和实现 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());
            }
        }
    }

关键是理解 using 的正确使用(应该在实例化对象上实现 idisposable,如上所示),并了解属性如何为流工作。位置实际上是流中的索引(从 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 与缓冲区的大小和原始缓冲区的大小进行比较,如果它大于原始大小字节数组,结束运行。

【讨论】:

  • FWIW:Jon Skeet 展示了一种更高性能的方法来执行第二个 sn-p,使用读取/写入方法需要一个长度(而不是一次一个字节)。第三个 sn-p 是多余的 - 使内存流保存所有数据 - 对于大数据不实用。再次,请参阅 Jon Skeet 的第二个 sn-p。它具有相同的特性,即一次写入一大块数据。它无需将所有数据都拉入内存,而且代码更简单。
猜你喜欢
  • 2015-03-23
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多