C# System.IO文件处理全攻略:从基础API到高性能实战

发布时间:2026/8/13 22:31:50

C# System.IO文件处理全攻略:从基础API到高性能实战
1. 项目概述为什么System.IO是C#开发者的必修课如果你用C#写过任何需要和文件打交道的程序无论是读取一个配置文件、保存用户上传的图片还是处理海量的日志数据那你一定绕不开System.IO这个命名空间。它就像是C#世界里通往文件系统的“万能钥匙”但很多开发者尤其是刚入门的往往只停留在File.ReadAllText和File.WriteAllText的层面遇到复杂一点的场景就束手无策或者写出性能低下、容易出错的代码。我见过不少项目文件操作部分的代码写得像“打补丁”这里一个try-catch那里一个字符串拼接路径不仅难以维护还潜藏着各种安全风险比如路径遍历漏洞和性能瓶颈。System.IO提供的是一整套强大、精细的工具集理解并正确使用它能让你从“能用”进阶到“高效、稳健”。这篇文章不是API文档的简单罗列而是结合我十多年踩过的坑、优化过的代码为你梳理出一套从基础到进阶再到实战避坑的System.IO文件处理心法。无论你是正在开发一个WPF上位机软件需要处理Excel报表还是用ASP.NET Core构建Web API处理上传文件或是写一个后台服务监控日志这里的内容都能让你直接“抄作业”并理解背后的“所以然”。2. 基石篇路径、基础读写与异常处理在动手读写文件之前我们必须打好地基。很多诡异的问题根源都出在地基没打牢。2.1 路径处理告别字符串拼接使用Path类直接拼接路径字符串是万恶之源。不同操作系统路径分隔符不同Windows是\Linux/macOS是/硬编码会导致跨平台兼容性问题。更危险的是如果用户输入的文件名包含..简单的拼接可能导致路径遍历攻击。// 错误示范脆弱的字符串拼接 string badPath folder “\\” “..\\” “敏感文件.txt”; // 危险 // 正确做法使用System.IO.Path类 string directory “C:\MyApp\Data”; string fileName “config.json”; string safePath Path.Combine(directory, fileName); // 输出C:\MyApp\Data\config.json // Path类其他常用方法 string fullPath “C:\MyApp\Data\SubFolder\file.txt”; string dirName Path.GetDirectoryName(fullPath); // C:\MyApp\Data\SubFolder string ext Path.GetExtension(fullPath); // .txt string fileNameWithoutExt Path.GetFileNameWithoutExtension(fullPath); // file string tempFile Path.GetTempFileName(); // 获取一个唯一的临时文件路径Path.Combine会自动处理分隔符并且更安全。记住一个原则所有文件路径的构建和解析都交给Path类。2.2 文件存在性检查一个常见的性能陷阱“检查文件是否存在”这个简单的操作却藏着坑。最常见的反模式是先检查存在再执行操作。// 反模式存在竞态条件 if (File.Exists(filePath)) { // 在这毫秒之间文件可能被其他进程删除或移动 var content File.ReadAllText(filePath); // 可能抛出FileNotFoundException! }在多线程或分布式环境下File.Exists返回true和你真正打开文件之间文件状态可能已经改变。正确的模式是尝试执行操作并处理可能发生的异常。File.Exists的最佳用途是用于非关键性的逻辑判断比如决定是否要显示一个“创建新文件”的按钮而不是作为文件操作的前提守卫。2.3 同步读写简单场景的利器与局限File类提供了一系列静态方法用于同步读写它们适用于文件小、操作不频繁的场景代码非常简洁。读取// 读取所有文本适用于小文本文件如配置 string allText File.ReadAllText(“config.json”, Encoding.UTF8); // 读取所有行返回字符串数组适用于日志、CSV等行式文件 string[] allLines File.ReadAllLines(“log.txt”); // 读取所有字节适用于图片、音频等二进制文件 byte[] allBytes File.ReadAllBytes(“image.png”);写入// 写入所有文本覆盖已存在文件 File.WriteAllText(“output.txt”, “Hello World”, Encoding.UTF8); // 追加文本到文件末尾 File.AppendAllText(“log.txt”, $“{DateTime.Now}: New event\n”); // 写入所有行 File.WriteAllLines(“data.csv”, new string[] { “Name,Age”, “Alice,30” }); // 写入字节数组 File.WriteAllBytes(“backup.dat”, dataBytes);局限性这些方法是一次性将整个文件内容加载到内存中。对于一个大文件比如几百MB的日志File.ReadAllText会瞬间消耗大量内存甚至导致程序崩溃。对于大文件我们必须使用流Stream。2.4 异常处理你必须捕获的几种类型文件操作是I/O操作充满了不确定性磁盘满、文件被占用、权限不足、路径无效。健壮的程序必须处理异常。try { string content File.ReadAllText(somePath); // 处理内容... } catch (DirectoryNotFoundException ex) { // 路径中的目录不存在 Console.WriteLine($“目录不存在: {ex.Message}”); // 处理逻辑创建目录或提示用户 } catch (FileNotFoundException ex) { // 文件不存在 Console.WriteLine($“未找到文件: {ex.FileName}”); // 处理逻辑使用默认值或提示用户 } catch (IOException ex) // 这是一个更通用的I/O异常基类 { // 文件正在被使用、磁盘空间不足、网络驱动器断开等 Console.WriteLine($“I/O错误: {ex.Message}”); // 检查ex.HResult或ex.InnerException获取更多信息 // 可以考虑重试逻辑 } catch (UnauthorizedAccessException ex) { // 没有访问文件的权限 Console.WriteLine($“权限拒绝: {ex.Message}”); } catch (ArgumentException ex) // 来自Path.Combine等方法的参数错误 { // 路径包含非法字符等 Console.WriteLine($“参数错误: {ex.Message}”); } finally { // 对于File.ReadAllText这类方法不需要清理资源。 // 但对于后面要讲的StreamReader/FileStreamfinally块是释放资源的关键。 }核心经验不要简单地用catch (Exception ex)一网打尽。针对不同的异常类型进行精细化处理能让你的程序更友好、更易于调试。例如文件不存在和文件被占用给用户的提示应该是完全不同的。3. 核心篇深入流Stream与读写器Reader/Writer当处理大文件、需要增量读写或进行网络传输时流Stream是唯一的选择。Stream是字节序列的抽象FileStream是操作文件最基本的流。3.1 理解FileStream字节级的精细控制FileStream让你能控制文件打开的每一个细节模式、访问权限、共享选项。// 使用FileStream构造函数 using (FileStream fs new FileStream(filePath, FileMode.OpenOrCreate, // 打开或创建 FileAccess.ReadWrite, // 读写权限 FileShare.Read)) // 允许其他进程读 { // 定位到文件末尾 fs.Seek(0, SeekOrigin.End); // 准备要写入的数据 byte[] data Encoding.UTF8.GetBytes(“Appended line\n”); // 写入字节数组 fs.Write(data, 0, data.Length); // 记得刷新缓冲区确保数据写入磁盘 fs.Flush(); } // using语句结束时会自动调用fs.Dispose()关闭文件流。关键参数解析FileMode:CreateNew: 创建新文件如果存在则抛出异常。Create: 创建新文件如果存在则覆盖。Open: 打开现有文件不存在则异常。OpenOrCreate: 打开文件不存在则创建。Append: 打开文件并定位到末尾用于追加。配合FileAccess.Write使用。FileAccess:Read,Write,ReadWrite。FileShare: 控制其他FileStream如何访问同一文件。None独占、Read允许读、Write允许写、ReadWrite。在处理可能被多个进程访问的文件如日志时正确设置FileShare至关重要可以避免IOException。重要经验务必使用using语句包裹FileStream。这能确保即使在发生异常的情况下文件句柄也能被正确释放避免资源泄漏和文件被长期锁定的问题。这是C#文件操作的第一铁律。3.2 文本读写器StreamReader与StreamWriter直接操作FileStream的字节很繁琐。对于文本文件我们使用StreamReader和StreamWriter它们提供了基于字符而非字节的、带缓冲的高层接口。// 写入文本文件 using (StreamWriter writer new StreamWriter(“data.txt”, false, Encoding.UTF8)) // false表示覆盖true表示追加 { writer.WriteLine(“Name, Age, City”); // 写入一行并换行 writer.Write(“Alice, ”); // 写入不换行 writer.WriteLine(30); writer.WriteLine(“Bob, 25, London”); // 无需手动FlushDispose时会自动Flush。但在长时间循环中写入可间歇性调用writer.Flush()。 } // 读取文本文件高效逐行读取大文件 using (StreamReader reader new StreamReader(“large_log.txt”, Encoding.UTF8)) { string line; while ((line reader.ReadLine()) ! null) // 一次只读一行到内存 { ProcessLine(line); // 处理每一行 } } // 读取整个文件对于大文件仍比File.ReadAllText好因为可控制编码和缓冲 using (StreamReader reader new StreamReader(“file.txt”)) { string allText reader.ReadToEnd(); // 小心内存 }编码Encoding的重要性这是中文等非ASCII文本乱码的罪魁祸首。默认情况下StreamWriter使用UTF-8 without BOM在.NET Core/.NET 5中而StreamReader会尝试自动检测编码。为了最大程度的兼容性和确定性我强烈建议在构造函数中显式指定编码尤其是Encoding.UTF8。如果你需要处理Windows记事本保存的带BOM的UTF-8文件或者GB2312的遗留文件显式指定编码是唯一可靠的方式。3.3 二进制读写器BinaryReader与BinaryWriter处理非文本文件如图像、音频、自定义数据结构时需要使用BinaryReader和BinaryWriter。// 假设我们有一个简单的数据结构要保存 struct PlayerData { public int Id; public float Health; public string Name; } // 写入二进制文件 using (FileStream fs new FileStream(“savegame.dat”, FileMode.Create)) using (BinaryWriter writer new BinaryWriter(fs)) { PlayerData data new PlayerData { Id 1, Health 95.5f, Name “Hero” }; writer.Write(data.Id); writer.Write(data.Health); writer.Write(data.Name); // 会自动写入字符串长度前缀和字符 } // 读取二进制文件 using (FileStream fs new FileStream(“savegame.dat”, FileMode.Open)) using (BinaryReader reader new BinaryReader(fs)) { PlayerData data new PlayerData(); data.Id reader.ReadInt32(); data.Health reader.ReadSingle(); data.Name reader.ReadString(); // 根据长度前缀读取字符串 Console.WriteLine($“Loaded: {data.Name}, HP: {data.Health}”); }注意事项读写顺序必须严格一致。BinaryWriter.Write(string)会先写入该字符串的长度7位编码的整数再写入字符。BinaryReader.ReadString()会先读取长度再读取对应数量的字符。这种格式是.NET特有的其他语言编写的二进制文件可能不兼容。4. 进阶篇文件系统操作、监控与异步编程掌握了单个文件的读写我们来看看如何操作目录、批量处理文件以及如何让文件操作不阻塞主线程。4.1 目录操作Directory类与文件枚举Directory类用于创建、移动、枚举目录。// 创建目录如果已存在也不会报错 Directory.CreateDirectory(“C:\MyApp\Logs\2023-10”); // 检查目录是否存在 if (Directory.Exists(logDir)) { // 获取目录下所有文件返回完整路径 string[] allFiles Directory.GetFiles(logDir); // 默认不包含子目录 string[] txtFiles Directory.GetFiles(logDir, “*.txt”); // 搜索模式 string[] allFilesRecursive Directory.GetFiles(logDir, “*.*”, SearchOption.AllDirectories); // 包含子目录 // 获取目录下所有子目录 string[] subDirs Directory.GetDirectories(logDir); // 移动目录 Directory.Move(sourceDir, targetDir); // 目标目录不能已存在 } // 删除目录 Directory.Delete(emptyDir); // 删除空目录 Directory.Delete(dirToDelete, true); // 第二个参数为true时递归删除目录及其所有内容危险操作文件枚举的陷阱Directory.GetFiles和Directory.GetDirectories在遇到无权限访问的目录时会抛出UnauthorizedAccessException。如果你需要遍历一个可能包含受限目录的路径如整个C盘需要使用Directory.EnumerateFiles和Directory.EnumerateDirectories。这两个方法返回IEnumerablestring是延迟执行的只有在遍历到那个无权限的目录时才会抛出异常你可以用try-catch包裹遍历逻辑来处理它而不是一开始就整个失败。4.2 文件与目录信息类FileInfo与DirectoryInfoFileInfo和DirectoryInfo是面向对象的文件/目录信息表示它们缓存了文件系统的信息在需要多次访问同一文件属性时比静态的File/Directory方法更高效。FileInfo fileInfo new FileInfo(“largefile.iso”); if (fileInfo.Exists) { Console.WriteLine($“文件名: {fileInfo.Name}”); Console.WriteLine($“完整路径: {fileInfo.FullName}”); Console.WriteLine($“大小: {fileInfo.Length} bytes”); Console.WriteLine($“创建时间: {fileInfo.CreationTime}”); Console.WriteLine($“最后访问时间: {fileInfo.LastAccessTime}”); Console.WriteLine($“最后写入时间: {fileInfo.LastWriteTime}”); Console.WriteLine($“扩展名: {fileInfo.Extension}”); Console.WriteLine($“目录: {fileInfo.DirectoryName}”); // 文件操作 fileInfo.CopyTo(“backup.iso”, true); // 覆盖复制 // fileInfo.MoveTo(“newlocation.iso”); // fileInfo.Delete(); } DirectoryInfo dirInfo new DirectoryInfo(“C:\MyApp”); var recentFiles dirInfo.EnumerateFiles(“*.log”, SearchOption.AllDirectories) .Where(f f.LastWriteTime DateTime.Now.AddDays(-7)) .OrderByDescending(f f.LastWriteTime); // 这是一个高效的查询延迟执行不会一次性加载所有文件信息到内存。选择建议如果只做一次性的操作如检查是否存在、读一次内容用静态方法File.Exists/File.ReadAllText更简洁。如果需要多次访问文件的属性如在循环中检查多个文件的大小、时间或者要进行一系列关联操作如获取文件信息后再复制使用FileInfo/DirectoryInfo更合适。4.3 异步文件操作释放UI线程提升响应性在GUI应用如WPF、WinForms或Web服务器如ASP.NET Core中同步的文件I/O操作会阻塞当前线程导致界面卡顿或请求响应变慢。异步APIasync/await可以将耗时的I/O操作交给系统底层释放当前线程去处理其他任务。// 异步读取文本文件 public async Taskstring ReadConfigAsync(string filePath) { try { using (StreamReader reader new StreamReader(filePath)) { return await reader.ReadToEndAsync(); // 异步读取 } } catch (FileNotFoundException) { return await Task.FromResult(“{}”); // 返回默认配置 } } // 在WPF按钮事件中调用不会冻结界面 private async void LoadButton_Click(object sender, RoutedEventArgs e) { LoadingIndicator.Visibility Visibility.Visible; string config await ReadConfigAsync(“appsettings.json”); // 更新UI LoadingIndicator.Visibility Visibility.Collapsed; ParseConfig(config); } // 异步写入例如保存用户数据 public async Task SaveUserDataAsync(UserData data, string filePath) { string json JsonSerializer.Serialize(data); // File类也提供了便捷的异步方法 await File.WriteAllTextAsync(filePath, json, Encoding.UTF8); } // 异步复制大文件使用Stream public static async Task CopyFileAsync(string sourcePath, string destinationPath, IProgresslong progress null) { using (FileStream sourceStream new FileStream(sourcePath, FileMode.Open, FileAccess.Read, FileShare.Read, bufferSize: 4096, useAsync: true)) using (FileStream destStream new FileStream(destinationPath, FileMode.CreateNew, FileAccess.Write, FileShare.None, bufferSize: 4096, useAsync: true)) { byte[] buffer new byte[81920]; // 80KB缓冲区 int bytesRead; long totalRead 0; while ((bytesRead await sourceStream.ReadAsync(buffer, 0, buffer.Length)) 0) { await destStream.WriteAsync(buffer, 0, bytesRead); totalRead bytesRead; progress?.Report(totalRead); // 报告进度 } } }关键点useAsync: true参数在创建FileStream时指定此参数能获得更好的异步性能。缓冲区大小ReadAsync/WriteAsync配合缓冲区使用避免频繁的小数据块I/O。通常64KB到1MB是不错的选择。进度报告通过IProgressT接口可以方便地向UI报告复制进度。错误处理异步方法同样需要try-catch并且异常会在await的位置抛出。4.4 文件系统监控FileSystemWatcher当你需要实时响应某个目录下文件的变化时如监控日志文件夹、实现配置文件热重载FileSystemWatcher是你的利器。public void StartWatchingLogs(string logDirectory) { FileSystemWatcher watcher new FileSystemWatcher(); watcher.Path logDirectory; watcher.Filter “*.log”; // 只监控.log文件 watcher.NotifyFilter NotifyFilters.LastWrite | NotifyFilters.FileName; // 监控写入和文件名变化 // 订阅事件 watcher.Changed OnLogFileChanged; // 文件被修改 watcher.Created OnLogFileCreated; // 新文件被创建 watcher.Deleted OnLogFileDeleted; // 文件被删除 watcher.Renamed OnLogFileRenamed; // 文件被重命名 watcher.EnableRaisingEvents true; // 开始监控 // 注意watcher对象需要在整个监控周期内保持存活通常作为类的字段。 } private void OnLogFileChanged(object source, FileSystemEventArgs e) { // 重要Changed事件可能会被触发多次例如文本编辑器保存时。 // 需要防抖处理Debounce。 Console.WriteLine($“文件被修改: {e.FullPath}, 变更类型: {e.ChangeType}”); // 通常在这里触发一个延迟处理逻辑比如500ms后再去读取文件内容。 } private void OnLogFileRenamed(object source, RenamedEventArgs e) { Console.WriteLine($“文件重命名: 从 {e.OldName} 到 {e.Name}”); }避坑指南事件重复触发Changed事件可能因程序多次写文件而快速连续触发。解决方案是使用防抖Debounce或节流Throttle机制例如用一个定时器在事件触发后等待一小段时间如500ms如果期间没有新事件再执行实际处理逻辑。跨线程UI更新FileSystemWatcher的事件在后台线程触发。如果你需要在WPF或WinForms中更新UI必须通过Dispatcher.Invoke或Control.Invoke切换到UI线程。缓冲区溢出FileSystemWatcher有一个内部缓冲区。如果短时间内文件系统变化极快可能导致缓冲区溢出触发Error事件。可以通过设置watcher.InternalBufferSize属性默认8KB来增加缓冲区大小。资源释放监控结束后记得设置watcher.EnableRaisingEvents false并取消事件订阅或直接Dispose掉watcher对象。5. 实战与避坑性能、并发与特殊场景理论说再多不如实战踩坑来得深刻。下面分享几个高频场景下的具体做法和容易忽略的细节。5.1 大文件处理内存友好的读写策略处理GB级别的大文件核心思路是分块读取流式处理永远不要试图一次性读入内存。场景统计一个超大文本文件的行数。public long CountLinesEfficiently(string filePath) { long lineCount 0; const int BufferSize 65536; // 64KB缓冲区 byte[] buffer new byte[BufferSize]; // 使用FileStream直接读取字节比StreamReader更底层内存开销更小 using (var fs new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read, BufferSize, FileOptions.SequentialScan)) // FileOptions.SequentialScan提示系统进行顺序读取优化 { int bytesRead; // 上一次缓冲区末尾可能残留的半行 byte[] leftover null; while ((bytesRead fs.Read(buffer, 0, buffer.Length)) 0) { int start 0; // 如果上次有残留将其与本次缓冲区开头合并处理 if (leftover ! null) { // 简单处理这里为了演示假设残留很小。实际生产代码需要更严谨的合并逻辑。 // 更健壮的做法是使用一个可增长的MemoryStream或Listbyte来累积。 start ScanBufferForNewlines(leftover.Concat(buffer.Take(bytesRead)).ToArray(), ref lineCount); leftover null; } else { start ScanBufferForNewlines(buffer, bytesRead, ref lineCount, out leftover); } } // 处理最后可能残留的数据如果文件末尾没有换行符 if (leftover ! null leftover.Length 0) { lineCount; // 最后一行 } } return lineCount; } // 注意ScanBufferForNewlines是一个需要你实现的辅助方法用于在字节数组中高效地查找换行符(‘\n’)。 // 更简单的方案使用StreamReader.ReadLine但设置大缓冲区 public long CountLinesWithReader(string filePath) { long count 0; using (var reader new StreamReader(filePath, Encoding.UTF8, true, 65536)) // 设置64KB缓冲区 { while (reader.ReadLine() ! null) { count; } } return count; }对于非文本的二进制大文件如视频分割模式类似在循环中Read字节块处理然后Write到目标流。5.2 文件并发访问与锁当多个线程或进程试图同时读写同一个文件时就需要锁机制来防止数据损坏。使用FileShare控制共享级别如前所述在打开FileStream时指定FileShare.None可以获得独占锁阻止其他所有访问。FileShare.Read允许其他进程读但不能写。这是最基础的并发控制。使用FileStream.Lock/Unlock方法Windows特有可以锁定文件的某个区域字节范围其他进程可以访问其他区域。适用于超大文件中修改特定部分的场景粒度更细。使用外部锁机制对于复杂的并发逻辑文件锁可能不够用。常见的做法是使用一个单独的“锁文件”lock file。进程在操作目标文件前先尝试创建一个唯一的锁文件例如使用FileMode.CreateNew因为它是原子的如果创建成功表示获取了锁操作完成后删除锁文件。如果创建失败文件已存在则等待或重试。这本质上是利用文件系统的原子操作实现的一个互斥锁。public bool TryAcquireFileLock(string targetFilePath, TimeSpan timeout, out IDisposable lockObj) { string lockFilePath targetFilePath “.lock”; DateTime startTime DateTime.Now; while (DateTime.Now - startTime timeout) { try { // 尝试独占创建锁文件 var fs new FileStream(lockFilePath, FileMode.CreateNew, FileAccess.Write, FileShare.None, 1, FileOptions.DeleteOnClose); lockObj fs; // 返回FileStream对象Dispose时会关闭并删除文件 return true; } catch (IOException) // 文件已存在锁被占用 { Thread.Sleep(50); // 等待50毫秒后重试 } } lockObj null; return false; } // 使用示例 using (var fileLock TryAcquireFileLock(“data.db”, TimeSpan.FromSeconds(5), out var lockObj)) { if (lockObj ! null) { // 安全地操作 data.db // ... } else { Console.WriteLine(“获取文件锁超时”); } }5.3 临时文件与资源清理程序运行中经常需要创建临时文件。关键是要确保临时文件最终被删除避免磁盘空间泄漏。// 方法1使用Path.GetTempFileName() - 会创建一个0字节的临时文件 string tempFile Path.GetTempFileName(); // 例如C:\Users\...\AppData\Local\Temp\tmp1234.tmp try { File.WriteAllText(tempFile, “临时数据”); // 使用临时文件... } finally { File.Delete(tempFile); // 务必删除 } // 方法2使用Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString() “.tmp”) // 这样不会立即创建文件更灵活。 string tempPath Path.Combine(Path.GetTempPath(), Guid.NewGuid() “.tmp”); try { using (var fs new FileStream(tempPath, FileMode.CreateNew)) using (var writer new StreamWriter(fs)) { writer.Write(“临时数据”); } // 使用... } finally { if (File.Exists(tempPath)) { File.Delete(tempPath); } } // 方法3推荐对于需要自动清理的临时文件可以使用FileOptions.DeleteOnClose using (var fs new FileStream(tempPath, FileMode.Create, FileAccess.ReadWrite, FileShare.None, 4096, FileOptions.DeleteOnClose)) { // 写入数据... // 文件流关闭后操作系统会自动删除该文件。 }经验将临时文件的创建和删除逻辑封装在try-finally块或using语句中利用IDisposable模式可以最大程度保证资源被清理。对于长时间运行的服务可以考虑定期扫描临时目录清理过期文件。5.4 处理特殊文件符号链接、隐藏文件等File和Directory的静态方法默认会跟随符号链接Symbolic Link。有时你需要知道一个路径是否是链接或者想获取链接本身的信息。string path “C:\MyLink”; FileAttributes attr File.GetAttributes(path); if ((attr FileAttributes.ReparsePoint) FileAttributes.ReparsePoint) { Console.WriteLine($“{path} 是一个重解析点可能是符号链接或挂载点”); } // 要获取链接的真实目标需要使用平台调用(P/Invoke)调用Windows API(如GetFinalPathNameByHandle)或使用.NET Core 3.0的System.IO.FileSystem.LinkTarget包跨平台。枚举时排除系统/隐藏文件var files new DirectoryInfo(sourceDir) .EnumerateFiles(“*.*”, SearchOption.AllDirectories) .Where(f !f.Attributes.HasFlag(FileAttributes.Hidden) !f.Attributes.HasFlag(FileAttributes.System) !f.Attributes.HasFlag(FileAttributes.Temporary)) .Select(f f.FullName);5.5 跨平台兼容性注意事项如果你的代码需要运行在Linux或macOS上以下几点至关重要路径分隔符始终使用Path.Combine()不要手动拼接\或/。使用Path.DirectorySeparatorChar获取当前平台的正确分隔符。大小写敏感性Linux和macOS文件系统默认区分大小写。File.Exists(“MyFile.txt”)和File.Exists(“myfile.txt”)可能结果不同。比较文件名时考虑使用StringComparison.OrdinalIgnoreCase。文件权限在Unix-like系统上文件权限读、写、执行非常重要。尝试打开一个没有读权限的文件会抛出UnauthorizedAccessException。创建可执行文件后可能需要使用System.Diagnostics.Process.Start(“chmod”, “x file.sh”)来添加执行权限。特殊设备文件在/dev、/proc等目录下的不是普通文件尝试用普通文件API操作它们可能会产生意外行为或异常。6. 综合案例一个简单的日志记录器实现让我们把上面的知识串联起来实现一个线程安全、支持按日期滚动、异步写入的简单文件日志记录器。这个例子涵盖了路径处理、并发锁、异步操作、异常处理等多个要点。public class SimpleFileLogger : IDisposable { private readonly string _logDirectory; private readonly string _logFileBaseName; private readonly SemaphoreSlim _writeLock new SemaphoreSlim(1, 1); // 用于异步写入的锁 private StreamWriter _currentWriter; private DateTime _currentFileDate; public SimpleFileLogger(string logDirectory, string appName “MyApp”) { _logDirectory Path.GetFullPath(logDirectory); // 规范化路径 Directory.CreateDirectory(_logDirectory); // 确保目录存在 _logFileBaseName appName; _currentFileDate DateTime.Today; _currentWriter CreateWriterForDate(_currentFileDate); } private StreamWriter CreateWriterForDate(DateTime date) { string logFilePath Path.Combine(_logDirectory, $“{_logFileBaseName}_{date:yyyyMMdd}.log”); // 使用追加模式UTF8编码并设置自动刷新AutoFlush true确保日志不丢失 var fs new FileStream(logFilePath, FileMode.Append, FileAccess.Write, FileShare.Read, 4096, FileOptions.SequentialScan | FileOptions.Asynchronous); return new StreamWriter(fs, Encoding.UTF8) { AutoFlush true }; } public async Task LogAsync(string message, LogLevel level LogLevel.Info) { await _writeLock.WaitAsync().ConfigureAwait(false); // 获取写入锁 try { // 检查是否需要滚动到新的一天 if (DateTime.Today ! _currentFileDate) { _currentWriter?.Dispose(); _currentFileDate DateTime.Today; _currentWriter CreateWriterForDate(_currentFileDate); } string logEntry $“{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff} [{level}] {message}”; await _currentWriter.WriteLineAsync(logEntry).ConfigureAwait(false); } catch (Exception ex) { // 日志记录器自身记录失败这是一个棘手的问题。 // 可以尝试写入Windows事件日志、控制台或者至少不要抛出异常导致主程序崩溃。 Console.Error.WriteLine($“日志写入失败: {ex.Message}”); } finally { _writeLock.Release(); // 释放锁 } } public void Dispose() { _writeLock?.Dispose(); _currentWriter?.Dispose(); } } // 使用示例 public async Task DoWorkWithLogging() { using (var logger new SimpleFileLogger(“C:\AppLogs”, “MyService”)) { await logger.LogAsync(“服务启动”, LogLevel.Info); try { // ... 执行一些操作 await logger.LogAsync(“操作完成”, LogLevel.Info); } catch (Exception ex) { await logger.LogAsync($“操作失败: {ex}”, LogLevel.Error); } await logger.LogAsync(“服务停止”, LogLevel.Info); } }这个实现包含了几个关键设计按日期滚动每天创建一个新的日志文件避免单个文件过大。线程安全使用SemaphoreSlim确保多线程调用LogAsync时不会交叉写入导致日志混乱。异步写入使用WriteLineAsync避免阻塞调用线程。自动刷新设置AutoFlush true在每次写入后立即刷新缓冲区降低程序崩溃时丢失日志的风险但会轻微影响性能可根据需要调整。异常处理在日志记录器内部捕获I/O异常防止日志失败导致主程序崩溃。资源管理实现了IDisposable确保文件流被正确关闭。文件处理是C#开发中看似基础却暗藏玄机的一环。从正确的路径处理到稳健的异常捕获从高效的流式读写到复杂的并发控制每一个细节都影响着程序的可靠性、性能和安全性。我个人的体会是对待文件I/O要像对待数据库事务一样谨慎——总是假设操作可能失败总是考虑并发场景总是确保资源被释放。希望这篇长文能成为你手边的一份实用指南下次当你需要和文件系统打交道时能多一份从容少踩一个坑。

