Распознать индексатор в выражении LINQ

Мне нужно программно распознать, когда индексатор встречается в выражении, но результирующее дерево выражений не соответствует ожиданиям.

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 возможно, в любом случае использовался для переименования свойства индексатора.

Так есть ли способ заставить компилятор генерировать заданное дерево выражений? Пожалуйста, не предлагайте вручную строить выражение, так как это не вариант. Или есть какой-то способ программно убедиться, что у меня есть индексатор?

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

However, all is not lost: You can write a Visitor to try to stuff the get_Item call back into an IndexExpression, I started on it here:

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

You can then safely modify your Test method as so:

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
}

Недавно я столкнулся с той же проблемой и в итоге нашел следующее решение (на вашем примере):

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, но такое свойство нельзя использовать для создания выражения с лямбда-синтаксисом.
  3. Доступ к неиндексированным свойствам MemberExpression вместо MethodCallExpression (и даже если бы не было, PropertyInfo представляющее простое свойство всегда можно отличить от одного представляющего индексатора с помощью метода GetIndexParameters, поскольку все индексаторы имеют хотя бы один параметр).

Этот подход также работает, если множественные перегрузки индексатора exst в классе, так как каждый из них имеет свои собственные MethodInfo и только один будет равен тому, который используется в выражении.

Примечание. Приведенный выше метод не будет работать ни с закрытым индексатором, ни с индексатором с закрытым методом get. Для обобщения подхода следует использовать правильные перегрузки GetProperties а также GetGetMethod:

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

Надеюсь, что это поможет кому-то.

Другие вопросы по тегам