C# 从 lambda 表达式中检索属性名称

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/671968/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-04 12:44:42  来源:igfitidea点击:

Retrieving Property name from lambda expression

c#linqlambdaexpression-trees

提问by Schotime

Is there a better way to get the Property name when passed in via a lambda expression? Here is what i currently have.

通过 lambda 表达式传入时,是否有更好的方法来获取属性名称?这是我目前拥有的。

eg.

例如。

GetSortingInfo<User>(u => u.UserId);

It worked by casting it as a memberexpression only when the property was a string. because not all properties are strings i had to use object but then it would return a unaryexpression for those.

只有当属性是字符串时,它才通过将其转换为成员表达式来工作。因为并非所有属性都是字符串,所以我必须使用对象,但它会为那些返回一元表达式。

public static RouteValueDictionary GetInfo<T>(this HtmlHelper html, 
    Expression<Func<T, object>> action) where T : class
{
    var expression = GetMemberInfo(action);
    string name = expression.Member.Name;

    return GetInfo(html, name);
}

private static MemberExpression GetMemberInfo(Expression method)
{
    LambdaExpression lambda = method as LambdaExpression;
    if (lambda == null)
        throw new ArgumentNullException("method");

    MemberExpression memberExpr = null;

    if (lambda.Body.NodeType == ExpressionType.Convert)
    {
        memberExpr = 
            ((UnaryExpression)lambda.Body).Operand as MemberExpression;
    }
    else if (lambda.Body.NodeType == ExpressionType.MemberAccess)
    {
        memberExpr = lambda.Body as MemberExpression;
    }

    if (memberExpr == null)
        throw new ArgumentException("method");

    return memberExpr;
}

采纳答案by Schotime

I found another way you can do it was to have the source and property strongly typed and explicitly infer the input for the lambda. Not sure if that is correct terminology but here is the result.

我发现另一种方法是强类型化源和属性并显式推断 lambda 的输入。不确定这是否是正确的术语,但这是结果。

public static RouteValueDictionary GetInfo<T,P>(this HtmlHelper html, Expression<Func<T, P>> action) where T : class
{
    var expression = (MemberExpression)action.Body;
    string name = expression.Member.Name;

    return GetInfo(html, name);
}

And then call it like so.

然后这样称呼它。

GetInfo((User u) => u.UserId);

and voila it works.
Thanks all.

瞧,它起作用了。
谢谢大家。

回答by Cameron MacFarland

I recently did a very similar thing to make a type safe OnPropertyChanged method.

我最近做了一个非常相似的事情来制作一个类型安全的 OnPropertyChanged 方法。

Here's a method that'll return the PropertyInfo object for the expression. It throws an exception if the expression is not a property.

这是一个返回表达式的 PropertyInfo 对象的方法。如果表达式不是属性,则抛出异常。

public PropertyInfo GetPropertyInfo<TSource, TProperty>(
    TSource source,
    Expression<Func<TSource, TProperty>> propertyLambda)
{
    Type type = typeof(TSource);

    MemberExpression member = propertyLambda.Body as MemberExpression;
    if (member == null)
        throw new ArgumentException(string.Format(
            "Expression '{0}' refers to a method, not a property.",
            propertyLambda.ToString()));

    PropertyInfo propInfo = member.Member as PropertyInfo;
    if (propInfo == null)
        throw new ArgumentException(string.Format(
            "Expression '{0}' refers to a field, not a property.",
            propertyLambda.ToString()));

    if (type != propInfo.ReflectedType &&
        !type.IsSubclassOf(propInfo.ReflectedType))
        throw new ArgumentException(string.Format(
            "Expression '{0}' refers to a property that is not from type {1}.",
            propertyLambda.ToString(),
            type));

    return propInfo;
}

The sourceparameter is used so the compiler can do type inference on the method call. You can do the following

使用该source参数是为了让编译器可以对方法调用进行类型推断。您可以执行以下操作

var propertyInfo = GetPropertyInfo(someUserObject, u => u.UserID);

回答by Marc Gravell

Well, there's no need to call .Name.ToString(), but broadly that is about it, yes. The only consideration you might need is whether x.Foo.Barshould return "Foo", "Bar", or an exception - i.e. do you need to iterate at all.

嗯,没有必要调用.Name.ToString(),但大致就是这样,是的。您可能需要的唯一考虑是是否x.Foo.Bar应该返回“Foo”、“Bar”或异常 - 即您是否需要迭代。

(re comment) for more on flexible sorting, see here.

(重新评论)有关灵活排序的更多信息,请参见此处

回答by M Thelen

I was playing around with the same thing and worked this up. It's not fully tested but seems to handle the issue with value types (the unaryexpression issue you ran into)

我在玩同样的事情并解决了这个问题。它没有经过全面测试,但似乎可以处理值类型的问题(您遇到的一元表达式问题)

