访问成员表达式的值

75

如果我有一个产品。

var p = new Product { Price = 30 };

我有以下的 LINQ 查询:

var q = repo.Products().Where(x=>x.Price == p.Price).ToList()
在一个IQueryable提供程序中,我得到了一个包含常量表达式的p.Price成员表达式,但是似乎无法从中获取值"30"。 更新: 我已经尝试过这个方法,但它似乎没有起作用。
var memberExpression = (MemberExpression)GetRootConstantExpression(m);
var fi = (PropertyInfo)memberExpression.Member;
var val = fi.GetValue(((ConstantExpression)memberExpression.Expression).Value, null);

祝你好运。

9个回答

135
你可以编译并调用一个lambda表达式,其主体是成员访问。
private object GetValue(MemberExpression member)
{
    var objectMember = Expression.Convert(member, typeof(object));

    var getterLambda = Expression.Lambda<Func<object>>(objectMember);

    var getter = getterLambda.Compile();

    return getter();
}

当解析表达式树时,本地评估是一种常见的技术。LINQ to SQL 在许多地方都使用了这种方法。


1
在我使用的示例中,当它解析为double时,类型为'System.Double'的表达式不能用于返回类型'System.Object'。 - Schotime
6
必须添加以下代码:var expression = Expression.Convert(member, typeof(object)); ,放在该函数的第一行,以修复关于双重转换的错误! - Schotime
啊是的,我有时会忘记,在表达树中你必须显式地指定 C# 隐含的内容(例如转换)。我很高兴这对你起作用。 - Bryan Watts
太棒了!! :) 非常感谢。 - sebastian
注意:如果你有一个类型为 Expression<Func<T>>无参表达式 expr(例如 expr 包含表达式 () => myObj),那么一行代码 object exprValue = expr.Compile()(); 就可以搞定了。之后再将其转换成所需的类型。在某些情况下非常有用。 - Matt
正是我所需要的。 :) 非常感谢 - Ege Aydın

41
 MemberExpression right = (MemberExpression)((BinaryExpression)p.Body).Right;
 Expression.Lambda(right).Compile().DynamicInvoke();

获取结果的最快、最简洁的方式。 - iggymoran
1
无法相信涉及DynamicInvoke的东西可以是最快的@iggymoran,你测试过吗?或者你是指打字最快?;) - nawfal
最快输入和最容易理解正在发生的事情。DynamicInvoke最终使用反射来执行它,并不是世界上最快的东西。Bryan Watts的答案通过获取一个func并执行它(只是用invoke)来解决了这个问题。当我第一次看到这个答案时,更容易理解正在发生的事情。 - iggymoran
如果可以的话,我会给你+10分的 :) 真棒! - icesar

29

这个常量表达式将指向编译器生成的捕获类。我没有包括决策点等,但以下内容可以得到30:

var p = new Product { Price = 30 };
Expression<Func<Product, bool>> predicate = x => x.Price == p.Price;
BinaryExpression eq = (BinaryExpression)predicate.Body;
MemberExpression productToPrice = (MemberExpression)eq.Right;
MemberExpression captureToProduct = (MemberExpression)productToPrice.Expression;
ConstantExpression captureConst = (ConstantExpression)captureToProduct.Expression;
object product = ((FieldInfo)captureToProduct.Member).GetValue(captureConst.Value);
object price = ((PropertyInfo)productToPrice.Member).GetValue(product, null);

price 现在是 30。请注意,我假设 Price 是一个属性,但实际上你应该编写一个 GetValue 方法来处理属性/字段。


如果对象中有另一层嵌套,会发生什么变化吗?例如:p.Product.Price。 - Schotime
3
@Schotime - 确实如此。要以通用的方式处理这个问题,请参考这里的 EvaluateTryEvaluate:http://code.google.com/p/protobuf-net/source/browse/trunk/protobuf-net.Extensions/ServiceModel/Client/ProtoClientExtensions.cs - Marc Gravell
1
@MarcGravell 哪个更快:编译 MemberExpression 然后评估它,还是到达其 PropertyInfo/FieldInfo 然后像 TryEvaluate 一样评估它? - Ashraf Sabry
2
@AshrafSabry 这取决于您要执行多少次以及是否重复使用委托。 - Marc Gravell

6

截至2020年

这个辅助方法会优雅地检索任何表达式的值,而不需要"编译黑客":

public static object GetMemberExpressionValue (MemberExpression expression)
{
    // Dependency chain of a MemberExpression is of the form:
    // MemberExpression expression
    //    MemberExpression expression.Expression
    //        ... MemberExpression expression.[...].Expression
    //            ConstantExpression expression.[...].Expression.Expression <- base object
    var dependencyChain = new List<MemberExpression>();
    var pointingExpression = expression;
    while (pointingExpression != null)
    {
        dependencyChain.Add(pointingExpression);
        pointingExpression = pointingExpression.Expression as MemberExpression;
    }