相关新闻

C语言字符串函数深度解析:从strcpy到strtok的实战指南与安全编程

C语言字符串函数深度解析:从strcpy到strtok的实战指南与安全编程

2026/8/13 22:21:50

1. 项目概述:为什么字符串函数是C语言的基石在C语言的世界里,字符串处理是绕不开的核心技能。无论你是刚入门的新手,还是已经写过上万行代码的老手,只要你的程序需要和人交互、处理文本文件、解析网络协议,就必然要和字…

全志A40i学习笔记():开发资料、启用串口

全志A40i学习笔记():开发资料、启用串口

2026/8/13 22:21:50

目录概述A40i User Manual V1.1A40i Datasheet V1.1开启UART61. [uart6]2. uart6_used3. uart6_port4. uart6_type5. TX/RX 配置6. <3><1><default><default> 的含义&#xff08;1&#xff09;第一个参数&#xff1a;复用功能&#xff08;2&#xff09…

常用调试接线备忘记录

常用调试接线备忘记录

2026/8/13 22:21:50

一、网线水晶头线序颜色T568B 标准对 RJ45 接口的线序颜色一致。二、USB to RS485 连网线连接信号线 (A 和 B)&#xff1a;在RS485通讯中&#xff0c;A 通常指数据正极&#xff08;Data&#xff09;&#xff0c;B 指数据负极&#xff08;Data-&#xff09;-。A 端子 (Data)&…

