Linq 表达式树编译非平凡的对象常量并以某种方式引用它们

Linq Expression tree compiling non-trivial object constants and somehow referring to them

通常,在编译表达式树时,我会认为不是原始类型或字符串的常量是不可能的。但是,这段代码:

   public class A
        { public int mint = -1; }

 public static void Main(String[] pArgs)
        {
            //Run(pArgs);

            Action pact = Thing();

            pact();
        }

        public static Action Thing()
        {
            var a = new A();
            a.mint = -1;

            LambdaExpression p =
                Expression.Lambda<Action>(Expression.Assign(Expression.Field(Expression.Constant(a, typeof(A)), Strong.Instance<A>.Field<int>(b => b.mint)), Expression.Constant(3, typeof(int))));

            return ((Expression<Action>)p).Compile();

        }

不仅编译而且实际上 运行s!如果你 运行 Thing() 方法中的编译方法,那么你实际上可以看到变量 a 将其字段从 -1 更改为 3

我不明白这如何使 sense/is 成为可能。方法如何引用其范围之外的局部变量(在检查 Thing() 的 IL 时,变量 a 只是一个标准的局部变量,而不是像闭包那样在堆上)。周围是否有某种隐藏的背景?当局部变量 a 可能已从堆栈中删除时,如何在 Main 中约定 运行!

只有a是局部变量;实际的 object(来自 new A())总是在堆上。当您使用 Expression.Constant(a, typeof(A)) 时,您输入的不是 a 作为常量 - 它是 avalue,即对象参考。所以:就树而言, a 的范围是无关紧要的。这实际上正是编译器通常实现捕获变量(闭包)的方式(虽然你通常看不到它,并且基于 C# 的表达式编译器不允许赋值运算符),就表达式树而言: 一切如常。

作为使用 C# 表达式编译器的比较示例,see here,其中

public void M() {
    int mint = -1;
    Expression<Func<int>> lambda = () => mint;
}

编译为:

[CompilerGenerated]
private sealed class <>c__DisplayClass0_0
{
    public int mint;
}

public void M()
{
    <>c__DisplayClass0_0 <>c__DisplayClass0_ = new <>c__DisplayClass0_0();
    <>c__DisplayClass0_.mint = -1;
    Expression.Lambda<Func<int>>(Expression.Field(Expression.Constant(<>c__DisplayClass0_, typeof(<>c__DisplayClass0_0)), FieldInfo.GetFieldFromHandle((RuntimeFieldHandle)/*OpCode not supported: LdMemberToken*/)), Array.Empty<ParameterExpression>());
}

How can a method refer to a local variable outside its scope

不能,也不能。

它可以有时引用局部变量指向的对象。

是否可以取决于表达式的编译方式或其他使用方式。

表达式本身可以通过三种方式将表达式编译成方法:

  1. Compile() 编译到 DynamicMethod 中的 IL。
  2. 使用 CompileToMethod() 编译为 IL(并非所有版本都可用。
  3. 使用运行解释的 thunk 委托将 Compile() 编译成一组解释的指令。

如果 IL 编译可用,则使用第一个,除非将 true 传递给首选解释(在具有该重载的那些版本上)并且解释也不可用。这里一个数组用于闭包,它与在委托中关闭本地 is 非常相似。

第二个用于写入另一个程序集,不能以这种方式关闭。由于这个原因,许多适用于 Compile() 的常量不适用于 CompileToMethod()

如果 IL 编译不可用,或者 true 在那些具有该重载以更喜欢解释的版本中传递,则使用第三个。这里对对象的引用被放入 "constants" 的数组中,解释器随后可以引用该数组。

另一种可能性是其他东西完全解释了这个表达式,例如在生成 SQL 代码时。通常这会失败,但非原始常量不是字符串,但如果查询处理器知道常量的类型(例如,如果它是它知道的实体类型),那么代码会产生等同于该常量的代码可以生产实体。