C# 文件 IO

文件读写是程序最常做的事之一——配置、日志、缓存、用户数据。C# 提供了从一行搞定的小文件 API精细控制的流式 API 的全谱系工具。这一篇我们看 File、Directory、Path、Stream、JSON 序列化,以及异步 IO。

一、命名空间

文件 IO 主要在 System.IO 命名空间:

二、File 静态类(小文件首选)

对于小文件,一次性读写最简单:

using System.IO;

// 一次性读写(小文件用,简单直接)
string text = File.ReadAllText("a.txt");
File.WriteAllText("b.txt", "Hello");

string[] lines = File.ReadAllLines("a.txt");
File.WriteAllLines("b.txt", lines);

byte[] bytes = File.ReadAllBytes("image.png");
File.WriteAllBytes("copy.png", bytes);

// 追加
File.AppendAllText("log.txt", $"[{DateTime.Now}] 启动\n");

// 信息查询
bool exists = File.Exists("a.txt");
var info = new FileInfo("a.txt");
Console.WriteLine(info.Length);       // 字节数
Console.WriteLine(info.LastWriteTime);

// 文件操作
File.Copy("a.txt", "b.txt", overwrite: true);
File.Move("a.txt", "c.txt");
File.Delete("b.txt");

注意:

三、Directory 静态类

using System.IO;

// 创建
Directory.CreateDirectory("path/to/dir");
// 内部目录不存在会一并创建,无报错

// 判断
bool exists = Directory.Exists("dir");

// 列出内容
string[] files = Directory.GetFiles("dir");
string[] subdirs = Directory.GetDirectories("dir");

// 带搜索模式
var csFiles = Directory.GetFiles("dir", "*.cs");
var allFiles = Directory.GetFiles("dir", "*.cs", SearchOption.AllDirectories);

// 删除(必须为空,或递归)
Directory.Delete("dir", recursive: true);

// 枚举(大数据推荐,延迟执行)
foreach (string f in Directory.EnumerateFiles("dir"))
    Console.WriteLine(f);

// 当前目录/临时目录
string cwd = Environment.CurrentDirectory;
string temp = Path.GetTempPath();

要点:

四、Path 路径处理(跨平台关键!)

using System.IO;

// 路径拼接:推荐用 Path.Combine,自动处理分隔符
string p1 = Path.Combine("a", "b", "c.txt");   // "a/b/c.txt" 或 "a\b\c.txt"

// 提取路径组成部分
Console.WriteLine(Path.GetDirectoryName("a/b/c.txt"));  // "a/b"
Console.WriteLine(Path.GetFileName("a/b/c.txt"));       // "c.txt"
Console.WriteLine(Path.GetExtension("c.txt"));          // ".txt"
Console.WriteLine(Path.GetFileNameWithoutExtension("c.txt"));   // "c"

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

// 改扩展名(返回新路径,不改原文件)
string renamed = Path.ChangeExtension("a.txt", ".bak");   // "a.bak"

// 跨平台:路径分隔符
// Windows: \
// Linux/Mac: /
// Path.Combine 自动用平台分隔符,所以跨平台代码总是用 Combine
// 而不是用字符串拼接

// C# 不推荐:string path = "a" + "/" + "b.txt";

跨平台注意事项:

五、Stream 流式读写

大文件、网络、二进制数据用逐块读写,内存友好:

using System.IO;

// 读文件流(用 using 自动释放)
using (var fs = new FileStream("a.txt", FileMode.Open, FileAccess.Read))
using (var reader = new StreamReader(fs))
{
    string line;
    while ((line = reader.ReadLine()) != null)
        Console.WriteLine(line);
}

// 写文件流
using (var writer = new StreamWriter("output.txt", append: true))
{
    writer.WriteLine($"时间: {DateTime.Now}");
    writer.Write("不带换行的");
}

// 异步读(C# 推荐,UI/服务器场景)
async Task<string> ReadAllAsync(string path)
{
    using var reader = new StreamReader(path);
    return await reader.ReadToEndAsync();
}

// 字节流(二进制文件)
using var binary = new FileStream("data.bin", FileMode.Create);
binary.Write(new byte[] { 1, 2, 3, 4, 5 }, 0, 5);

核心概念:

六、JSON 序列化

现代 .NET 用 System.Text.Json(高性能、内置、推荐):

using System.Text.Json;
using System.Text.Json.Serialization;

// 序列化对象到 JSON
var user = new User { Id = 1, Name = "Tom", Age = 25 };
string json = JsonSerializer.Serialize(user);
// {"Id":1,"Name":"Tom","Age":25}

// 写文件
File.WriteAllText("user.json", json);

// 读 JSON 反序列化
string content = File.ReadAllText("user.json");
User loaded = JsonSerializer.Deserialize<User>(content);

// 配置选项
var options = new JsonSerializerOptions
{
    WriteIndented = true,                         // 美化输出
    PropertyNamingPolicy = JsonNamingPolicy.CamelCase,   // camelCase
    DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
};
string pretty = JsonSerializer.Serialize(user, options);

// 集合序列化
var list = new List<User> { user, new User { Id = 2, Name = "Amy" } };
string listJson = JsonSerializer.Serialize(list);
var back = JsonSerializer.Deserialize<List<User>>(listJson);

// 异步
await using var stream = File.Create("user.json");
await JsonSerializer.SerializeAsync(stream, user);

