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

通常,在编译表达式树时,我会想到不是基元类型或字符串的常量是不可能的 . 但是,这段代码:

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();

        }

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

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

回答(2)

2 years ago

它只是 a 是一个局部变量;实际对象(来自 new A() )始终在堆上 . 当你使用 Expression.Constant(a, typeof(A)) 时,你输入的不是 a 常量 - 它是 a 的值,即对象引用 . 所以:就树而言, a 的范围无关紧要 . 这实际上就是捕获变量(闭包)通常由编译器实现的方式(尽管你不允许赋值操作符),因此就表达式而言:这是常规操作 .

作为使用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>());
}

2 years ago

方法如何引用其范围之外的局部变量

它不能,也不能 .

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

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

表达式本身有三种方法将表达式编译成方法:

  • DynamicMethod 中使用 Compile() 编译为IL .

  • 使用 CompileToMethod() 编译到IL(并非在所有版本中都可用 .

  • 使用 Compile() 编译成一组解释的指令,其中包含运行解释的thunk委托 .

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

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

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

另一种可能性是其他东西完全解释了表达,例如在生成SQL代码 . 通常,这将失败,除了字符串之外的非基本常量,但如果查询处理器知道常量的类型(例如,如果它是一种它知道的实体类型),那么代码生成等价的那个实体可以 生产环境 .