Skip to content

20-文件 IO 操作

文件 IO(输入/输出)操作用于读写文件和目录。C# 在 System.IO 命名空间中提供了丰富的文件操作类。


一、文件操作的核心类总览

特点适用场景
File静态方法简单的文件读写(少量数据)
FileInfo实例方法需要多次获取文件属性
Directory静态方法目录创建、删除、查找
DirectoryInfo实例方法需要多次获取目录属性
StreamReader/Writer文本流大文件、逐行处理
BinaryReader/Writer二进制流图片、序列化数据
FileStream字节流底层精细控制
Path路径处理路径拼接、解析扩展名

二、文件和目录操作

目录操作

csharp
using System.IO;

// 静态方法(File/Directory)
string dirPath = @"C:\MyFolder";

// 创建目录
Directory.CreateDirectory(dirPath);

// 判断是否存在
bool exists = Directory.Exists(dirPath);

// 获取文件列表
string[] files = Directory.GetFiles(dirPath, "*.txt");  // 只查 .txt 文件
string[] allFiles = Directory.GetFiles(dirPath, "*", SearchOption.AllDirectories);  // 递归

// 获取子目录
string[] subDirs = Directory.GetDirectories(dirPath);

// 删除目录(true 表示递归删除所有子目录和文件)
Directory.Delete(dirPath, true);

// 移动/重命名
Directory.Move(dirPath, @"C:\NewFolder");

// 实例方法(DirectoryInfo)
DirectoryInfo dirInfo = new DirectoryInfo(dirPath);
if (!dirInfo.Exists)
    dirInfo.Create();

foreach (FileInfo file in dirInfo.GetFiles())
{
    Console.WriteLine($"{file.Name} - {file.Length} 字节 - {file.LastWriteTime}");
}

文件操作

csharp
string filePath = @"C:\test.txt";

// 创建(注意:Create 返回 FileStream,需要关闭)
File.Create(filePath).Close();  // 创建空文件

// 复制
File.Copy(filePath, @"C:\test_backup.txt", overwrite: true);

// 移动(也可用于重命名)
File.Move(filePath, @"C:\new\test.txt");

// 删除
File.Delete(filePath);

// 判断是否存在
bool exists = File.Exists(filePath);

// 获取属性(FileInfo)
FileInfo fi = new FileInfo(filePath);
Console.WriteLine($"名称:{fi.Name}");               // test.txt
Console.WriteLine $"完整路径:{fi.FullName}");         // C:\test.txt
Console.WriteLine($"大小:{fi.Length} 字节");
Console.WriteLine($"扩展名:{fi.Extension}");           // .txt
Console.WriteLine($"创建时间:{fi.CreationTime}");
Console.WriteLine($"最后修改:{fi.LastWriteTime}");
Console.WriteLine($"是否为只读:{fi.IsReadOnly}");

// 属性操作
fi.IsReadOnly = true;           // 设置为只读
fi.Attributes |= FileAttributes.Hidden;  // 添加隐藏属性

三、文本文件读写

一次性读写(适合小文件)

csharp
string filePath = @"C:\test.txt";
string content = "第一行内容\n第二行内容";

// 写入(覆盖)
File.WriteAllText(filePath, content);

// 追加
File.AppendAllText(filePath, "\n追加的内容");

// 读取全部
string text = File.ReadAllText(filePath);

// 逐行读写
string[] lines = { "行1", "行2", "行3" };
File.WriteAllLines(filePath, lines);  // 写入多行

string[] readLines = File.ReadAllLines(filePath);  // 读取所有行
foreach (string line in readLines)
    Console.WriteLine(line);

流式读写(适合大文件)

csharp
// StreamWriter:写入
using (StreamWriter writer = new StreamWriter(@"C:\output.txt"))
{
    writer.WriteLine("第一行");
    writer.WriteLine("第二行");
    writer.Write("不换行");
    writer.Write("继续写在同一行");
}

// 追加模式
using (StreamWriter writer = new StreamWriter(@"C:\output.txt", append: true))
{
    writer.WriteLine("追加的内容");
}

