ClosedXML 是 C# 中最省心稳定的无 Office 依赖 Excel 生成方案,但需严守行为边界:必须引入 using ClosedXML.Excel;,Cell 行列号从 1 开始,RowsUsed() 才返回真实数据行。

直接上结论:ClosedXML 是目前 C# 中最省心、最稳、最易上手的无 Office 依赖 Excel 生成方案,但必须严格遵守它的行为边界——否则 SaveAs() 不报错却输出空文件、Cell(1, 1) 写错行号、RowsUsed() 被误用成 RowCount(),都是高频翻车点。
安装与命名空间:漏掉 using ClosedXML.Excel; 就编译不过
装完 NuGet 包后,90% 的新手卡在这一步:只执行了 Install-Package ClosedXML,却没在 .cs 文件顶部加 using ClosedXML.Excel;。结果 XLWorkbook、IWorksheet 全红,IDE 提示“找不到类型”。这不是环境问题,是硬性引用缺失。
-
XLWorkbook、IXLWorksheet、IXLCell等核心类型全在该命名空间下 - .NET 6+ 项目也需显式声明,别指望全局 using(除非你手动配了
Directory.Build.props) - 若用 .NET Framework 4.7.2 或更低版本,确认目标框架 ≥ 4.0;ClosedXML 不支持 3.5 及以下
Cell("A1") 和 Cell(row, column) 的坐标逻辑必须分清
列号从 1 开始,不是 0;Cell(1, 1) 对应 A1,Cell(2, 3) 对应 C2——这个反直觉设计容易写错,尤其在循环中用 i 当行、j 当列时。
-
Cell("B5")和Cell(5, 2)等价,但后者更适合 for 循环赋值 - 合并单元格后,只有左上角单元格保留值,其余
Cell(row, col).Value返回 null 或空字符串 - 写入日期时,
cell.Value = DateTime.Now会自动按 Excel 序列号存储;若单元格格式为文本,则读回来是字符串,需DateTime.TryParse()容错
读取数据别用 RowCount(),必须用 RowsUsed()
ws.RowCount() 返回的是 Excel 文件内部记录的最大行数(通常是 1048576),哪怕你只填了 A1,它也返回一百万。而 ws.RowsUsed() 才真正返回“有内容或有样式的行”的集合,这才是你要遍历的对象。
- 错误写法:
for (int i = 1; i → 死循环或 OOM - 正确写法:
foreach (var row in ws.RowsUsed())→ 自动跳过全空行、带样式的空行 - 如果必须按索引处理,用
ws.Rows(1, ws.LastRowUsed().RowNumber()),它比RowCount()安全得多 -
RowsUsed()会包含设置了边框、背景色但没填值的行,这是设计使然,不是 bug
SaveAs() 是唯一落盘动作,不调就等于没写
很多人写了几十行 cell.Value = ...,最后忘了 workbook.SaveAs("xxx.xlsx"),运行完发现目录里根本没有文件——ClosedXML 不会自动保存,也不抛异常,就是静默失败。
-
SaveAs()必须在using块内或显式Dispose()前调用,否则可能因流未关闭导致文件损坏 - 路径不存在时,
SaveAs()会抛DirectoryNotFoundException,需提前Directory.CreateDirectory() - 若要内存流导出(如 Web API 返回 Excel),用
workbook.SaveAs(stream),注意stream.Position = 0再返回给前端 - 不支持 .xls 格式,传入 .xls 路径会抛
InvalidDataException或静默失败;老文件务必先转成 .xlsx
最常被忽略的其实是线程安全:ClosedXML 实例(XLWorkbook、IXLWorksheet)**不能跨线程共享**。一个 XLWorkbook 实例只能由单一线程操作,多线程并行生成多个 Excel 时,必须每个线程 new 自己的实例——这点在 ASP.NET Core 高并发导出场景里,一踩一个准。