    if (!(dependencyChain.Last().Expression is ConstantExpression baseExpression))
    {
        throw new Exception(
            $"Last expression {dependencyChain.Last().Expression} of dependency chain of {expression} is not a constant." +
            "Thus the expression value cannot be found.");
    }

    var resolvedValue = baseExpression.Value;

    for (var i = dependencyChain.Count; i > 0; i--)
    {
        var expr = dependencyChain[i - 1];
        resolvedValue = new PropOrField(expr.Member).GetValue(resolvedValue);
    }

    return resolvedValue;
}

PropOrField 类:

public class PropOrField
{
    public readonly MemberInfo MemberInfo;

    public PropOrField (MemberInfo memberInfo)
    {
        if (!(memberInfo is PropertyInfo) && !(memberInfo is FieldInfo))
        {
            throw new Exception(
                $"{nameof(memberInfo)} must either be {nameof(PropertyInfo)} or {nameof(FieldInfo)}");
        }

        MemberInfo = memberInfo;
    }

    public object GetValue (object source)
    {
        if (MemberInfo is PropertyInfo propertyInfo) return propertyInfo.GetValue(source);
        if (MemberInfo is FieldInfo fieldInfo) return fieldInfo.GetValue(source);

        return null;
    }

    public void SetValue (object target, object source)
    {
        if (MemberInfo is PropertyInfo propertyInfo) propertyInfo.SetValue(target, source);
        if (MemberInfo is FieldInfo fieldInfo) fieldInfo.SetValue(target, source);
    }

    public Type GetMemberType ()
    {
        if (MemberInfo is PropertyInfo propertyInfo) return propertyInfo.PropertyType;
        if (MemberInfo is FieldInfo fieldInfo) return fieldInfo.FieldType;

        return null;
    }
}

5
如果你有一个类:
public class Item
{
    public int Id { get; set; }
}

以及对象的一个实例:

var myItem = new Item { Id = 7 };

你可以使用以下代码来使用表达式获取Id的值:

Expression<Func<Item, int>> exp = x => x.Id;
var me = exp.Body as MemberExpression;
var propInfo = me.Member as PropertyInfo;
var myValue = propInfo.GetValue(myItem, null);

myValue将包含"7"


1
value 是一个保留标识符。 - Nick Gallimore
@NickGallimore 谢谢,发现的很好!我已经相应地更新了示例以删除此内容。 - t_warsop

2
使用Expression.Lambda(myParameterlessExpression).Compile().Invoke()存在一些缺点:
  • .Compile()速度较慢。即使对于小的表达式片段,它也可能需要多毫秒才能完成。但是之后的Invoke调用非常快,对于简单的算术表达式或成员访问,只需要几个纳秒。
  • .Compile()将生成(发出)MSIL代码。这听起来很完美(并解释了优秀的执行速度),但问题在于:该代码占用内存,即使GC收集了委托引用,该内存也不能在应用程序完成前被释放!
可以完全避免使用Compile()来避免这些问题,或者缓存编译后的委托以便重复使用。我的expression-utils库提供了两种方法:对Expressions进行解释和缓存编译,其中所有表达式的常量和闭包都会自动替换为附加参数,然后重新插入到闭包中,并返回给用户。这两个过程都经过了充分测试,在生产中使用,它们各有优缺点,但比Compile()快100倍以上 - 并避免了内存泄漏!

编译的委托和相关代码将被垃圾回收。 - OwnageIsMagic

1

q 是一个 List<Product> 类型。该列表没有价格属性 - 只有单个产品有。

第一个或最后一个产品将有一个价格。

q.First().Price
q.Last().Price

如果你知道集合中只有一个元素,你也可以使用 Single 方法将其展平。
q.Single().Price

是的,但是在末尾加上.ToList()会将其转换为列表。 - Kirk Broadhurst
2
无论是List还是IQueryable,您仍然可以使用First、Last或Single - 但请不要误解,repo.Products.ToList()绝对是一个List。 - Kirk Broadhurst
1
你说得对,Kobi。我对这个很熟悉,因为我正在尝试解析表达式树。只是稍微复杂一些。 - Schotime
好的,我现在明白你/你们试图实现什么了,我没有从最初的问题中理解到这一点。 - Kirk Broadhurst

1

你能使用以下内容吗:

var price = p.Price;
var q = repo.Products().Where(x=>x.Price == price).ToList()

这个可以工作,但如果不需要这样做就更好了。Linq-2-Sql支持我尝试实现的语法吗? - Schotime

0
你想要达成什么目标呢?
因为如果要访问“价格”(Price)的值,你需要执行类似以下操作:
var valueOfPrice = q[0].Price;

我正在尝试将表达式转换为纯文本,并且需要将p.Price评估为"30"。 - Schotime

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接