MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

C#文件与流操作(FileStream、MemoryStream)详解

2021-08-236.1k 阅读

C# 中的文件与流操作基础概念

在 C# 编程中,文件与流操作是处理数据持久化和数据传输的重要手段。文件是存储在外部存储设备(如硬盘、U盘 等)上的数据集合,而流则是一种抽象概念,用于在不同数据源和数据目标之间传输数据。流提供了一种连续的、顺序访问数据的方式,就像水流一样,数据依次通过流进行读取或写入。

C# 提供了丰富的类库来处理文件和流操作,其中 FileStreamMemoryStream 是两个常用的类。FileStream 主要用于对文件进行读写操作,它直接与文件系统交互;而 MemoryStream 则是在内存中创建一个流,适用于需要在内存中临时处理数据的场景。

FileStream 类的深入剖析

FileStream 的构造函数

FileStream 类有多个重载的构造函数,这些构造函数允许我们以不同的方式打开或创建文件,并指定文件的访问模式、共享模式以及其他相关选项。

  1. 最常用的构造函数形式
public FileStream(string path, FileMode mode);

这里的 path 参数指定要操作的文件路径,FileMode 枚举指定文件的打开或创建方式。FileMode 枚举包含多个值,例如:

  • Create:如果文件存在则覆盖,不存在则创建新文件。
  • Open:打开现有文件,如果文件不存在则抛出异常。
  • OpenOrCreate:如果文件存在则打开,不存在则创建新文件。

例如,要创建一个新文件并准备写入数据,可以这样使用:

using System.IO;

class Program
{
    static void Main()
    {
        using (FileStream fs = new FileStream("test.txt", FileMode.Create))
        {
            // 这里可以进行写入操作
        }
    }
}

在上述代码中,使用 using 语句确保 FileStream 对象在使用完毕后正确释放资源,避免资源泄漏。

  1. 带有访问模式和共享模式的构造函数
public FileStream(string path, FileMode mode, FileAccess access, FileShare share);

FileAccess 枚举指定对文件的访问类型,包括 Read(只读)、Write(只写)、ReadWrite(读写)。FileShare 枚举指定文件的共享方式,例如 None(不共享,独占访问)、Read(允许其他进程读取文件)、Write(允许其他进程写入文件)等。

下面的代码示例演示了以只读方式打开文件,并允许其他进程读取该文件:

using System.IO;

class Program
{
    static void Main()
    {
        using (FileStream fs = new FileStream("test.txt", FileMode.Open, FileAccess.Read, FileShare.Read))
        {
            // 这里可以进行读取操作
        }
    }
}

FileStream 的读写操作

  1. 写入操作 FileStream 提供了 Write 方法用于将数据写入文件。Write 方法有多种重载形式,常用的是写入字节数组的形式:
public override void Write(byte[] array, int offset, int count);

array 是要写入的字节数组,offset 是数组中开始写入的位置,count 是要写入的字节数。

下面的代码示例将字符串写入文件:

using System.IO;
using System.Text;

class Program
{
    static void Main()
    {
        string content = "Hello, FileStream!";
        byte[] bytes = Encoding.UTF8.GetBytes(content);

        using (FileStream fs = new FileStream("test.txt", FileMode.Create))
        {
            fs.Write(bytes, 0, bytes.Length);
        }
    }
}

在上述代码中,首先将字符串转换为字节数组,然后使用 FileStreamWrite 方法将字节数组写入文件。

  1. 读取操作 FileStream 提供了 Read 方法用于从文件中读取数据。Read 方法也有多种重载形式,常用的是读取字节数组的形式:
public override int Read(byte[] array, int offset, int count);

该方法从文件中读取最多 count 个字节的数据到 array 数组中,从 offset 位置开始存储。返回值是实际读取的字节数,如果到达文件末尾则返回 0。

下面的代码示例从文件中读取数据并输出到控制台:

using System.IO;
using System.Text;

class Program
{
    static void Main()
    {
        byte[] buffer = new byte[1024];
        using (FileStream fs = new FileStream("test.txt", FileMode.Open))
        {
            int bytesRead = fs.Read(buffer, 0, buffer.Length);
            string content = Encoding.UTF8.GetString(buffer, 0, bytesRead);
            Console.WriteLine(content);
        }
    }
}