// StreamReader:读取
using (StreamReader reader = new StreamReader(@"C:\output.txt"))
{
    // 方式一:全部读取
    string content = reader.ReadToEnd();

    // 方式二:逐行读取(大文件推荐)
    string? line;
    while ((line = reader.ReadLine()) != null)
    {
        Console.WriteLine($"行:{line}");
    }
}

// 指定编码
using (StreamReader reader = new StreamReader(@"C:\file.txt", Encoding.UTF8))
using (StreamWriter writer = new StreamWriter(@"C:\file.txt", false, Encoding.UTF8))
{
    // 处理文件...
}

为什么要使用 using?

文件流使用操作系统资源(文件句柄),必须及时释放,否则其他进程无法访问该文件。

csharp
// ❌ 错误:未释放资源
var reader = new StreamReader(@"C:\test.txt");
string content = reader.ReadToEnd();
// 文件句柄未释放!

// ✅ 正确:using 语句(推荐)
using (var reader = new StreamReader(@"C:\test.txt"))
{
    string text = reader.ReadToEnd();
}  // 自动释放

// ✅ 正确:using 声明(C# 8.0+)
using var reader2 = new StreamReader(@"C:\test.txt");
string text2 = reader2.ReadToEnd();
// 作用域结束时自动释放

File.ReadAllText vs StreamReader 选择

csharp
// 小文件(< 10MB)→ 简单方便
string content = File.ReadAllText(@"C:\small.txt");

// 大文件(> 10MB)→ 流式处理,避免内存爆满
using var reader = new StreamReader(@"C:\large.txt");
string? line;
while ((line = reader.ReadLine()) != null)
{
    // 逐行处理,不需要将全部内容加载到内存
    ProcessLine(line);
}

四、二进制文件读写

BinaryWriter / BinaryReader

csharp
// 写入二进制数据
string filePath = @"C:\data.bin";
using (BinaryWriter writer = new BinaryWriter(File.Open(filePath, FileMode.Create)))
{
    writer.Write(12345);         // int
    writer.Write(3.14159);       // double
    writer.Write("Hello World"); // string
    writer.Write(true);          // bool
    writer.Write(DateTime.Now.Ticks);  // long
}

// 读取(必须按写入的顺序和类型读取)
using (BinaryReader reader = new BinaryReader(File.Open(filePath, FileMode.Open)))
{
    int intVal = reader.ReadInt32();
    double dblVal = reader.ReadDouble();
    string strVal = reader.ReadString();
    bool boolVal = reader.ReadBoolean();
    long longVal = reader.ReadInt64();

    Console.WriteLine($"{intVal}, {dblVal}, {strVal}, {boolVal}");
}

文件复制示例

csharp
static void CopyFile(string sourcePath, string destPath)
{
    const int bufferSize = 8192;  // 缓冲区大小
    using (FileStream source = File.OpenRead(sourcePath))
    using (FileStream dest = File.OpenWrite(destPath))
    {
        byte[] buffer = new byte[bufferSize];
        int bytesRead;

        while ((bytesRead = source.Read(buffer, 0, bufferSize)) > 0)
        {
            dest.Write(buffer, 0, bytesRead);
        }
    }

    Console.WriteLine($"文件已复制:{sourcePath} → {destPath}");
}

// 测试
CopyFile(@"C:\large.zip", @"D:\backup\large.zip");

五、FileStream(底层字节流)

FileStream 提供最底层的文件读写控制,适用于图片、视频等二进制文件。

csharp
string path = @"C:\stream.dat";

// 写入
byte[] data = Encoding.UTF8.GetBytes("Hello FileStream");
using (FileStream fs = new FileStream(path, FileMode.Create, FileAccess.Write))
{
    fs.Write(data, 0, data.Length);
    Console.WriteLine($"写入 {data.Length} 字节");
}

// 读取
using (FileStream fs = new FileStream(path, FileMode.Open, FileAccess.Read))
{
    byte[] buffer = new byte[fs.Length];
    int bytesRead = fs.Read(buffer, 0, buffer.Length);
    string result = Encoding.UTF8.GetString(buffer);
    Console.WriteLine($"读取 {bytesRead} 字节:{result}");
}