public static string GetName(Expression<Func<object>> exp)
{
    MemberExpression body = exp.Body as MemberExpression;

    if (body == null) {
       UnaryExpression ubody = (UnaryExpression)exp.Body;
       body = ubody.Operand as MemberExpression;
    }

    return body.Member.Name;
}

回答by Anders

I created an extension method on ObjectStateEntry to be able to flag properties (of Entity Framework POCO classes) as modified in a type safe manner, since the default method only accepts a string. Here's my way of getting the name from the property:

我在 ObjectStateEntry 上创建了一个扩展方法,以便能够将(实体框架 POCO 类的)属性标记为以类型安全的方式修改,因为默认方法只接受一个字符串。这是我从属性中获取名称的方法:

public static void SetModifiedProperty<T>(this System.Data.Objects.ObjectStateEntry state, Expression<Func<T>> action)
{
    var body = (MemberExpression)action.Body;
    string propertyName = body.Member.Name;

    state.SetModifiedProperty(propertyName);
}

回答by faester

I have done the INotifyPropertyChangedimplementation similar to the method below. Here the properties are stored in a dictionary in the base class shown below. It is of course not always desirable to use inheritance, but for view models I think it is acceptable and gives very clean property references in the view model classes.

我已经完成了INotifyPropertyChanged类似于下面的方法的实现。这里的属性存储在如下所示的基类中的字典中。使用继承当然并不总是可取的,但对于视图模型,我认为这是可以接受的,并且在视图模型类中提供了非常干净的属性引用。

public class PhotoDetailsViewModel
    : PropertyChangedNotifierBase<PhotoDetailsViewModel>
{
    public bool IsLoading
    {
        get { return GetValue(x => x.IsLoading); }
        set { SetPropertyValue(x => x.IsLoading, value); }
    }

    public string PendingOperation
    {
        get { return GetValue(x => x.PendingOperation); }
        set { SetPropertyValue(x => x.PendingOperation, value); }
    }

    public PhotoViewModel Photo
    {
        get { return GetValue(x => x.Photo); }
        set { SetPropertyValue(x => x.Photo, value); }
    }
}

The somewhat more complex base class is shown below. It handles the translation from lambda expression to property name. Note that the properties are really pseudo properties since only the names are used. But it will appear transparent to the view model and references to the properties on the view model.

稍微复杂一些的基类如下所示。它处理从 lambda 表达式到属性名称的转换。请注意,这些属性实际上是伪属性,因为只使用了名称。但它对视图模型和对视图模型上的属性的引用看起来是透明的。

public class PropertyChangedNotifierBase<T> : INotifyPropertyChanged
{
    readonly Dictionary<string, object> _properties = new Dictionary<string, object>();

    protected U GetValue<U>(Expression<Func<T, U>> property)
    {
        var propertyName = GetPropertyName(property);

        return GetValue<U>(propertyName);
    }

    private U GetValue<U>(string propertyName)
    {
        object value;

        if (!_properties.TryGetValue(propertyName, out value))
        {
            return default(U);
        }

        return (U)value;
    }

    protected void SetPropertyValue<U>(Expression<Func<T, U>> property, U value)
    {
        var propertyName = GetPropertyName(property);

        var oldValue = GetValue<U>(propertyName);

        if (Object.ReferenceEquals(oldValue, value))
        {
            return;
        }
        _properties[propertyName] = value;

        RaisePropertyChangedEvent(propertyName);
    }

    protected void RaisePropertyChangedEvent<U>(Expression<Func<T, U>> property)
    {
        var name = GetPropertyName(property);
        RaisePropertyChangedEvent(name);
    }

    protected void RaisePropertyChangedEvent(string propertyName)
    {
        if (PropertyChanged != null)
        {
            PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }
    }

    private static string GetPropertyName<U>(Expression<Func<T, U>> property)
    {
        if (property == null)
        {
            throw new NullReferenceException("property");
        }

        var lambda = property as LambdaExpression;

        var memberAssignment = (MemberExpression) lambda.Body;
        return memberAssignment.Member.Name;
    }

    public event PropertyChangedEventHandler PropertyChanged;
}

回答by kornman00

There's an edge case when it comes to Array.Length. While 'Length' is exposed as a property, you can't use it in any of the previously proposed solutions.

当涉及到Array.Length时,有一个边缘情况。虽然“长度”作为属性公开,但您不能在之前提出的任何解决方案中使用它。

using Contract = System.Diagnostics.Contracts.Contract;
using Exprs = System.Linq.Expressions;

static string PropertyNameFromMemberExpr(Exprs.MemberExpression expr)
{
    return expr.Member.Name;
}