在上述代码中,创建一个字节数组作为缓冲区,使用 FileStreamRead 方法读取文件内容到缓冲区,然后将缓冲区中的字节转换为字符串并输出。

FileStream 的定位操作

FileStream 允许我们在文件中进行定位操作,即可以移动文件指针到指定位置进行读写。这是通过 Seek 方法实现的:

public override long Seek(long offset, SeekOrigin origin);

offset 是相对于 origin 的偏移量,SeekOrigin 枚举指定起始位置,包括 Begin(文件开头)、Current(当前位置)、End(文件末尾)。

下面的代码示例演示了如何将文件指针移动到文件末尾并写入新的数据:

using System.IO;
using System.Text;

class Program
{
    static void Main()
    {
        string newContent = "\nNew line added at the end.";
        byte[] newBytes = Encoding.UTF8.GetBytes(newContent);

        using (FileStream fs = new FileStream("test.txt", FileMode.OpenOrCreate))
        {
            fs.Seek(0, SeekOrigin.End);
            fs.Write(newBytes, 0, newBytes.Length);
        }
    }
}

在上述代码中,首先使用 Seek 方法将文件指针移动到文件末尾,然后写入新的内容。

MemoryStream 类的深入剖析

MemoryStream 的构造函数

MemoryStream 类用于在内存中创建一个流,它同样有多个重载的构造函数。

  1. 无参数构造函数
public MemoryStream();

使用这个构造函数创建的 MemoryStream 初始容量为 0,随着数据的写入会自动扩展容量。

  1. 指定初始容量的构造函数
public MemoryStream(int capacity);

这里的 capacity 参数指定 MemoryStream 的初始容量。如果预先知道大概需要存储的数据量,指定合适的初始容量可以减少内存重新分配的次数,提高性能。

  1. 基于现有字节数组的构造函数
public MemoryStream(byte[] buffer);
public MemoryStream(byte[] buffer, bool writable);
public MemoryStream(byte[] buffer, int index, int count);
public MemoryStream(byte[] buffer, int index, int count, bool writable, bool publiclyVisible);

这些构造函数允许我们基于现有的字节数组创建 MemoryStreamwritable 参数指定是否可以对该流进行写入操作,indexcount 用于指定从字节数组的哪个位置开始以及使用多少字节。

例如,基于现有字节数组创建一个只读的 MemoryStream

using System.IO;

class Program
{
    static void Main()
    {
        byte[] existingBytes = { 1, 2, 3, 4, 5 };
        using (MemoryStream ms = new MemoryStream(existingBytes, false))
        {
            // 这里只能进行读取操作
        }
    }
}

MemoryStream 的读写操作

  1. 写入操作 MemoryStream 的写入操作与 FileStream 类似,也提供了 Write 方法:
public override void Write(byte[] buffer, int offset, int count);

下面的代码示例向 MemoryStream 中写入字符串:

using System.IO;
using System.Text;

class Program
{
    static void Main()
    {
        string content = "Hello, MemoryStream!";
        byte[] bytes = Encoding.UTF8.GetBytes(content);

        using (MemoryStream ms = new MemoryStream())
        {
            ms.Write(bytes, 0, bytes.Length);
        }
    }
}

在上述代码中,将字符串转换为字节数组后写入 MemoryStream

  1. 读取操作 MemoryStream 提供了 Read 方法用于从流中读取数据:
public override int Read(byte[] buffer, int offset, int count);

下面的代码示例从 MemoryStream 中读取数据并输出:

using System.IO;
using System.Text;

class Program
{
    static void Main()
    {
        string content = "Hello, MemoryStream!";
        byte[] bytes = Encoding.UTF8.GetBytes(content);

        using (MemoryStream ms = new MemoryStream(bytes))
        {
            byte[] readBuffer = new byte[1024];
            int bytesRead = ms.Read(readBuffer, 0, readBuffer.Length);
            string readContent = Encoding.UTF8.GetString(readBuffer, 0, bytesRead);
            Console.WriteLine(readContent);
        }
    }
}

