DataTable转List需处理DBNull、类型兼容性、反射性能、列名映射及实例创建五大问题:判空后按可空类型赋值,用Convert.ChangeType配合TryParse兜底;缓存PropertyInfo与预编译setter委托;通过Column特性或字典映射列名;用Activator.CreateInstance或default(T)安全构造实例。

DataTable转List时,DataRow字段类型不匹配导致报错
DataRow字段类型不匹配导致报错直接用Convert.ChangeType强转DataRow值到目标属性类型,遇到DBNull或类型不兼容(比如数据库里是int但C#属性是int?)就会抛InvalidCastException或NullReferenceException。
实操建议:
- 先判断
row[columnName] == DBNull.Value,再决定是否跳过、赋默认值或走可空类型路径 - 用
typeof(T).GetProperty(...)获取属性类型后,调用Convert.ChangeType(value, propertyType),而不是硬写(int)row["Id"] - 对
DateTime、bool等易出错类型,加一层TryParse兜底(尤其当数据库字段允许NULL且实际存了空字符串时)
示例关键片段:
if (row[columnName] == DBNull.Value)
{
if (property.PropertyType.IsNullable())
property.SetValue(item, null);
continue;
}
var converted = Convert.ChangeType(row[columnName], property.PropertyType);
反射填充List性能差,10万行DataTable要几秒?
每次循环都调用GetProperty、GetValue、SetValue,反射开销大;更糟的是没缓存PropertyInfo数组和setter委托,反复解析元数据。
实操建议:
- 把
typeof(T).GetProperties()结果缓存为静态IReadOnlyList<PropertyInfo>,避免每次重建 - 用
Expression.Lambda或Delegate.CreateDelegate预编译setter,代替PropertyInfo.SetValue - 若T是简单POCO且字段名与列名严格一致,优先用
System.Data.DataSetExtensions里的CopyToDataTable反向思路——但注意它只支持DataRow,不直接生成List<T>
缓存PropertyInfo的典型写法:
private static readonly IReadOnlyList<PropertyInfo> _props = typeof(T).GetProperties()
.Where(p => p.CanWrite && p.GetSetMethod() != null)
.ToArray();
列名和属性名大小写/下划线不一致,反射找不到对应属性
数据库字段是user_name,C#属性叫UserName或userName,默认反射按精确名称匹配,直接失败。
实操建议:
- 统一用
StringComparison.OrdinalIgnoreCase做列名→属性名映射,别依赖ToPascalCase这种不可靠转换 - 加一个
Dictionary<string, PropertyInfo>做列名映射表,初始化时遍历一次_props,按规则注册键(如"user_name"→UserName属性) - 更稳妥的做法:在属性上加
[Column("user_name")]自定义特性,运行时读取,避免字符串硬编码
映射逻辑示意:
var map = new Dictionary<string, PropertyInfo>(StringComparer.OrdinalIgnoreCase);
foreach (var p in _props)
{
var attr = p.GetCustomAttribute<ColumnAttribute>();
var key = attr?.Name ?? p.Name;
map[key] = p;
}
泛型约束不够,T带无参构造函数但被JIT优化掉
写了where T : new(),但某些场景(如T是struct或有显式私有构造函数)仍会运行时报MissingMethodException;更隐蔽的是.NET Core 6+ AOT编译下,new T()可能被裁剪。
实操建议:
- 不用
new T(),改用Activator.CreateInstance<T>(),它能绕过部分AOT限制 - 对
struct类型,检查typeof(T).IsValueType && !typeof(T).IsGenericTypeDefinition,必要时用default(T) - 如果项目启用了
TrimMode=partial,在.csproj中为该类型加<TrimmerRootAssembly Include="YourAssembly" />
安全创建实例的方式:
T item;
if (typeof(T).IsValueType)
item = default;
else
item = Activator.CreateInstance<T>();
反射自动填充的核心难点不在“怎么写”,而在“怎么让每一步都不依赖运行时侥幸”。列名映射、类型转换、实例创建、委托缓存——每个环节漏掉一个边界条件,上线后就卡在某张表的某个字段上。


