static string PropertyNameFromUnaryExpr(Exprs.UnaryExpression expr)
{
    if (expr.NodeType == Exprs.ExpressionType.ArrayLength)
        return "Length";

    var mem_expr = expr.Operand as Exprs.MemberExpression;

    return PropertyNameFromMemberExpr(mem_expr);
}

static string PropertyNameFromLambdaExpr(Exprs.LambdaExpression expr)
{
         if (expr.Body is Exprs.MemberExpression)   return PropertyNameFromMemberExpr(expr.Body as Exprs.MemberExpression);
    else if (expr.Body is Exprs.UnaryExpression)    return PropertyNameFromUnaryExpr(expr.Body as Exprs.UnaryExpression);

    throw new NotSupportedException();
}

public static string PropertyNameFromExpr<TProp>(Exprs.Expression<Func<TProp>> expr)
{
    Contract.Requires<ArgumentNullException>(expr != null);
    Contract.Requires<ArgumentException>(expr.Body is Exprs.MemberExpression || expr.Body is Exprs.UnaryExpression);

    return PropertyNameFromLambdaExpr(expr);
}

public static string PropertyNameFromExpr<T, TProp>(Exprs.Expression<Func<T, TProp>> expr)
{
    Contract.Requires<ArgumentNullException>(expr != null);
    Contract.Requires<ArgumentException>(expr.Body is Exprs.MemberExpression || expr.Body is Exprs.UnaryExpression);

    return PropertyNameFromLambdaExpr(expr);
}

Now example usage:

现在示例用法:

int[] someArray = new int[1];
Console.WriteLine(PropertyNameFromExpr( () => someArray.Length ));

If PropertyNameFromUnaryExprdidn't check for ArrayLength, "someArray" would be printed to the console (compiler seems to generate direct access to the backing Length field, as an optimization, even in Debug, thus the special case).

如果PropertyNameFromUnaryExpr没有检查ArrayLength,“someArray”将打印到控制台(编译器似乎生成对支持 Length字段的直接访问,作为优化,即使在 Debug 中,因此是特殊情况)。

回答by Paul Fleming

public string GetName<TSource, TField>(Expression<Func<TSource, TField>> Field)
{
    return (Field.Body as MemberExpression ?? ((UnaryExpression)Field.Body).Operand as MemberExpression).Member.Name;
}

This handles member and unary expressions. The difference being that you will get a UnaryExpressionif your expression represents a value type whereas you will get a MemberExpressionif your expression represents a reference type. Everything can be cast to an object, but value types must be boxed. This is why the UnaryExpression exists. Reference.

这处理成员和一元表达式。不同之处在于,UnaryExpression如果您的表达式表示值类型,您将获得一个,而MemberExpression如果您的表达式表示一个引用类型,您将获得一个。一切都可以转换为对象,但值类型必须装箱。这就是 UnaryExpression 存在的原因。参考。

For the sakes of readability (@Jowen), here's an expanded equivalent:

为了可读性(@Jowen),这里有一个扩展的等价物:

public string GetName<TSource, TField>(Expression<Func<TSource, TField>> Field)
{
    if (object.Equals(Field, null))
    {
        throw new NullReferenceException("Field is required");
    }

    MemberExpression expr = null;

    if (Field.Body is MemberExpression)
    {
        expr = (MemberExpression)Field.Body;
    }
    else if (Field.Body is UnaryExpression)
    {
        expr = (MemberExpression)((UnaryExpression)Field.Body).Operand;
    }
    else
    {
        const string Format = "Expression '{0}' not supported.";
        string message = string.Format(Format, Field);

        throw new ArgumentException(message, "Field");
    }

    return expr.Member.Name;
}

回答by drzaus

I've found that some of the suggested answerswhich drill down into the MemberExpression/UnaryExpressiondon't capture nested/subproperties.

我发现一些深入到/的建议答案没有捕获嵌套/子属性。MemberExpressionUnaryExpression

ex) o => o.Thing1.Thing2returns Thing1rather than Thing1.Thing2.

ex)o => o.Thing1.Thing2返回Thing1而不是Thing1.Thing2.

This distinction is important if you're trying to work with EntityFramework DbSet.Include(...).

如果您尝试使用 EntityFramework ,则这种区别很重要DbSet.Include(...)

I've found that just parsing the Expression.ToString()seems to work fine, and comparatively quickly. I compared it against the UnaryExpressionversion, and even getting ToStringoff of the Member/UnaryExpressionto see if that was faster, but the difference was negligible. Please correct me if this is a terrible idea.

我发现只是解析Expression.ToString()似乎工作正常,而且相对较快。我将它与UnaryExpression版本进行了比较,甚至ToString下车Member/UnaryExpression看看它是否更快,但差异可以忽略不计。如果这是一个糟糕的主意,请纠正我。

The Extension Method

扩展方法

