同一个ParameterExpression被用在不同嵌套层次的lambda里会怎样?

本文探讨了使用 LINQ 时 ParameterExpression 在不同 ExpressionTree 中的语义变化,尤其是在 LINQv1 和 LINQv2 之间的差异,并通过调试揭示了其背后的原因。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

今天写代码的时候不小心写错了几个地方,把同一个ParameterExpression用在内外两个Expression tree里了。结果很诡异,总之先记下来。

用LINQv1,测试这样的代码:
using System;
using Linq = System.Linq.Expressions;
using Ast = System.Linq.Expressions.Expression;

sealed class Program {
static void Main(string[] args) {
var x = Ast.Parameter(typeof(double), "x");
var y = Ast.Parameter(typeof(double), "y");

var expr = Ast.Lambda<Action<double, double>>(
Ast.Invoke(
Ast.Lambda<Action<double, double>>(
Ast.Call(
null,
typeof(Console).GetMethod("WriteLine", new Type[] { typeof(object) }),
Ast.Convert(
x,
typeof(object)
)
),
new [] { x, y }
),
new Linq.Expression[] {
y, x
}
),
new [] { x, y }
);
expr.Compile()(2, 5);
}
}

结果无论在内层的lambda里我输出x还是y,得到的都是5。于是我就纳闷了……

糟就糟在,这个lambda没办法用普通C#的lambda表达式写出来,因为如果这样写:
Expression<Action<double, double>> expr
= (x,y) => ((Action<double, double>)((x,y) => Console.WriteLine(x)))(y,x);

在C#里是不合法的:x和y在内层被重定义了,而C#不允许这样的重定义。

本来这个时候应该拿SOS来调试一下,看看到底Compile出了怎样的DynamicMethod。不过想着既然有IronPython的LINQv2,干脆拿源码来调试更方便。
于是引用IronPython里的Microsoft.Scripting.Core.dll,把测试代码改成:
using System;
using Dlr = Microsoft.Linq.Expressions;
using Ast = Microsoft.Linq.Expressions.Expression;

sealed class Program {
static void Main(string[] args) {
var x = Ast.Parameter(typeof(double), "x");
var y = Ast.Parameter(typeof(double), "y");

var expr = Ast.Lambda<Action<double, double>>(
Ast.Invoke(
Ast.Lambda<Action<double, double>>(
Ast.Call(
null,
typeof(Console).GetMethod("WriteLine", new Type[] { typeof(object) }),
Ast.Convert(
x,
typeof(object)
)
),
new [] { x, y }
),
new Dlr.Expression[] {
y, x
}
),
new [] { x, y }
);
expr.Compile()(2, 5);
}
}

就是换了俩namespace。在第29行设上断点,然后跟到LambdaExpression.Compile() -> LambdaCompiler.Compile() -> LambdaCompiler.CreateDelegate(),在这里看到底生成了怎样的DynamicMethod。
结果看到外层的DynamicMethod是:Void lambda_method$1(Microsoft.Runtime.CompilerServices.Closure, Double, Double)
IL_0000: /* 02  |          */ ldarg.0    
IL_0001: /* 7b | 04000002 */ ldfld !"指定的转换无效。"!
IL_0006: /* 16 | */ ldc.i4.0
IL_0007: /* 9a | */ ldelem.ref
IL_0008: /* 74 | 02000003 */ castclass System.Reflection.Emit.DynamicMethod
IL_000d: /* d0 | 02000004 */ ldtoken Microsoft.Action`2[System.Double,System.Double]/
IL_0012: /* 28 | 06000005 */ call System.Type GetTypeFromHandle(System.RuntimeTypeHandle)/System.Type
IL_0017: /* 14 | */ ldnull
IL_0018: /* 6f | 06000006 */ callvirt System.Delegate CreateDelegate(System.Type, System.Object)/System.Reflection.Emit.DynamicMethod
IL_001d: /* 74 | 02000007 */ castclass Microsoft.Action`2[System.Double,System.Double]
IL_0022: /* 04 | */ ldarg.2
IL_0023: /* 03 | */ ldarg.1
IL_0024: /* 6f | 06000008 */ callvirt Void Invoke(Double, Double)/Microsoft.Action`2[System.Double,System.Double]
IL_0029: /* 2a | */ ret

其中调用的内层DynamicMethod是:Void lambda_method$2(Microsoft.Runtime.CompilerServices.Closure, Double, Double)
IL_0000: /* 03  |          */ ldarg.1    
IL_0001: /* 8c | 02000002 */ box System.Double
IL_0006: /* 28 | 06000003 */ call Void WriteLine(System.Object)/System.Console
IL_000b: /* 2a | */ ret


一看,这不就对了么?编译出来的应该跟这个lambda表达式的一样才对啊:
Expression<Action<double, double>> expr
= (x,y) => ((Action<double, double>)((m,n) => Console.WriteLine(m)))(y,x);

然后离开单步模式,直接运行下去,发现确实跟我原本预期的行为是一样的。把内层的x换成y也没错:内层是x的时候看到输出是5,内层是y的时候看到输出是2。

也就是说LINQv1和LINQv2中ParameterExpression的语义有了很大的变化,影响到了原本LINQv1中的Expression tree的语义……

那LINQv1到底是怎么回事呢?还真是得拿SOS来调试才行了,呜。回头再说吧。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值