C# DataRow自定义Get<T>方法疑问:类型转换与性能优化
C# DataRow 扩展方法相关疑问
我在使用C#从数据库获取数据时,了解到DataRow有Field<T>扩展方法:
public static T? Field<T>(this DataRow row, string columnName)
但我希望有一个更易用的方法,满足以下需求:
- 当
row["COLUMNNAME"]为null或DBNull时返回default(T)(我知道可以用int?,但不够便捷) - 访问Int16列时用Int32类型不会抛出异常(如
dr.Field<int>("COMLUMINT16")) - 支持将int类型转换为string类型(如
dr.Field<string>("COLUMNINT"))
于是我编写了一个替代版本:
public static T Get<T>(this DataRow row, string columnName) { if (row == null || row.IsNull(columnName)) return default(T); else { if (row[columnName].GetType() == typeof(T)) return row.Field<T>(columnName); else return (T)row[columnName]; //return (T)Convert.ChangeType(row[columnName], typeof(T)); } }
但我有两个疑问:
- 为什么直接使用
return (T)row[columnName];会抛出“Unable to cast object of type 'System.Int16' to type 'System.Int32'”异常,而如下隐式转换却能正常工作:short x = 6; int y = x; - 有没有比使用Convert类更简便的方法?听说Convert类的性能较差。
解答
针对第一个疑问
核心原因是装箱拆箱的类型匹配限制:
- 当你写
short x = 6; int y = x;时,这是编译阶段就能确定的值类型隐式转换,编译器会直接生成转换指令,把short的数值直接转成int类型,全程没有涉及装箱操作。 - 但
row[columnName]返回的是object类型,数据库返回的short已经被装箱成了object实例。CLR拆箱时有严格要求:拆箱的目标类型必须和装箱前的原始类型完全一致。你不能直接把装箱后的short对象拆箱成int,必须先拆成short,再转成int,这就是直接强制转换报错的原因。
针对第二个疑问
如果不想依赖Convert.ChangeType,可以根据场景选择以下两种更高效的方案:
方案1:针对常用类型做分支转换
对项目中高频使用的转换场景写分支判断,跳过Convert的通用逻辑开销,示例如下:
public static T Get<T>(this DataRow row, string columnName) { if (row == null || row.IsNull(columnName)) return default(T); var value = row[columnName]; var targetType = typeof(T); // 类型直接匹配时直接返回 if (value.GetType() == targetType) return (T)value; // 针对常用类型做定向转换 if (targetType == typeof(int)) { return (T)(object)Convert.ToInt32(value); } else if (targetType == typeof(long)) { return (T)(object)Convert.ToInt64(value); } else if (targetType == typeof(string)) { return (T)(object)(value?.ToString() ?? string.Empty); } // 可继续扩展double、bool等常用类型 else { // 兜底用Convert处理小众转换场景 return (T)Convert.ChangeType(value, targetType); } }
这种方式对高频场景做了优化,性能比全用Convert要好很多,同时代码复杂度也不高。
方案2:用表达式树预编译转换逻辑
如果追求极致性能,可以用表达式树在首次调用时预编译转换委托,后续调用直接复用编译后的逻辑:
private static readonly Dictionary<Type, Dictionary<Type, Delegate>> _converterCache = new Dictionary<Type, Dictionary<Type, Delegate>>(); public static T Get<T>(this DataRow row, string columnName) { if (row == null || row.IsNull(columnName)) return default(T); var value = row[columnName]; var sourceType = value.GetType(); var targetType = typeof(T); if (sourceType == targetType) return (T)value; // 从缓存获取预编译的转换委托 if (!_converterCache.TryGetValue(sourceType, out var targetMap) || !targetMap.TryGetValue(targetType, out var converter)) { // 构建转换表达式并编译 var paramExpr = Expression.Parameter(typeof(object), "obj"); var unboxExpr = Expression.Convert(paramExpr, sourceType); var convertExpr = Expression.Convert(unboxExpr, targetType); var lambda = Expression.Lambda<Func<object, T>>(convertExpr, paramExpr); converter = lambda.Compile(); // 缓存编译后的委托 if (!_converterCache.ContainsKey(sourceType)) _converterCache[sourceType] = new Dictionary<Type, Delegate>(); _converterCache[sourceType][targetType] = converter; } return ((Func<object, T>)converter)(value); }
表达式树只会在首次遇到特定类型转换时编译一次,后续调用的性能几乎和硬编码转换逻辑一致,适合转换场景多且性能要求高的项目。
内容的提问来源于stack exchange,提问作者b0bik
相关产品推荐
相关产品推荐

