Распознать индексатор в выражении 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
}
}
}
Таким образом, в основном, вместо того, чтобы полагаться на индексатор, который будет назван определенным образом, я полагаюсь на следующее:
- Два объекта
MethodInfo
можно сравнить с равенством (или неравенством) (operator ==
а такжеoperator !=
оба реализованы дляMethodInfo
). - У индексатора есть метод get. В общем случае свойство может не иметь метода get, но такое свойство нельзя использовать для создания выражения с лямбда-синтаксисом.
- Доступ к неиндексированным свойствам
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);
// ...
Надеюсь, что это поможет кому-то.