/// <summary>
/// Given an expression, extract the listed property name; similar to reflection but with familiar LINQ+lambdas.  Technique @via https://stackoverflow.com/a/16647343/1037948
/// </summary>
/// <remarks>Cheats and uses the tostring output -- Should consult performance differences</remarks>
/// <typeparam name="TModel">the model type to extract property names</typeparam>
/// <typeparam name="TValue">the value type of the expected property</typeparam>
/// <param name="propertySelector">expression that just selects a model property to be turned into a string</param>
/// <param name="delimiter">Expression toString delimiter to split from lambda param</param>
/// <param name="endTrim">Sometimes the Expression toString contains a method call, something like "Convert(x)", so we need to strip the closing part from the end</param>
/// <returns>indicated property name</returns>
public static string GetPropertyName<TModel, TValue>(this Expression<Func<TModel, TValue>> propertySelector, char delimiter = '.', char endTrim = ')') {

    var asString = propertySelector.ToString(); // gives you: "o => o.Whatever"
    var firstDelim = asString.IndexOf(delimiter); // make sure there is a beginning property indicator; the "." in "o.Whatever" -- this may not be necessary?

    return firstDelim < 0
        ? asString
        : asString.Substring(firstDelim+1).TrimEnd(endTrim);
}//--   fn  GetPropertyNameExtended

(Checking for the delimiter might even be overkill)

(检查分隔符甚至可能有点矫枉过正)

Demo (LinqPad)

演示 (LinqPad)

Demonstration + Comparison code -- https://gist.github.com/zaus/6992590

演示+对比代码——https://gist.github.com/zaus/6992590

回答by nawfal

This is a general implementation to get the string name of fields/properties/indexers/methods/extension methods/delegates of struct/class/interface/delegate/array. I have tested with combinations of static/instance and non-generic/generic variants.

这是获取 struct/class/interface/delegate/array 的字段/属性/索引器/方法/扩展方法/委托的字符串名称的通用实现。我已经测试了静态/实例和非通用/通用变体的组合。

//involves recursion
public static string GetMemberName(this LambdaExpression memberSelector)
{
    Func<Expression, string> nameSelector = null;  //recursive func
    nameSelector = e => //or move the entire thing to a separate recursive method
    {
        switch (e.NodeType)
        {
            case ExpressionType.Parameter:
                return ((ParameterExpression)e).Name;
            case ExpressionType.MemberAccess:
                return ((MemberExpression)e).Member.Name;
            case ExpressionType.Call:
                return ((MethodCallExpression)e).Method.Name;
            case ExpressionType.Convert:
            case ExpressionType.ConvertChecked:
                return nameSelector(((UnaryExpression)e).Operand);
            case ExpressionType.Invoke:
                return nameSelector(((InvocationExpression)e).Expression);
            case ExpressionType.ArrayLength:
                return "Length";
            default:
                throw new Exception("not a proper member selector");
        }
    };

    return nameSelector(memberSelector.Body);
}

This thing can be written in a simple whileloop too:

这件事也可以写成一个简单的while循环:

//iteration based
public static string GetMemberName(this LambdaExpression memberSelector)
{
    var currentExpression = memberSelector.Body;

    while (true)
    {
        switch (currentExpression.NodeType)
        {
            case ExpressionType.Parameter:
                return ((ParameterExpression)currentExpression).Name;
            case ExpressionType.MemberAccess:
                return ((MemberExpression)currentExpression).Member.Name;
            case ExpressionType.Call:
                return ((MethodCallExpression)currentExpression).Method.Name;
            case ExpressionType.Convert:
            case ExpressionType.ConvertChecked:
                currentExpression = ((UnaryExpression)currentExpression).Operand;
                break;
            case ExpressionType.Invoke:
                currentExpression = ((InvocationExpression)currentExpression).Expression;
                break;
            case ExpressionType.ArrayLength:
                return "Length";
            default:
                throw new Exception("not a proper member selector");
        }
    }
}

I like the recursive approach, though the second one might be easier to read. One can call it like:

我喜欢递归方法,尽管第二种方法可能更容易阅读。人们可以这样称呼它:

someExpr = x => x.Property.ExtensionMethod()[0]; //or
someExpr = x => Static.Method().Field; //or
someExpr = x => VoidMethod(); //or
someExpr = () => localVariable; //or
someExpr = x => x; //or
someExpr = x => (Type)x; //or
someExpr = () => Array[0].Delegate(null); //etc

string name = someExpr.GetMemberName();

to print the last member.

打印最后一个成员。

Note:

笔记:

  1. In case of chained expressions like A.B.C, "C" is returned.

  2. This doesn't work with consts, array indexers or enums (impossible to cover all cases).

  1. 在像 那样的链式表达式的情况下,A.B.C返回“C”。

  2. 这不适用于consts、数组索引器或enums(不可能涵盖所有情况)。