使用 StreamReader 和 StreamWriter 类读取和写入文本文件

已完成

在 C# 应用程序中,文本文件通常用于存储和交换数据。 命名空间 System.IO 提供用于读取和写入文本文件的类,让开发人员能够轻松操作文本数据。 用于此目的的两个常用类是 StreamReaderStreamWriter

StreamReader 类用于从特定编码中的字节流读取字符,而该 StreamWriter 类用于将字符写入特定编码中的流。 这些 StreamReaderStreamWriter 类可用于处理文本文件,例如逗号分隔值(CSV)文件,其中数据按行和列进行组织。

什么是流?

在 .NET 中,流用于表示可从中读取或写入的字节序列。 流提供了一种以灵活、高效的方式处理数据的方法,允许以各种格式(包括文本、二进制和网络流)读取和写入数据。 该 Stream 类是 .NET 中所有流的基类,它提供读取和写入数据的方法,以及查找流中特定位置的方法。

Stream 是抽象类,这意味着无法直接实例化。 而是使用派生类(例如FileStreamMemoryStreamNetworkStream和其他类)来处理特定类型的数据源。

这些 StreamReaderStreamWriter 类基于 Stream 类构建,提供用于读取和写入文本数据的其他功能。

使用 StreamReader 和 StreamWriter 类读取和写入文本文件

这些 StreamReaderStreamWriter 类旨在处理文本数据,该数据通常表示为字符序列。 它们处理字符的编码和解码,允许你处理各种格式的文本文件,包括 UTF-8、ASCII 和 Unicode。

StreamReader 类用于从流中读取文本,而该 StreamWriter 类用于将文本写入流。

注释

StreamReaderStreamWriter类实现IDisposable接口,这意味着它们应在using语句中使用,以确保在操作完成后正确释放资源。

使用 StreamWriter 编写文本文件

StreamWriter 类用于将字符写入特定编码中的流。 它提供按行或字符逐个字符写入文本文件的方法。 该 StreamWriter 类可用于编写大型文件,因为它允许以内存高效的方式写入数据。

StreamWriter 类包括以下方法:

  • Write():将指定的字符串写入当前流。
  • WriteLine():将指定的字符串后跟行终止符写入当前流。
  • Flush():清除当前编写器的所有缓冲区,并导致任何缓冲数据写入基础设备。

以下示例演示如何使用 StreamWriter 类将数据写入 CSV 文件:


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

class Program
{
    static void Main()
    {
        string filePath = "data.csv";

        using (StreamWriter writer = new StreamWriter(filePath, false, Encoding.UTF8))
        {
            // Write some data
            writer.WriteLine("Name,Age,Occupation");
            writer.WriteLine("Elize Harmsen,30,Engineer");
            writer.WriteLine("Peter Zammit,25,Designer");
            writer.WriteLine("Niki Demetriou,35,Manager");
        }

        Console.WriteLine($"CSV file created at: {filePath}");
    }
}

此代码创建 CSV 文件,并将一些示例数据写入其中。 该 StreamWriter 类会自动处理编码,以便轻松编写各种格式的文本文件。

using 语句可确保 StreamWriter 在使用后被正确处理,并释放与其关联的任何资源。 这对于管理系统资源以及防止内存泄漏非常重要。 如果不使用using语句,则应该在StreamWriter上调用Close()方法,以确保文件已正确关闭并释放资源。 也可以直接调用 Dispose() 该方法,但建议使用 using 语句。

使用 StreamReader 读取文本文件

StreamReader 类用于从特定编码中的字节流读取字符。 它提供按行或字符逐个字符读取文本文件的方法。 该 StreamReader 类可用于读取大型文件,因为它允许以内存高效的方式读取数据。

StreamReader 类包括以下方法:

  • Read():从输入流中读取下一个字符,并将读取器的位置提升一个字符。
  • ReadLine():从当前流中读取一行字符,并将其作为字符串返回。
  • ReadToEnd():从当前位置读取流末尾的所有字符,并将其作为字符串返回。
  • Peek():返回下一个可用字符,而不移动读取器的位置。

以下示例演示如何使用 StreamReader 类逐行读取 CSV 文件:


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

class Program
{
    static void Main()
    {
        string filePath = "data.csv";

        using (StreamReader reader = new StreamReader(filePath, Encoding.UTF8))
        {
            string line;
            while ((line = reader.ReadLine()) != null)
            {
                Console.WriteLine(line);
            }
        }
    }
}

此代码逐行读取 CSV 文件,并将每行打印到控制台。 该 StreamReader 类自动处理编码,使以各种格式轻松读取文本文件。

using 语句确保 StreamReader 在使用后被正确处理,释放与其关联的任何资源。 与类一样 StreamWriterStreamReader 类提供 Close()Dispose() 方法,但建议使用 using 语句。

在面向对象的应用程序中使用 CSV 文件

CSV 文件通常用于存储和交换数据,并且可以使用StreamReaderStreamWriter类来轻松读取和写入它们。 但是,使用 CSV 文件时,可能需要在 CSV 格式和面向对象的表示形式之间转换数据。 例如,如果应用程序使用 CSV 文件来存储或交换 Employee 类信息,则需要在 CSV 字符串和 Employee 对象之间进行转换。

有很多方法可以编写 CSV 字符串和对象之间的代码。 使用StringBuilder类和String.Split方法是最常见的方法之一。 该 StringBuilder 类用于有效地生成字符串,而 String.Split 该方法用于将 CSV 字符串分析为其各个组件。

StringBuilder 类是命名空间的 System.Text 一部分,提供可变字符串表示形式,无需创建新的字符串实例即可对其进行修改。 这在从对象构造 CSV 字符串时非常有用,因为它允许高效地连接和操作字符串数据。

