C#使用StreamWriter合并CSV二次执行时报文件被进程占用错误
我使用C#尝试将多个CSV文件合并为单个CSV文件。
该功能首次运行可以正常生效,但第二次执行时会出现如下报错:
"The process cannot access the file "c:\test\CSV_merged.csv because it is being used by another process"
通过ProcessExplorer排查得知,该文件正被我开发的应用程序(CSV_Merge.exe)占用。
但奇怪的是,我可以通过Windows资源管理器删除"c:\test\CSV_merged.csv"文件(即便它处于锁定状态?),删除后第二次执行合并功能即可正常运行。
我已经尝试使用using (StreamWriter....)语句块,还额外调用了Flush()、Close()、Dispose()方法——按理说使用using (StreamWriter....)语句时本不需要手动调用这些释放方法?
以下是我的实现代码:
private void btnMergeCSVFiles_Click(object sender, EventArgs e) { string sourceFolder = lblSelectedCSVFolder.Text; string destinationFile = lblSelectedCSVFolder.Text + "\\CSV_Files_Combined.csv"; // Specify wildcard search to match CSV files that will be combined string[] CSVFiles = Directory.GetFiles(sourceFolder, "*.csv"); using (StreamWriter fileDest = new StreamWriter(destinationFile)) { //StreamWriter fileDest = new StreamWriter (destinationFile, true); int i; for (i = 0; i < CSVFiles.Length; i++) { string file = CSVFiles[i]; string[] lines = File.ReadAllLines(file); if (chkBoxCSVHasHeaderline.Checked && i > 0) { lines = lines.Skip(1).ToArray(); // Skip header row for all but first file } foreach (string line in lines) { fileDest.WriteLine(line); } } //fileDest.Flush(); //fileDest.Close(); //fileDest.Dispose(); } //fileDest.Flush(); //fileDest.Close(); //fileDest.Dispose(); }

你的代码出现文件占用报错和资源释放没有关系,using (StreamWriter...) 块会在执行结束(包括抛出异常时)自动正确释放文件句柄,你手动加的Flush()/Close()/Dispose()都是多余的,完全不需要额外调用。
触发报错的根本原因是输出文件存放路径和待合并CSV在同一个文件夹,且你枚举了该文件夹下所有.csv文件作为合并源:
- 第一次运行时,合并后的输出文件还不存在,
Directory.GetFiles枚举到的只有你需要合并的源CSV文件,流程正常执行,跑完后输出文件被生成在源文件夹下。 - 第二次运行时,
Directory.GetFiles会把上次生成的合并输出文件也当成待合并的源文件加入处理列表。你首先通过StreamWriter以写权限打开了输出文件并持有句柄,当循环处理到输出文件本身时,File.ReadAllLines尝试以读权限打开该文件,由于Windows文件共享权限的双向校验规则,这一操作会和已存在的写句柄冲突,直接抛出文件被占用的错误。
你能在资源管理器里删除被锁定的文件,是因为Windows系统支持对打开的文件标记删除状态,等所有持有该文件的句柄关闭后就会真正执行删除;删除后第二次运行时,GetFiles 枚举不到输出文件,就和第一次运行的场景一致,自然可以正常执行。
你在Process Explorer中看到文件被自己的程序占用,是因为报错发生时程序还处于using块的执行流程中,StreamWriter持有的文件句柄还没到释放时机,属于正常现象,不是资源泄漏。
两种方案选其一即可:
- 将合并后的输出文件存放到源CSV文件夹之外的其他目录,从根源上避免输出文件被枚举为待合并源。
- 如果必须把输出文件放在源文件夹下,在获取CSV文件列表后,显式将输出文件从待处理列表中过滤掉,参考代码:
string[] CSVFiles = Directory.GetFiles(sourceFolder, "*.csv") .Where(filePath => !filePath.Equals(destinationFile, StringComparison.OrdinalIgnoreCase)) .ToArray();
- 路径拼接建议使用
Path.Combine()方法,不要手动拼接\\,避免路径末尾自带斜杠、跨系统兼容等问题,比如string destinationFile = Path.Combine(lblSelectedCSVFolder.Text, "CSV_Files_Combined.csv"); - 如果需要合并体积较大的CSV文件,不建议用
File.ReadAllLines把整个文件一次性读入内存,可以改用流逐行读写,大幅降低内存占用,优化后的参考代码如下:private void btnMergeCSVFiles_Click(object sender, EventArgs e) { string sourceFolder = lblSelectedCSVFolder.Text; string destinationFile = Path.Combine(sourceFolder, "CSV_Files_Combined.csv"); // 枚举所有csv文件,排除输出文件自身 string[] CSVFiles = Directory.GetFiles(sourceFolder, "*.csv") .Where(f => !f.Equals(destinationFile, StringComparison.OrdinalIgnoreCase)) .ToArray(); using (StreamWriter fileDest = new StreamWriter(destinationFile)) { for (int i = 0; i < CSVFiles.Length; i++) { bool skipHeader = chkBoxCSVHasHeaderline.Checked && i > 0; using (StreamReader reader = new StreamReader(CSVFiles[i])) { string line; if (skipHeader) { // 跳过非首个文件的表头行 reader.ReadLine(); } while ((line = reader.ReadLine()) != null) { fileDest.WriteLine(line); } } } } }
内容的提问来源于stack exchange,提问作者Spacewalker