常用选项:

第三方库 Newtonsoft.Json 老牌、功能更丰富,但内置 System.Text.Json 在新项目中首选。

七、异步 IO(强烈推荐)

IO 操作(文件、网络)是异步的最佳场景——线程在等磁盘时可以干别的:

// 大文件异步读,不阻塞线程
async Task ProcessAsync()
{
    string content = await File.ReadAllTextAsync("big.txt");
    // 处理 content
}

// 异步写日志
async Task LogAsync(string msg)
{
    await File.AppendAllTextAsync(
        "log.txt",
        $"[{DateTime.Now}] {msg}\n"
    );
}

// 流式处理超大文件(不全加载内存)
async Task<int> CountLinesAsync(string path)
{
    using var reader = new StreamReader(path);
    int count = 0;
    while (await reader.ReadLineAsync() is not null)
        count++;
    return count;
}

// IAsyncEnumerable(C# 8+):异步流
async IAsyncEnumerable<string> ReadLinesAsync(string path)
{
    using var reader = new StreamReader(path);
    string line;
    while ((line = await reader.ReadLineAsync()) is not null)
        yield return line;
}

await foreach (string line in ReadLinesAsync("big.txt"))
    Console.WriteLine(line);

原则:

八、相对路径与工作目录

// 当前工作目录(命令行启动位置决定)
Console.WriteLine(Environment.CurrentDirectory);

// 应用所在目录(程序集位置,推荐用)
Console.WriteLine(AppContext.BaseDirectory);

// 改变工作目录
Environment.CurrentDirectory = @"C:\Temp";

// 跨平台用户目录
string home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
string docs = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
string appData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);

// 推荐:配置/数据文件放在这些标准位置,不要硬编码路径
string configFile = Path.Combine(appData, "MyApp", "config.json");
Directory.CreateDirectory(Path.GetDirectoryName(configFile));

九、文件监视(FileSystemWatcher)

using var watcher = new FileSystemWatcher
{
    Path = "watch_dir",
    IncludeSubdirectories = true,
    NotifyFilter = NotifyFilters.FileName | NotifyFilters.LastWrite
};
watcher.Filter = "*.txt";

watcher.Created += (s, e) => Console.WriteLine($"新建: {e.Name}");
watcher.Changed += (s, e) => Console.WriteLine($"修改: {e.Name}");
watcher.Deleted += (s, e) => Console.WriteLine($"删除: {e.Name}");
watcher.Renamed += (s, e) => Console.WriteLine($"重命名: {e.OldName} -> {e.Name}");

watcher.EnableRaisingEvents = true;

// 注意:
// 1. Windows 比 Linux/Mac 触发更可靠
// 2. 同名事件可能触发多次(写入分多次)
// 3. 大文件可能触发 Created 但内容还没写完

用于日志监控、热重载配置、文件同步等场景。生产环境要小心事件丢失/重复。

十、内存流与压缩

// MemoryStream:内存中的流,适合临时数据
using var ms = new MemoryStream();
using (var writer = new StreamWriter(ms, leaveOpen: true))
    writer.Write("Hello");

ms.Position = 0;   // 重置位置才能读
using var reader = new StreamReader(ms);
Console.WriteLine(reader.ReadToEnd());

// GZip 压缩
using System.IO.Compression;
using var input = File.OpenRead("big.txt");
using var output = File.Create("big.txt.gz");
using var gzip = new GZipStream(output, CompressionMode.Compress);
input.CopyTo(gzip);

// 解压
using var gz = new GZipStream(File.OpenRead("big.txt.gz"), CompressionMode.Decompress);
using var outFs = File.Create("big-decompressed.txt");
gz.CopyTo(outFs);

// ZipFile 整个目录打包
ZipFile.CreateFromDirectory("src", "src.zip");
ZipFile.ExtractToDirectory("src.zip", "extracted");

十一、CSV/JSONL/Parquet 等格式

十二、错误处理与最佳实践

十三、综合示例:简易配置系统

public class AppConfig
{
    public string AppName { get; set; } = "MyApp";
    public int Version { get; set; } = 1;
    public Dictionary<string, string> Settings { get; set; } = new();
}

public static class ConfigManager
{
    static string ConfigPath => Path.Combine(
        AppContext.BaseDirectory, "config.json");

    public static async Task<AppConfig> LoadAsync()
    {
        if (!File.Exists(ConfigPath))
            return new AppConfig();

        string json = await File.ReadAllTextAsync(ConfigPath);
        return JsonSerializer.Deserialize<AppConfig>(json)
               ?? new AppConfig();
    }

    public static async Task SaveAsync(AppConfig config)
    {
        var options = new JsonSerializerOptions { WriteIndented = true };
        string json = JsonSerializer.Serialize(config, options);

        // 原子写:先写临时,再替换
        string tmp = ConfigPath + ".tmp";
        await File.WriteAllTextAsync(tmp, json);
        File.Replace(tmp, ConfigPath, destinationBackupFileName: null);
    }
}

小结

文件 IO 是日常必备技能。记住几个要点:小文件用 File 一次性 API大文件用 Stream + 异步路径用 Path.Combine资源用 using 释放JSON 用 System.Text.Json。到这里 C# 入门系列的核心内容就讲完了!接下来你应该:

← 上一篇 C# 异常处理

返回 C# 教程目录

✈️💬