在上述代码中,首先基于字符串创建字节数组并作为 MemoryStream 的数据源,然后从 MemoryStream 中读取数据到缓冲区并转换为字符串输出。

MemoryStream 的特殊操作

  1. 获取字节数组 MemoryStream 提供了 ToArray 方法,用于将流中的数据转换为字节数组:
public byte[] ToArray();

下面的代码示例演示了如何将 MemoryStream 中的数据转换为字节数组:

using System.IO;
using System.Text;

class Program
{
    static void Main()
    {
        string content = "Hello, MemoryStream!";
        byte[] bytes = Encoding.UTF8.GetBytes(content);

        using (MemoryStream ms = new MemoryStream())
        {
            ms.Write(bytes, 0, bytes.Length);
            byte[] resultArray = ms.ToArray();
        }
    }
}
  1. 设置和获取位置 MemoryStream 提供了 Position 属性用于获取或设置当前流的位置:
public override long Position { get; set; }

下面的代码示例演示了如何设置和获取 MemoryStream 的位置:

using System.IO;
using System.Text;

class Program
{
    static void Main()
    {
        string content = "Hello, MemoryStream!";
        byte[] bytes = Encoding.UTF8.GetBytes(content);

        using (MemoryStream ms = new MemoryStream(bytes))
        {
            Console.WriteLine("Initial position: " + ms.Position);
            ms.Position = 5;
            Console.WriteLine("New position: " + ms.Position);
        }
    }
}

FileStream 与 MemoryStream 的比较与适用场景

性能比较

  1. 读取性能
  • FileStream:从文件系统读取数据时,由于涉及磁盘 I/O 操作,其性能相对较慢。磁盘 I/O 操作通常比内存操作慢几个数量级,特别是在读取大量数据时,磁盘的寻道时间和数据传输速率会成为性能瓶颈。
  • MemoryStream:从内存中读取数据,速度非常快,因为内存的访问速度远远高于磁盘。在需要快速处理数据且数据量不是特别大(在内存可承受范围内)时,MemoryStream 具有明显的性能优势。
  1. 写入性能
  • FileStream:写入文件时,同样受到磁盘 I/O 性能的限制。每次写入操作都需要将数据从内存缓冲区刷新到磁盘,这涉及到磁盘的写入操作,速度相对较慢。
  • MemoryStream:写入内存的速度极快,因为只涉及内存操作。然而,如果需要将 MemoryStream 中的数据最终持久化到文件,还需要额外的步骤将内存中的数据写入文件,这会增加一定的开销。

适用场景

  1. FileStream 的适用场景
  • 数据持久化:当需要将数据长期存储在外部存储设备(如硬盘)上时,FileStream 是首选。例如,保存用户设置、日志文件、数据库文件等。
  • 处理大文件:对于非常大的文件,由于内存容量的限制,无法一次性将整个文件加载到内存中处理。FileStream 允许以流的方式逐块读取和写入大文件,避免内存溢出问题。
  1. MemoryStream 的适用场景
  • 临时数据处理:当需要在内存中临时处理数据,如对数据进行加密、压缩、转换格式等操作时,MemoryStream 非常合适。它提供了快速的内存访问速度,便于高效地处理数据。
  • 网络数据传输:在网络编程中,当需要将数据发送到网络或者从网络接收数据时,MemoryStream 可以作为数据的临时存储和处理容器。例如,在处理 HTTP 请求和响应时,MemoryStream 可以用于存储和解析数据。

实际应用案例

案例一:文件加密与解密

在这个案例中,我们将使用 FileStream 来读取和写入文件,并结合 MemoryStream 进行数据的临时处理,实现文件的简单加密和解密。

  1. 加密算法 这里我们使用简单的异或加密算法,对文件中的每个字节与一个固定的密钥进行异或运算。

  2. 代码实现

using System.IO;
using System.Text;