该方法 String.Split 用于根据指定的分隔符将字符串拆分为子字符串数组。 对于 CSV 文件,分隔符通常是逗号(,)。 该方法 Split 可用于将 CSV 字符串分析为其各个组件,从而允许从分析的数据创建对象。

以下示例演示如何创建 Employee 类、将员工数据写入 CSV 文件,以及如何将数据读回到 Employee 对象中:

using System;
using System.Collections.Generic;
using System.IO;
using System.Text;

class Employee
{
    public string Name { get; set; }
    public int Age { get; set; }
    public string Occupation { get; set; }

    public override string ToString()
    {
        return $"{Name},{Age},{Occupation}";
    }
}

class Program
{
    static void Main()
    {
        string filePath = "data_with_header.csv";

        // Create a list of employees
        List<Employee> employees = new List<Employee>
        {
            new Employee { Name = "Elize Harmsen", Age = 30, Occupation = "Engineer" },
            new Employee { Name = "Peter Zammit", Age = 25, Occupation = "Designer" },
            new Employee { Name = "Niki Demetriou", Age = 35, Occupation = "Manager" }
        };

        // Write employees to a CSV file with a header
        using (StreamWriter writer = new StreamWriter(filePath))
        {
            StringBuilder sb = new StringBuilder();
            sb.AppendLine("Name,Age,Occupation"); // Write the header

            foreach (var employee in employees)
            {
                sb.AppendLine(employee.ToString()); // Write each employee as a CSV row
            }

            writer.Write(sb.ToString());
        }

        // Append additional employees to the CSV file without a header
        List<Employee> additionalEmployees = new List<Employee>
        {
            new Employee { Name = "Hannah Haynes", Age = 28, Occupation = "Developer" },
            new Employee { Name = "Conrad Nuber", Age = 32, Occupation = "Analyst" }
        };

        using (StreamWriter writer = new StreamWriter(filePath, true))
        {
            StringBuilder sb = new StringBuilder();

            foreach (var employee in additionalEmployees)
            {
                sb.AppendLine(employee.ToString()); // Append each employee as a CSV row
            }

            writer.Write(sb.ToString());
        }

        // Create a list to hold employees read from the CSV file
        List<Employee> readEmployees = new List<Employee>();

        // Read the CSV file and create Employee objects
        using (StreamReader reader = new StreamReader(filePath))
        {
            string headerLine = reader.ReadLine(); // Read the header line
            Console.WriteLine($"Header: {headerLine}");


            string line;
            while ((line = reader.ReadLine()) != null)
            {
                string[] data = line.Split(',');
                Employee employee = new Employee
                {
                    Name = data[0],
                    Age = int.Parse(data[1]),
                    Occupation = data[2]
                };
                readEmployees.Add(employee);
            }
        }

        // Display the list of employees
        foreach (var employee in readEmployees)
        {
            Console.WriteLine($"Name: {employee.Name}, Age: {employee.Age}, Occupation: {employee.Occupation}");
        }

    }
}

在此示例中,我们创建了一个具有NameAgeOccupation属性的Employee类。 然后,我们创建一个员工列表,并将其数据写入包含标题行的 CSV 文件。 之后,我们会将其他员工追加到同一 CSV 文件,而无需再次写入标头。 最后,我们将 CSV 文件读回 Employee 对象并显示其信息。 重写 Employee 类的方法 ToString(),以 CSV 格式提供对象的字符串表示形式。 这样,我们就可以在写入文件时轻松地将对象转换为 Employee CSV 字符串。

是否应在 CSV 文件中包括标头?

在 CSV 文件中包括标头是一种常见做法,但是否包含标头取决于所表示数据的特定用例和要求。 标题行通常包含列的名称,为后面的数据提供上下文。 它对于人类可读性和对数据的编程访问都很有用。

你可能希望将标头包含在 CSV 文件中的原因有多种:

  • 可读性:标题行为数据提供上下文,使用户更容易理解每列的含义。 与可能不熟悉数据结构的其他人员共享文件时,这一点尤其重要。

  • 数据完整性:包括标题行有助于通过明确定义每个列的预期格式和内容来防止数据输入错误。 将数据导入数据库或其他系统时,这非常有用。

  • 互作性:许多使用 CSV 文件的应用程序和库需要标头行。 包括标头可以提高与这些工具的兼容性,并更轻松地在不同系统之间导入或导出数据。

  • 文档:标题行充当数据的一种文档形式,提供有关数据结构和含义的信息,而无需其他文档或注释。

在某些情况下,你可能不希望在 CSV 文件中包括标头:

  • 计算机可读文件:如果 CSV 文件旨在由已知道数据结构的程序或系统使用(例如列的顺序和含义),则可能不需要标头。 这可以稍微减小文件大小并简化分析。

  • 性能优化:在性能至关重要且文件非常大的情况下,省略标头可以节省处理时间和存储空间,尤其是在标头对于预期用途而言是冗余的情况下。

  • 旧系统:某些旧系统或工具可能不支持 CSV 文件中的标头,并且预期原始数据在顶部没有任何描述性行。

  • 极简数据共享:如果在结构记录良好或已同意的上下文中共享文件(例如,在已知道架构的协作者之间),则可能不需要标头。

但是,在大多数情况下,建议包括标头,因为它提高了文件的可读性和可用性,尤其是对于人类用户,或者文件的结构未预定义时。 标头为数据提供上下文,并使它更易于解释、调试和其他系统集成。

摘要

在本单元中,你学习了如何使用 C# 中的StreamReaderStreamWriter类来读取和写入文本文件。 你还了解了如何使用类和StringBuilder方法在 CSV 字符串和对象String.Split之间转换。