【问题标题】:Recognize indexer in LINQ expression识别 LINQ 表达式中的索引器
【发布时间】:2018-12-14 22:05:53
【问题描述】:

我需要以编程方式识别索引器何时出现在表达式中,但生成的表达式树不是我所期望的。

class IndexedPropertiesTest
{
    static void Main( string[] args ) { new IndexedPropertiesTest(); }

    public string this[int index]
    {
        get { return list[index]; }
        set { list[index] = value; }
    }
    List<string> list = new List<string>();

    public IndexedPropertiesTest()
    {
        Test( () => this[0] );
    }

    void Test( Expression<Func<string>> expression )
    {
        var nodeType = expression.Body.NodeType;
        var methodName = ((MethodCallExpression)expression.Body).Method.Name;
    }
}

在上面的代码中,nodeType 是“调用”,methodName 是“get_Item”。为什么? expression.Body 不应该等同于Expression.Property( Expression.Constant( this ), "Item", Expression.Constant( 0 ) ) 吗?这正是我所期望的。

我需要能够以非常通用的方式检测索引器 - 几乎可以给出任何表达式。这种对预期表达树的破坏损害了我这样做的能力。依赖方法名称为“get_Item”太脆弱了。另外,IndexerNameAttribute 可能已被用于重命名索引器属性。

那么有没有办法让编译器生成预期的表达式树?请不要建议手动构建表达式,因为这不是一个选项。或者有什么方法可以以编程方式确保我拥有的是索引器?

【问题讨论】:

  • I need to programmatically recognize when an indexer occurs within an expression 你确定吗?因为并非所有 .NET 语言都具有称为索引器的构造。这就是在内部创建方法 get_Item 的原因。如果从 VB.NET 调用您的代码,您会期望发生什么?
  • @nvoigt VB.NET 有 多个 索引器,我希望我的代码也能识别它们。那么对于确实有索引器的语言,为什么编译器不生成IndexExpression?缺少索引器的语言对这个问题有多重要?我认为重要的是底层反射模型包括索引属性,如 Property.GetValue 所示。

标签: c# linq-expressions indexed-properties


【解决方案1】:

我想你有你的答案:这就是 C# 编译器的工作方式。

我将您的代码翻译成 VB.NET。毫无帮助的是,VB.NET 并没有调用方法get_Item,而是通过你给它的名字来调用它。在下面的示例中,以 get_MyDefaultProperty 结尾。

Sub Main
    Dim x as IndexedPropertiesTest = New IndexedPropertiesTest()

End Sub

' Define other methods and classes here
Class IndexedPropertiesTest

    Private list as New List(Of String) From { "a" }
    Default Property MyDefaultProperty(index as Integer) as String
        Get
            Return list(index)
        End Get
        Set(value as String)
            list(index) = value
        End Set
    End Property

    Public Sub New
        Test( Function() Me(0))
    End Sub

    Public Sub Test(expression as Expression(Of Func(Of String)))

        Dim nodeType as ExpressionType = expression.Body.NodeType
        Dim methodName as String = CType(expression.Body, MethodCallExpression).Method.Name

        'expression.Dump() 'Using LINQPad

    End Sub

End Class

但是,一切都没有丢失:您可以编写一个访问者来尝试将 get_Item 回调填充到 IndexExpression 中。我从这里开始:

public class PropertyFixerVisitor : ExpressionVisitor
{
    protected override Expression VisitMethodCall(MethodCallExpression node)
    {
        if (node.Method.Name.StartsWith("get_"))
        {
            var possibleProperty = node.Method.Name.Substring(4);
            var properties = node.Method.DeclaringType.GetProperties()
                .Where(p => p.Name == possibleProperty);

            //HACK: need to filter out for overriden properties, multiple parameter choices, etc.
            var property = properties.FirstOrDefault();
            if (property != null)
                return Expression.Property(node.Object, possibleProperty, node.Arguments.ToArray());
            return base.VisitMethodCall(node);

        }
        else
            return base.VisitMethodCall(node);
    }
}

然后您可以安全地修改您的测试方法:

void Test(Expression<Func<string>> expression)
{
    var visitor = new PropertyFixerVisitor();
    var modExpr = (Expression<Func<string>>)visitor.Visit(expression);

    var indexExpression = (modExpr.Body as IndexExpression); //Not Null
}

【讨论】:

  • 您的Visitor 代码帮助我走上了克服问题的轨道。唯一缺少的是对编译器为何破坏索引器的解释。但我想只有微软才能回答这个问题。
  • 坦率地说,我自己也很好奇。我看了看,this answer 出现了一条线索:IndexExpression 在 .NET 3.5 中不存在,它是添加到 4.0 的众多 Expression 类之一。这些添加的表达式类(如AssignExpression)从未集成到编译器中。例如,您不能在表达式中使用赋值,您必须使用表达式类来构建它。
【解决方案2】:

我最近遇到了同样的问题,最终得到了以下解决方案(以您的示例为例):

void Test(Expression<Func<string>> expression)
{
    if (expression.Body.NodeType == ExpressionType.Call)
    {
        var callExpression = (MethodCallExpression)expression.Body;
        var getMethod = callExpression.Method;
        var indexer = getMethod.DeclaringType.GetProperties()
            .FirstOrDefault(p => p.GetGetMethod() == getMethod);
        if (indexer == null)
        {
            // Not indexer access
        }
        else
        {
            // indexer is a PropertyInfo accessed by expression
        }
    }
}

因此,基本上,我不依赖索引器以特定方式命名,而是依赖以下内容:

  1. MethodInfo 的两个对象可以比较为相等(或不相等)(operator ==operator != 均针对 MethodInfo 实现)。
  2. 索引器有一个 get 方法。在一般情况下,一个属性可能没有 get 方法,但这样的属性首先不能用于构造具有 lambda 语法的表达式。
  3. 访问非索引器属性会产生MemberExpression 而不是MethodCallExpression(即使它不会,代表简单属性的PropertyInfo 始终可以与使用 GetIndexParameters 方法的代表索引器区分开来,因为所有索引器都有至少一个参数)。

如果一个类中存在多个索引器重载,则此方法也适用,因为它们中的每一个都有自己的MethodInfo,并且只有一个等于表达式中使用的那个。

注意:上述方法不适用于私有索引器,也不适用于具有私有 get 方法的索引器。为了概括该方法,应该使用GetPropertiesGetGetMethod 的正确重载:

// ...
var indexer = getMethod.DeclaringType.GetProperties(
        BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)
    .FirstOrDefault(p => p.GetGetMethod(nonPublic: true) == getMethod);
// ...

希望对某人有所帮助。

【讨论】:

    猜你喜欢
    • 2021-10-11
    • 2019-08-03
    • 2010-09-07
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-01-11
    • 2021-06-05
    相关资源
    最近更新 更多