DataTable转List无零配置方案,需手动处理DBNull、类型转换、列名匹配等问题;推荐使用DataRowExtensions.Field()并添加new()约束,预建列名索引映射提升性能。

DataTable转List必须手写映射逻辑
没有通用、零配置的自动转换方案。.NET原生不提供 DataTable.AsEnumerable().ToList 这种魔法方法,强行用反射+泛型推导会掉进类型擦除、空值、日期格式、枚举映射等坑里。
常见错误现象:NullReferenceException(字段为DBNull)、InvalidCastException(比如把 DateTime 字符串列直接赋给 DateTime 属性)、PropertyNotFoundException(列名和属性名大小写/下划线不匹配)。
- 必须显式处理
DataRow.IsNull()判断,不能直接调用row["Col"] as T - 目标类型
T的属性名默认按列名(CaseSensitive)匹配,建议统一用[Column("col_name")]标记或预建映射字典 - 避免在循环内反复调用
Convert.ChangeType()——它慢且对Nullable支持差,优先用row.FieldCount对应位置索引 + 类型专用解析(如row.GetDateTime(2))
用System.Data.DataRowExtensions最省事但有兼容限制
DataRowExtensions.Field 是微软提供的安全取值封装,内部已处理 DBNull 和基础类型转换,比裸写 if (!row.IsNull("X")) x = (int)row["X"]; 干净得多。
使用场景:项目目标框架 ≥ .NET Framework 4.0 或 .NET Core 2.1,且不需要支持自定义类型(如 Point、JsonElement)。
- 必须引用
System.Data.DataSetExtensions程序集(.NET Framework 默认有,.NET 5+ 需手动加) - 对
Nullable要写row.Field,不能写("Age") row.Field否则遇到 null 会抛异常("Age") - 不支持从字符串列自动解析成
DateTimeOffset或Guid,仍需额外TryParse
泛型扩展方法里别漏掉构造函数约束
想封装一个 DataTable.ToList 扩展,必须要求 T 有无参构造函数,否则 Activator.CreateInstance 会失败——这是最常被忽略的编译通过但运行报错点。
性能影响:每次 new T() + 属性赋值比直接 new 具体类型慢 15%~30%,大数据量时建议改用 Span 预分配或对象池。
- 声明必须带
where T : new(),否则new T()编译不过 - 属性赋值别用
typeof(T).GetProperty(...).SetValue()——反射太重,改用Expression.Lambda编译一次缓存委托,或直接硬编码常用类型分支 - 列名到属性名映射建议用
Dictionary预热,避免每次循环都> GetProperties()
DataTable列顺序错乱会导致静默数据错位
很多人直接按 for (int i = 0; i 取值,但 DataTable.Columns 顺序可能和 ItemArray 一致,也可能因 DataTable.Load() 或动态添加列而错位——尤其跨数据库迁移时,Oracle 和 SQL Server 返回列序可能不同。
容易踩的坑:测试用例刚好列序一致,上线后某张表加了新字段,所有 Listrow[0] 被当成了 Id,实际却是 Name。
- 永远用列名索引:
row["Id"]、row.Table.Columns["Id"].Ordinal定位序号 - 如果必须用序号遍历(如性能敏感场景),先调用
table.Columns.Cast建立列名→序号映射并缓存().Select((c, i) => new { c.ColumnName, Index = i }).ToDictionary(x => x.ColumnName, x => x.Index) - 别依赖
DataTable.Load()的列顺序,显示调用table.Columns.Add("Id", typeof(int))明确控制顺序
事情说清了就结束