FileMode 枚举

说明文件不存在时文件存在时
CreateNew创建新文件创建新文件抛出异常
Create创建(覆盖)创建新文件覆盖
Open打开已有文件抛出异常打开
OpenOrCreate打开或创建创建新文件打开
Append追加创建新文件定位到末尾

FileAccess 枚举

说明
Read只读
Write只写
ReadWrite读写

FileShare 枚举

csharp
// 控制其他进程/线程同时访问的权限
using (FileStream fs = new FileStream(
    @"C:\test.txt",
    FileMode.Open,
    FileAccess.Read,
    FileShare.Read))  // 允许其他进程读取(但不能写入)
{
    // 读取文件...
}
说明
None拒绝共享(独占)
Read允许其他进程读取
Write允许其他进程写入
ReadWrite允许其他进程读写
Delete允许其他进程删除

六、路径操作(Path 类)

Path 类提供跨平台的路径处理方法,比字符串拼接更安全。

csharp
string path = @"C:\Users\张三\Documents\test.txt";

Console.WriteLine(Path.GetFileName(path));                // test.txt
Console.WriteLine(Path.GetFileNameWithoutExtension(path)); // test
Console.WriteLine(Path.GetExtension(path));               // .txt
Console.WriteLine(Path.GetDirectoryName(path));            // C:\Users\张三\Documents
Console.WriteLine(Path.GetFullPath(path));                 // 完整路径(解析相对路径)

// 路径组合(自动处理分隔符)
string fullPath = Path.Combine(@"C:\Users", "张三", "Documents", "test.txt");

// 更改扩展名
string newPath = Path.ChangeExtension(path, ".log");  // C:\Users\张三\Documents\test.log

// 临时文件
string tempFile = Path.GetTempFileName();  // 创建临时文件并返回路径
string tempDir = Path.GetTempPath();       // 临时文件夹路径

// 文件名合法性检查
char[] invalidChars = Path.GetInvalidFileNameChars();
bool isValid = path.IndexOfAny(invalidChars) == -1;

// 获取当前程序运行目录
string currentDir = Environment.CurrentDirectory;
string appDir = AppDomain.CurrentDomain.BaseDirectory;

七、异步文件操作

对大文件使用异步方法避免阻塞主线程。

csharp
// 异步读取
static async Task<string> ReadFileAsync(string path)
{
    using StreamReader reader = new StreamReader(path);
    return await reader.ReadToEndAsync();
}

// 异步写入
static async Task WriteFileAsync(string path, string content)
{
    using StreamWriter writer = new StreamWriter(path);
    await writer.WriteAsync(content);
}

// 异步复制
static async Task CopyFileAsync(string source, string dest)
{
    using FileStream sourceStream = File.OpenRead(source);
    using FileStream destStream = File.OpenWrite(dest);
    await sourceStream.CopyToAsync(destStream);
}

// 使用
string content = await ReadFileAsync(@"C:\large.txt");
await WriteFileAsync(@"C:\output.txt", "异步写入内容");
await CopyFileAsync(@"C:\source.zip", @"C:\dest.zip");

八、综合案例

案例一:日志记录器

csharp
public class FileLogger : IDisposable
{
    private readonly string logPath;
    private readonly StreamWriter writer;
    private readonly object lockObj = new object();

    public FileLogger(string logDirectory = "Logs")
    {
        Directory.CreateDirectory(logDirectory);
        logPath = Path.Combine(logDirectory, $"{DateTime.Now:yyyy-MM-dd}.log");
        writer = new StreamWriter(logPath, append: true);
        writer.AutoFlush = true;  // 自动刷新缓冲区
    }

    public void Log(string level, string message)
    {
        string line = $"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] [{level}] {message}";
        lock (lockObj)  // 线程安全
        {
            writer.WriteLine(line);
        }
    }

    public void Info(string msg) => Log("INFO", msg);
    public void Warn(string msg) => Log("WARN", msg);
    public void Error(string msg) => Log("ERROR", msg);

    public void Dispose()
    {
        writer?.Dispose();
    }
}