王虹、邓煜、张益唐、韦东奕四位数学家研究方向预测

王虹、邓煜、张益唐、韦东奕四位数学家研究方向预测

2026/8/13 23:31:52

结合四位数学家的核心成果、学术履历、已公开的研究动向背景&#xff0c;基于他们当前的研究基础和领域内的前沿共识&#xff0c;对2026年菲尔兹奖得主王虹、邓煜&#xff0c;以及同领域顶尖学者张益唐、韦东奕的未来研究方向做出如下预测&#xff1a; 一、王虹 未来研究方向预…

五河网站建设哪家好?避开这些坑,手把手教你选出最靠谱的设计团队

五河网站建设哪家好?避开这些坑,手把手教你选出最靠谱的设计团队

2026/8/13 23:31:52

在当下这个互联网时代,如果说生意不做了,那可能是在吹牛;但如果企业没有网站,那基本就是在跟未来说拜拜了。咱们五河县的老板们,现在的日子都不好过,房租人工样样涨,唯有客流是个谜。很多人第一反应是:“搞个抖音号得了,或者搞个微信公众号。”这话没错,但你要知道,…

三年开发者内功修炼:从API调用到系统思维与深度调试

三年开发者内功修炼:从API调用到系统思维与深度调试

2026/8/13 23:31:52

