我可以将局部变量捕获到LINQ表达式中作为常量而不是闭包引用吗?

时间:2010-10-21 20:29:10

标签: c# linq lambda

我想说

int x = magic(), y = moremagic();
return i => i + (x/y);

并将x捕获为常量而不是变量引用。这个想法是x永远不会改变,因此当表达式稍后编译时,编译器可以进行常量折叠并生成更高效的代码 - 即计算x/y一次而不是每次调用,通过指针解引用到关闭记录。

无法在方法中将x标记为只读,并且编译器不够聪明,无法检测到在创建表达式后它不会更改。

我不想手工构建表达式。有什么好主意吗?

UPDATE :我最终使用了奇妙的LinqKit来构建一个部分评估器,它将执行我想要的替换。只有当您知道相关引用不会改变时,转换才是安全的,但它适用于我的目的。可以通过在其中添加额外的一两个检查来限制部分评估仅限于您控制的闭包的直接成员,这在检查LinqKit中提供的示例代码时非常明显。

/// <summary>Walks your expression and eagerly evaluates property/field members and substitutes them with constants.
/// You must be sure this is semantically correct, by ensuring those fields (e.g. references to captured variables in your closure)
/// will never change, but it allows the expression to be compiled more efficiently by turning constant numbers into true constants, 
/// which the compiler can fold.</summary>
public class PartiallyEvaluateMemberExpressionsVisitor : ExpressionVisitor
{
    protected override Expression VisitMemberAccess(MemberExpression m)
    {
        Expression exp = this.Visit(m.Expression);

        if (exp == null || exp is ConstantExpression) // null=static member
        {
            object @object = exp == null ? null : ((ConstantExpression)exp).Value;
            object value = null; Type type = null;
            if (m.Member is FieldInfo)
            {
                FieldInfo fi = (FieldInfo)m.Member;
                value = fi.GetValue(@object);
                type = fi.FieldType;
            }
            else if (m.Member is PropertyInfo)
            {
                PropertyInfo pi = (PropertyInfo)m.Member;
                if (pi.GetIndexParameters().Length != 0)
                    throw new ArgumentException("cannot eliminate closure references to indexed properties");
                value = pi.GetValue(@object, null);
                type = pi.PropertyType;
            }
            return Expression.Constant(value, type);
        }
        else // otherwise just pass it through
        {
            return Expression.MakeMemberAccess(exp, m.Member);
        }
    }
}

5 个答案:

答案 0 :(得分:4)

在C#中没有办法做到这一点。编译器不支持按值/ const捕获变量。也不能以这种方式在运行时将非const值转换为const值。

此外,C#编译器仅在已知常量值的初始编译期间执行常量折叠。如果可以将运行时的值冻结为常量,则它不会参与编译器常量折叠,因为它在运行时发生。

答案 1 :(得分:2)

编译器不执行此类“值缓存”。常量折叠在编译时仅针对常量完成,不适用于只读字段,当然也不适用于编译时没有已知值的局部变量。

你必须自己做这个,但它必须保留一个闭包引用(因为该值实际上在编译时是不可确定的,这就是为什么它可能在构建表达式时被置于闭包中):

int x = magic(), y = moremagic();
int xy = x/y;
return i => i + xy;

答案 2 :(得分:0)

x不能是一个常量,因为你正在做运行时魔法来确定它是什么。但是,如果您知道xy没有更改,请尝试:

int x = magic(), y = moremagic();
int xOverY = x/y;
return i => i + xOverY;

我还应该提一下,即使i => i + (x/y)的已编译IL代码将显示除法,JIT编译器几乎肯定会对此进行优化。

答案 3 :(得分:0)

我在vb2005中使用的一种技术是使用通用委托工厂来实现按值闭包。我只为子而不是函数实现它,但它也可以用于函数。如果以这种方式延长:

FunctionOf.NewInv()

将是一个静态函数,它将接受函数(稍后描述),T3和T4作为参数。传入函数应接受T2,T3和T4类型的参数,并返回T1。 NewInv返回的函数将接受一个T2类型的参数,并使用该参数和赋予NewInv的函数调用传入函数。

调用看起来像:

return FunctionOf.NewInv((i,x,y) => i+x/y, x, y)

答案 4 :(得分:-1)

如果您(像我一样)正在为SQL查询创建一些表达式构建器,您可以考虑以下内容:首先创建一个类变量,使其成为常量,然后像这样访问它:

var constant= Expression.Constant(values);
var start = Expression.MakeMemberAccess(constant, values.GetMemberInfo(f => f.Start));
var end = Expression.MakeMemberAccess(constant, values.GetMemberInfo(f => f.End));

var more = Expression.GreaterThanOrEqual(memberBody, start);
var less = Expression.LessThanOrEqual(memberBody, end);