C# 文件 IO
文件读写是程序最常做的事之一——配置、日志、缓存、用户数据。C# 提供了从一行搞定的小文件 API 到精细控制的流式 API 的全谱系工具。这一篇我们看 File、Directory、Path、Stream、JSON 序列化,以及异步 IO。
一、命名空间
文件 IO 主要在 System.IO 命名空间:
- File / FileInfo:文件操作。
- Directory / DirectoryInfo:目录操作。
- Path:跨平台路径处理。
- FileStream / StreamReader / StreamWriter:流式读写。
- BinaryReader / BinaryWriter:二进制数据。
二、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");注意:
- 整文件加载到内存——不适合大文件(GB 级)。
- ReadAllText/WriteAllText 是同步的;推荐用 ReadAllTextAsync/WriteAllTextAsync。
- FileInfo 是实例版本(同一个文件做多次操作时更高效)。
三、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();要点:
- Create.CreateDirectory 不报错:目录已存在也成功,适合"幂等"创建。
- Delete 必须 empty 或 recursive=true。
- EnumerateFiles 比 GetFiles 高效:延迟枚举,大目录不一次性加载所有路径。
- SearchOption.AllDirectories 递归遍历,但要注意无权限会抛异常。
四、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";跨平台注意事项:
- 永远用 Path.Combine,不要字符串拼接(分隔符不同)。
- 用 Path.DirectorySeparatorChar 处理特殊情况。
- 避免硬编码路径:用环境变量、AppContext.BaseDirectory、当前目录。
- Windows 路径不区分大小写,Linux/Mac 区分——开发测试都要考虑。
五、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);核心概念:
- Stream:字节序列的抽象,File、Network、Memory 都可做流。
- StreamReader/Writer:文本读写封装,自动处理编码(默认 UTF-8)。
- BinaryReader/Writer:二进制读写,处理 int/double 等。
- BufferedStream:为流加缓冲,提高小批量读写性能。
六、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);常用选项:
- WriteIndented:美化输出。
- PropertyNamingPolicy:命名策略(CamelCase / SnakeCaseLower)。
- DefaultIgnoreCondition:控制何时忽略字段。
- [JsonPropertyName("xxx")]:属性重命名。
- [JsonIgnore]:忽略属性。
第三方库 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);原则:
- 用 Async 版本:ReadAllTextAsync、WriteAllTextAsync、CopyToAsync。
- 大文件流式:ReadLineAsync 一行一行读,不全加载。
- IAsyncEnumerable:异步流,生产消费异步。
- UI/服务器场景必须异步:否则阻塞线程,响应慢。
八、相对路径与工作目录
// 当前工作目录(命令行启动位置决定)
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 等格式
- CSV:用第三方库 CsvHelper(简单可靠)。
- JSONL:每行一个 JSON,
StreamReader+JsonSerializer.Deserialize行级处理。 - XML:内置
System.Xml.Serialization或XmlDocument/XDocument。 - Parquet:用 Parquet.Net(数据科学场景)。
- Protocol Buffers:protobuf-net(高性能二进制)。
十二、错误处理与最佳实践
- 权限:UnauthorizedAccessException、IOException。捕获处理。
- 路径:用 Path.Combine,不要字符串拼接。
- 释放:Stream/Reader/Writer 实现 IDisposable,用 using。
- 编码:明确指定 Encoding.UTF8,避免默认编码跨平台问题。
- 大文件:流式 + 异步,不要 ReadAllBytes。
- 原子写:写到临时文件,完成后改名(File.Replace)。
- 并发:FileStream 加 FileShare 控制,或用锁。
- 事务:Windows 支持事务性文件操作(分布式,较少用)。
十三、综合示例:简易配置系统
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# 入门系列的核心内容就讲完了!接下来你应该:
- 挑一个方向深入:Web 后端(ASP.NET Core) / Unity 游戏 / 桌面应用。
- 读官方文档:learn.microsoft.com/dotnet,质量极高。
- 做项目:实践比看书有效 10 倍。
← 上一篇 C# 异常处理
返回 C# 教程目录