class Program
{
    static void Main()
    {
        string inputFilePath = "input.txt";
        string encryptedFilePath = "encrypted.txt";
        string decryptedFilePath = "decrypted.txt";
        byte key = 0x42; // 密钥

        // 加密文件
        using (FileStream inputFs = new FileStream(inputFilePath, FileMode.Open))
        using (FileStream encryptedFs = new FileStream(encryptedFilePath, FileMode.Create))
        using (MemoryStream ms = new MemoryStream())
        {
            byte[] buffer = new byte[1024];
            int bytesRead;
            while ((bytesRead = inputFs.Read(buffer, 0, buffer.Length)) > 0)
            {
                for (int i = 0; i < bytesRead; i++)
                {
                    buffer[i] ^= key;
                }
                ms.Write(buffer, 0, bytesRead);
            }
            ms.Position = 0;
            ms.CopyTo(encryptedFs);
        }

        // 解密文件
        using (FileStream encryptedFs = new FileStream(encryptedFilePath, FileMode.Open))
        using (FileStream decryptedFs = new FileStream(decryptedFilePath, FileMode.Create))
        using (MemoryStream ms = new MemoryStream())
        {
            byte[] buffer = new byte[1024];
            int bytesRead;
            while ((bytesRead = encryptedFs.Read(buffer, 0, buffer.Length)) > 0)
            {
                for (int i = 0; i < bytesRead; i++)
                {
                    buffer[i] ^= key;
                }
                ms.Write(buffer, 0, bytesRead);
            }
            ms.Position = 0;
            ms.CopyTo(decryptedFs);
        }
    }
}

在上述代码中,首先使用 FileStream 读取原始文件,将数据读入缓冲区,在 MemoryStream 中对每个字节进行异或加密后写入加密文件。解密过程类似,从加密文件读取数据,在 MemoryStream 中解密后写入解密文件。

案例二:内存数据压缩与文件存储

在这个案例中,我们将使用 MemoryStream 对内存中的数据进行压缩,然后使用 FileStream 将压缩后的数据存储到文件中。

  1. 压缩算法 这里我们使用 DeflateStream 类来进行数据压缩,DeflateStream 是.NET 提供的基于 Deflate 算法的压缩流。

  2. 代码实现

using System.IO;
using System.IO.Compression;
using System.Text;

class Program
{
    static void Main()
    {
        string originalContent = "This is a long string that needs to be compressed.";
        byte[] originalBytes = Encoding.UTF8.GetBytes(originalContent);

        using (MemoryStream originalMs = new MemoryStream(originalBytes))
        using (MemoryStream compressedMs = new MemoryStream())
        using (DeflateStream deflateStream = new DeflateStream(compressedMs, CompressionMode.Compress))
        using (FileStream compressedFs = new FileStream("compressed.bin", FileMode.Create))
        {
            originalMs.CopyTo(deflateStream);
            deflateStream.Close();
            compressedMs.Position = 0;
            compressedMs.CopyTo(compressedFs);
        }

        // 解压缩过程
        using (FileStream compressedFs = new FileStream("compressed.bin", FileMode.Open))
        using (MemoryStream compressedMs = new MemoryStream())
        using (MemoryStream decompressedMs = new MemoryStream())
        using (DeflateStream deflateStream = new DeflateStream(compressedMs, CompressionMode.Decompress))
        {
            compressedFs.CopyTo(compressedMs);
            compressedMs.Position = 0;
            compressedMs.CopyTo(deflateStream);
            deflateStream.Close();
            decompressedMs.Position = 0;
            byte[] decompressedBytes = decompressedMs.ToArray();
            string decompressedContent = Encoding.UTF8.GetString(decompressedBytes);
            Console.WriteLine(decompressedContent);
        }
    }
}

在上述代码中,首先将原始字符串转换为字节数组并存储在 MemoryStream 中,然后通过 DeflateStream 进行压缩,压缩后的数据存储在另一个 MemoryStream 中,最后使用 FileStream 将压缩数据写入文件。解压缩过程则是相反的操作,从文件读取压缩数据,通过 DeflateStream 解压缩后恢复原始数据。

通过以上对 FileStreamMemoryStream 的详细介绍、深入剖析以及实际应用案例,相信你对 C# 中的文件与流操作有了更全面和深入的理解。在实际编程中,根据具体的需求和场景,合理选择和使用这两个类,能够高效地处理数据的存储、传输和处理等任务。