表达式委托调用后out参数返回null的问题排查与修复咨询
问题描述
尝试通过表达式委托获取out类型参数的值,调用fun.Invoke(tableInfoCache, out tableInfo, out error)时,调试能进入TryGetTableInfo方法,且方法内的out参数tableInfo有有效值,但退出方法后,外部的tableInfo变为null。
相关代码如下:
internal class TableInfoExtractor { private TableInfoExtractor() { } public static TableInfoExtractor Extractor { get; } = new TableInfoExtractor(); public bool TryGetTableIndex(ITableInfoCache tableInfoCache, Type userDataType, out object tableInfo, out string error) { tableInfo = null; error = null; var t = tableInfoCache.GetType(); var mm = t.GenericTypeArguments[0]; var tableInfoCacheType = typeof(TableInfoCache<>).MakeGenericType(mm); try { var method = t.GetMethod("TryGetTableInfo", BindingFlags.Instance | BindingFlags.NonPublic).MakeGenericMethod(userDataType); var p1 = Expression.Parameter(typeof(ITableInfoCache)); var tableInfoCacheExp = Expression.Convert(p1, tableInfoCacheType); var objType = typeof(object).MakeByRefType(); var p2 = Expression.Parameter(objType, "tableInfo"); var tableInfoType = typeof(TableInfo<,>).MakeGenericType(mm, userDataType); var tableInfoExp = Expression.Convert(p2, tableInfoType); var errorExp = Expression.Parameter(typeof(string).MakeByRefType()); var callExp = Expression.Call(tableInfoCacheExp, method, tableInfoExp, errorExp); var fun = Expression.Lambda<TryGetTableInfoCall>(callExp, p1, p2, errorExp).Compile(); var ret = fun.Invoke(tableInfoCache, out tableInfo, out error); return ret; } catch (Exception ex) { Debug.WriteLine(ex.Message); } throw new NotImplementedException(); } }
关联代码:
internal class TableInfoCache<TDataBaseProvider> : ITableInfoCache where TDataBaseProvider : class { internal bool TryGetTableInfo<TUserData>(out TableInfo<TDataBaseProvider, TUserData> tableInfo, out string error) where TUserData : class { error = null; tableInfo = new TableInfo<TDataBaseProvider, TUserData>(); return true; } } internal class TableInfo<TDataBaseProvider, TUserData> where TDataBaseProvider : class where TUserData : class { } internal interface ITableInfoCache { } internal class UserInfo { } internal class DataBaseProvider { } internal delegate bool TryGetTableInfoCall(ITableInfoCache tableInfoCache, out object tableInfo, out string error);
调用示例:
var tableInfoCache = new TableInfoCache<DataBaseProvider>(); TableInfoExtractor.Extractor.TryGetTableIndex(tableInfoCache, typeof(UserInfo), out var tableInfo, out error);
问题原因
核心问题在于表达式树中对out参数的类型转换处理错误:
- 委托的
tableInfo参数是out object类型,而目标方法TryGetTableInfo的out参数是TableInfo<TDataBaseProvider, TUserData>类型。 - 直接使用
Expression.Convert(p2, tableInfoType)把object的引用参数转换成泛型TableInfo的引用参数,这在CLR中是不允许的——ref/in参数的类型必须严格匹配,不能直接跨类型转换。 - 这种错误的转换导致目标方法内部赋值的
TableInfo实例无法正确传递回外部的object参数,最终外部得到null。
修复方案
需要修改表达式树的构建逻辑,先创建一个临时变量存储目标方法的out参数值,调用方法后再将这个临时变量装箱赋值给object类型的out参数。具体改动如下:
internal class TableInfoExtractor { private TableInfoExtractor() { } public static TableInfoExtractor Extractor { get; } = new TableInfoExtractor(); public bool TryGetTableIndex(ITableInfoCache tableInfoCache, Type userDataType, out object tableInfo, out string error) { tableInfo = null; error = null; var t = tableInfoCache.GetType(); var mm = t.GenericTypeArguments[0]; var tableInfoCacheType = typeof(TableInfoCache<>).MakeGenericType(mm); try { var method = t.GetMethod("TryGetTableInfo", BindingFlags.Instance | BindingFlags.NonPublic).MakeGenericMethod(userDataType); var p1 = Expression.Parameter(typeof(ITableInfoCache)); var tableInfoCacheExp = Expression.Convert(p1, tableInfoCacheType); // 定义委托的两个out参数 var objOutParam = Expression.Parameter(typeof(object).MakeByRefType(), "tableInfo"); var errorOutParam = Expression.Parameter(typeof(string).MakeByRefType(), "error"); // 创建临时变量存储目标方法的out参数值 var tableInfoType = typeof(TableInfo<,>).MakeGenericType(mm, userDataType); var tempVar = Expression.Variable(tableInfoType); // 构建目标方法的调用:传入临时变量作为out参数 var methodCall = Expression.Call( tableInfoCacheExp, method, tempVar, errorOutParam ); // 调用方法后,将临时变量装箱赋值给object类型的out参数 var assignExpr = Expression.Assign( Expression.Convert(objOutParam, typeof(object)), Expression.Convert(tempVar, typeof(object)) ); // 组合调用和赋值操作,用Block表达式包裹 var blockExpr = Expression.Block( new[] { tempVar }, methodCall, assignExpr, methodCall // 返回方法调用的布尔结果 ); // 编译委托 var fun = Expression.Lambda<TryGetTableInfoCall>(blockExpr, p1, objOutParam, errorOutParam).Compile(); var ret = fun.Invoke(tableInfoCache, out tableInfo, out error); return ret; } catch (Exception ex) { Debug.WriteLine(ex.Message); } throw new NotImplementedException(); } }
关键改动说明
- 引入临时变量:创建与目标方法
out参数类型匹配的临时变量,用于接收方法内部赋值的TableInfo实例。 - Block表达式组合逻辑:用
Expression.Block把方法调用、赋值操作组合起来,确保方法调用完成后,将临时变量的值装箱到object类型的out参数中。 - 移除错误的ref转换:不再直接转换
out参数的引用类型,而是通过临时变量中转,实现类型安全的赋值传递。
内容的提问来源于stack exchange,提问作者long
相关产品推荐
相关产品推荐