1. 项目概述&#xff1a;一份来自一线开发者的“内功”修炼手册 “三年开发”&#xff0c;这个时间点很微妙。它既不是刚入行的懵懂新人&#xff0c;也远未达到十年一剑的宗师境界。这个阶段的开发者&#xff0c;往往已经熟练掌握了业务开发的“套路”&#xff0c;能独立完成模…

MySQL数据库实战:从环境搭建到SQL优化与安全运维全解析

MySQL数据库实战:从环境搭建到SQL优化与安全运维全解析

2026/8/13 23:31:52

1. 项目概述&#xff1a;一份参考答案的价值与边界 最近在技术社区和教学平台上&#xff0c;看到不少朋友在讨论“头歌”这类在线编程或数据库练习平台的参考答案&#xff0c;尤其是围绕MySQL数据库的题目。作为一个在数据库领域摸爬滚打了十多年的老DBA&#xff0c;我对这个话…

API安全漏洞剖析:从授权检查缺失看业务逻辑风险防范

API安全漏洞剖析:从授权检查缺失看业务逻辑风险防范

2026/8/13 23:31:52

1. 从一次演示看API安全的核心&#xff1a;授权检查缺失有多危险 最近一个名为OpenClaw的工具演示&#xff0c;再次把API接口的安全问题推到了开发者面前。演示的场景很具体&#xff1a;攻击者利用澳洲某健身房预订网站的API接口&#xff0c;绕过了授权检查&#xff0c;实现了取…