// 使用
using (var logger = new FileLogger())
{
    logger.Info("系统启动");
    logger.Warn("内存使用率超过 80%");
    logger.Error("数据库连接失败");
}

案例二:文件批量重命名工具

csharp
static void BatchRename(string directory, string searchPattern, string newPrefix)
{
    string[] files = Directory.GetFiles(directory, searchPattern);
    int count = 0;

    foreach (string filePath in files)
    {
        FileInfo fi = new FileInfo(filePath);
        // 新文件名:前缀 + 序号 + 原扩展名
        string newName = $"{newPrefix}_{++count:D3}{fi.Extension}";
        string newPath = Path.Combine(directory, newName);

        File.Move(filePath, newPath);
        Console.WriteLine($"重命名:{fi.Name} → {newName}");
    }

    Console.WriteLine($"\n共重命名 {count} 个文件");
}

// 使用:将 C:\Photos 下所有 .jpg 文件重命名为 Photo_001.jpg, Photo_002.jpg...
BatchRename(@"C:\Photos", "*.jpg", "Photo");

案例三:INI 配置文件读写

csharp
public class IniFile
{
    private readonly string filePath;
    private Dictionary<string, Dictionary<string, string>> data = new();

    public IniFile(string filePath)
    {
        this.filePath = filePath;
        Load();
    }

    private void Load()
    {
        if (!File.Exists(filePath)) return;

        string currentSection = "";
        foreach (string line in File.ReadAllLines(filePath))
        {
            string trimmed = line.Trim();
            if (trimmed.StartsWith(";") || trimmed.StartsWith("#")) continue;  // 注释

            if (trimmed.StartsWith("[") && trimmed.EndsWith("]"))
            {
                currentSection = trimmed[1..^1];
                data[currentSection] = new Dictionary<string, string>();
            }
            else if (trimmed.Contains('='))
            {
                int idx = trimmed.IndexOf('=');
                string key = trimmed[..idx].Trim();
                string value = trimmed[(idx + 1)..].Trim();
                if (!data.ContainsKey(currentSection))
                    data[currentSection] = new Dictionary<string, string>();
                data[currentSection][key] = value;
            }
        }
    }

    public string Get(string section, string key, string defaultValue = "")
    {
        if (data.TryGetValue(section, out var sectionData))
            if (sectionData.TryGetValue(key, out string value))
                return value;
        return defaultValue;
    }

    public void Set(string section, string key, string value)
    {
        if (!data.ContainsKey(section))
            data[section] = new Dictionary<string, string>();
        data[section][key] = value;
        Save();
    }

    private void Save()
    {
        using var writer = new StreamWriter(filePath);
        foreach (var section in data)
        {
            writer.WriteLine($"[{section.Key}]");
            foreach (var kvp in section.Value)
                writer.WriteLine($"{kvp.Key}={kvp.Value}");
            writer.WriteLine();
        }
    }
}

// 使用
var config = new IniFile(@"C:\config.ini");
string server = config.Get("Database", "Server", "localhost");
int port = int.Parse(config.Get("Database", "Port", "1433"));
config.Set("User", "Name", "张三");

核心知识点总结

文件读写方式选择

场景推荐方式示例
小文本文件(一次性)File.ReadAllText/WriteAllText配置文件、日志记录
大文本文件(逐行)StreamReader/Writer日志文件分析、CSV 处理
二进制文件BinaryReader/Writer图片、序列化
底层字节控制FileStream文件复制、自定义格式
异步操作ReadAsync/WriteAsync大文件、UI 不阻塞

注意事项

  1. 总是使用 using——确保文件资源及时释放
  2. 检查 File.Exists——打开不存在的文件会抛出 FileNotFoundException
  3. 路径使用 Path.Combine——避免手动拼接路径字符串
  4. 编码问题——默认 UTF-8,读取非 UTF-8 文件时指定编码
  5. 权限问题——写入系统目录(如 C:\Windows)可能需要管理员权限
  6. 异常处理——文件操作可能抛出 UnauthorizedAccessExceptionIOExceptionDirectoryNotFoundException
  7. 跨平台路径——Path 类在不同操作系统上使用正确的路径分隔符(/\

Released under the MIT License.