You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

表达式委托调用后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();
    }
}

关键改动说明

  1. 引入临时变量:创建与目标方法out参数类型匹配的临时变量,用于接收方法内部赋值的TableInfo实例。
  2. Block表达式组合逻辑:用Expression.Block把方法调用、赋值操作组合起来,确保方法调用完成后,将临时变量的值装箱到object类型的out参数中。
  3. 移除错误的ref转换:不再直接转换out参数的引用类型,而是通过临时变量中转,实现类型安全的赋值传递。

内容的提问来源于stack exchange,提问作者long

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.14 20:27:03