从零基础到月薪过万:普通人如何通过网站建设与运营就业逆袭

从零基础到月薪过万:普通人如何通过网站建设与运营就业逆袭

2026/8/13 23:21:52

说实话,每次看到后台收到那些私信,问我“现在入行晚不晚”、“零基础能不能干”的时候,我心里总是五味杂陈。大家焦虑,因为大环境在变,互联网的风口好像一夜之间就转了方向。但如果你把目光放长远一点,把“网站建设与运营就业”这六个字嚼碎了看,你会发现,这不仅仅是一…

比较好的亚太EMBA,问了6位校友师资差别真的挺大

比较好的亚太EMBA,问了6位校友师资差别真的挺大

2026/8/13 11:01:28

比较好的亚太EMBA核心差异先看什么&#xff1f;对于希望兼顾工作与系统管理能力提升的亚太区高管而言&#xff0c;筛选匹配度高的EMBA项目时&#xff0c;师资配置是决定学习体验与实际收获的核心要素之一。我们结合3-4个公开信息透明、办学历史较长的亚太区主流EMBA项目特点&am…

备考3个月对比6份资料 海外游学的亚洲EMBA面试注意点

备考3个月对比6份资料 海外游学的亚洲EMBA面试注意点

2026/8/11 8:44:43

备考海外游学的亚洲EMBA面试&#xff0c;核心要围绕项目国际化设计逻辑、个人跨文化管理经验匹配度两个维度准备&#xff0c;避免把游学模块等同于普通旅游参访的认知偏差。不少备考者花3个月对比6份资料&#xff0c;却容易忽略面试官对“国际视野落地能力”的考察——比如香港…

