C# 文件IO复盘
一、 划分使用边界:小文件靠 File,大文件靠 Stream
1. 小文件时代的“全家桶”(File 静态类)
如果文件只有几十 KB 或几 MB(比如配置文件、简单的配置日志),直接用官方封装好的 File 静态方法最香。
- 精确到行:用
File.ReadAllLines(),能把文本按行切分成字符串数组返回,非常适合解析行规则固定的配置。 - 整体吞吐:用
File.WriteAllText()或File.ReadAllText(),直接将整个文本作为一整个string返回。 - 二进制流:用
File.ReadAllBytes()读取多媒体文件(图片、小音频)。
2. 必须切换到 Stream(流)的危险信号
一旦文件规模达到 几十MB甚至上百MB,或者需要高频不间断地写入数据,就必须全面抛弃 File。
底层分析:File.ReadAllText 的本质是把文件数据一次性全部加载到堆内存中。如果要读一个 1G 的日志,它就会在内存里瞬间开辟 1G 的空间,直接引发频繁的 GC,甚至抛出 OutOfMemoryException 挂掉程序。
二、 核心底层拆解:FileStream 与字符流的配合
流(Stream)的本质就像一根管道,数据被拆分成一个个字节(byte)在里面流动,我们每次只处理其中一小段,这就是它省内存的核心原因。
1. FileStream 万能字节流的循环读写
FileStream 被称为万能流,因为它只认字节,能读写包括视频、图片、文本在内的任何文件。在复制一个几百兆的大视频文件时,在网上搜到职业级的写法是开辟一个小的缓冲区,通过 while 循环蚂蚁搬家式地读写:
1 | public static void CopyBigFile(string src, string desc) |
2. 文本处理的高效管道:StreamReader / StreamWriter
FileStream 控底层字节很强,但如果我们要处理的是文本,数字节去拼字符串就太反人类了。于是 .NET 提供了专门针对字符的工具——StreamReader 和 StreamWriter。
1 | // StreamWriter 写入:第二个参数传 true 表示追加文本,默认 false 表示覆盖 |
三、 实际开发中的几个典型大坑
1. 物理文件被强行锁死(忘记关闭或释放流)
文件操作最经典的 Bug 就是:“程序第一次跑正常,第二次跑就报错:文件正在被另一个进程使用。”
- 原因分析:实例化
File.Create()或者new FileStream()时,操作系统会分配一个文件句柄。如果用完没有显式执行Close()或Dispose(),这根管道就会一直占着物理文件,别人谁也动不了。 - 规范写法:任何流操作,必须强制写在
using块里。using在底层会被编译为try-finally,确保程序无论正常结束还是中途崩溃,文件句柄都能百分之百被释放。
2. 目录不存在引发的直接闪退
在使用 File.WriteAllText(@"D:\\App\\Log\\test.txt", "content") 时,如果 D:\\App\\Log 这个三级文件夹在硬盘上本来不存在,程序不会帮你创建,而是直接抛出 DirectoryNotFoundException 导致系统崩溃。
- 避坑准则:只要是写操作,必须先用静态类过一遍防火墙:
1 | string dirPath = @"D:\App\Log"; |
3. 跨平台路径分隔符硬编码(Windows 与 Linux/macOS 的冲突)
在拼接路径时,很多习惯写死斜杠:string path = baseDir + @"\\Log\\file.txt";。这在 Windows 下没问题,但如果项目发布到 Linux 容器(如 Docker)里运行,Linux 根本不认\ 反斜杠,程序直接报找不到路径。
- 职业级写法:彻底抛弃手动拼接字符串,一律使用
Path.Combine。它会根据当前的操作系统(Windows 还是 Linux),动态自动补充正确的目录分隔符。
1 | // 跨平台安全拼接 |
在做物理文件读写前,第一步永远是确定文件的绝对路径。在 C# 中,千万不要写死盘符(如 C:\Users\…),一律采用相对程序根目录的动态拼接。
- 获取程序根目录的标准写法:
1
2// 推荐:获取当前执行程序所在的 bin/Debug 或 bin/Release 根目录(最稳妥,跨平台通用)
string basePath = AppDomain.CurrentDomain.BaseDirectory;
四、 核心类 1:Path —— 路径处理的大师(静态类)
什么时候用:只要涉及拼接、解析文件路径字符串,必须无条件用它。 它纯粹是在内存里进行字符串运算,完全不影响物理磁盘文件。
最常用的 5 个核心方法:
Path.Combine(params string[] paths):(最高频) 动态拼接多段路径。会自动根据 Windows(反斜杠\)或 Linux(正斜杠/)系统补齐正确的目录分隔符。Path.GetFileName(string path):从一长串完整路径里,直接切出带后缀的文件名(如app.json)。Path.GetFileNameWithoutExtension(string path):只拿文件名,不要后缀(如输入test.txt,返回test)。Path.GetExtension(string path):提取文件的扩展名(包含点,如.txt、.mp4),常用于判断文件类型。Path.GetDirectoryName(string path):提取文件所属的上一级文件夹的完整路径。
五、 核心类 2:File —— 小文件的快捷全家桶(静态类)
- 什么时候用:文件体积小(通常在几 MB 以内),比如读写简单的配置文件、生成临时小文本、一次性吞吐。
- 最常用的 6 个核心方法:
File.Exists(string path):(必用) 检查物理文件在硬盘上是否存在,做防错的第一道关卡。File.WriteAllText(path, content, encoding):一次性写入一整段字符串。每次写入会直接覆盖原文件。建议显式指定编码(如Encoding.UTF8)。File.ReadAllText(path, encoding):一次性读取文件里的全部文本,直接返回一个string。File.ReadAllLines(path):按行读取文本文件,并返回一个字符串数组(string[]),非常适合解析行规则固定的配置。File.AppendAllText(path, content):在文件末尾追加新文本,非常适合写简单的日常操作日志。File.Delete(path):删除指定路径的文件。如果文件不存在,该方法不会报错。
- 🚨 核心暗坑: 用
File.WriteAllText写入文件时,如果路径里的多级文件夹(目录)在磁盘上不存在,程序会直接抛出DirectoryNotFoundException异常闪退。必须配合下面的Directory先把文件夹建好。
六、 核心类 3:Directory —— 文件夹的安全管家(静态类)
- 什么时候用:创建、删除、移动、遍历磁盘上的文件夹(目录)。
- 最常用的 5 个核心方法:
Directory.Exists(string path):检查文件夹是否存在。Directory.CreateDirectory(string path):(非常智能) 如果要建D:\\App\\Log\\2026目录,哪怕中间的App和Log都不存在,它也会一口气把多级父目录全部自动建好。Directory.Delete(path, recursive):删除文件夹。第二个参数如果传true,会进行递归删除,即连带文件夹里的所有子文件和子目录一起拔掉(如果传false且文件夹非空,会报错)。Directory.GetFiles(path, searchPattern):获取当前文件夹下的所有文件名(支持通配符过滤,如*.txt)。Directory.GetDirectories(path):获取当前文件夹下的所有子文件夹的完整路径。
- 避坑级标准写操作套路:
七、 核心类 4:FileStream —— 大文件与高频读写的万能管道(实例类)
- 什么时候用:文件很大(几十 MB 以上,如音视频、历史大日志),或者需要高频不间断地往文件里写数据(比如上位机高频循环记录传感器数据)。它是字节流,不关心文件格式,万能通用。
- 最常用的 2 种组合应用:
方案 A:原生字节流循环搬运(适合大文件、多媒体拷贝)
1 | // 必须用 using 包裹,不管中途是否发生报错,退出 using 块时流会自动关闭并释放锁 |
方案 B:FileStream 配合包装字符流(适合大文本、日志按行吞吐)
如果面对的是上百兆的文本日志,数字节太痛苦,可以用 StreamReader/Writer 套在 FileStream 外面使用。
1 | string logFile = "big_history.log"; |
个人复盘总结(快速记忆口诀)
- 路不熟,找
Path:只要牵扯到字符串路径的裁剪拼接,不要用 + 去凑,一律交给Path.Combine跨平台。 - 体积小,找
File:小文件、配置文件、几行小日志,直接用File的静态全家桶一步到位,省时省力。 - 有危险,找
Exists:动手动笔写文件前,先用Directory.Exists扫一遍文件夹,没有就Create垫后,彻底干掉路径缺失的闪退Bug。 - 体积大,找
Stream:大文件、视频、或者要在while循环里高频写数据的,必须用FileStream开辟缓冲区单向流通,且永远不要忘记加using释放句柄。