比较好的国内EMBA,问了二十位校友聊透人脉价值

比较好的国内EMBA,问了二十位校友聊透人脉价值

2026/8/13 17:17:06

比较好的国内EMBA核心差异体现在哪些方面&#xff1f;比较好的国内EMBA的核心长期价值&#xff0c;很大程度上依托于校友网络的连接质量与资源生态的活跃度&#xff0c;这也是不少高管在择校时优先考量的因素。我们结合3-4个市场关注度较高的项目公开信息&#xff0c;从课程、师…

电商毛利率别再手动算了!2026年3种自动分析工具实测对比

电商毛利率别再手动算了!2026年3种自动分析工具实测对比

2026/8/13 0:00:21

一、开篇&#xff1a;毛利率——电商运营最该盯但最难盯的指标 电商运营中有一个指标&#xff0c;几乎所有老板都会问&#xff0c;但几乎所有运营都回答得不够确定——毛利率。不是"店铺毛利率"&#xff0c;而是"每条链接的毛利率""每个品类的毛利率…

15-SaaS系统灰度发布:滚动更新、金丝雀发布、不停机迭代

15-SaaS系统灰度发布:滚动更新、金丝雀发布、不停机迭代

2026/8/13 0:00:21

15-SaaS系统灰度发布&#xff1a;滚动更新、金丝雀发布、不停机迭代 一、为什么需要不停机发布&#xff1f; 传统发布方式&#xff1a;停服务 → 替换包 → 启服务。在内部系统里勉强能用&#xff0c;但在SaaS系统中是灾难。 我们的无人售货柜SaaS平台服务全国几千台设备&#…

17-线上Bug热修复流程:紧急分支、补丁合并、版本快速回退方案

17-线上Bug热修复流程:紧急分支、补丁合并、版本快速回退方案

2026/8/13 0:00:21

17-线上Bug热修复流程&#xff1a;紧急分支、补丁合并、版本快速回退方案 前言 大家好&#xff0c;我是黒漂技术佬。 线上出 Bug 这种事&#xff0c;就像你正吃着火锅唱着歌&#xff0c;突然接到电话说"柜子门打不开了"。炸不炸&#xff1f;慌不慌&#xff1f;别急&a…

摆脱论文困扰!盘点2026年全网爆红的的AI论文写作工具

摆脱论文困扰!盘点2026年全网爆红的的AI论文写作工具

2026/8/8 5:07:31

一天写完毕业论文在2026年已不再是天方夜谭。2026年最炸裂、实测能大幅提速的AI论文写作工具&#xff0c;覆盖选题构思、文献整理、内容生成、格式排版等核心场景&#xff0c;真正帮你高效搞定论文难题。 一、全流程王者&#xff1a;一站式搞定论文全链路&#xff08;一天定稿首…

导师推荐!2026最新AI论文工具测评与实用推荐

导师推荐!2026最新AI论文工具测评与实用推荐

2026/8/9 13:42:46

2026年真正好用的AI论文工具&#xff0c;核心看生成的论文质量、低AI味、格式正确、学术适配四大指标。综合实测&#xff0c;千笔AI、ThouPen、豆包、DeepSeek、Grammarly 是当前最值得推荐的梯队&#xff0c;覆盖从免费到付费、从中文到英文、从文科到理工的全场景需求。 一、…

告别游戏崩溃:XCOM 2模组管理器的智能革命

告别游戏崩溃:XCOM 2模组管理器的智能革命

2026/8/8 2:30:15

告别游戏崩溃&#xff1a;XCOM 2模组管理器的智能革命 【免费下载链接】xcom2-launcher The Alternative Mod Launcher (AML) is a replacement for the default game launchers from XCOM 2 and XCOM Chimera Squad. 项目地址: https://gitcode.com/gh_mirrors/xc/xcom2-lau…