为什么我不能将表达式体转换为MethodCallExpression?

3

给定以下类:

public class MyClass {

    private readonly UrlHelper _urlHelper;

    // constructor left out for brevity

    // this is one of many overloaded methods
    public ILinkableAction ForController<TController, T1, T2>(Expression<Func<TController, Func<T1, T2>>> expression) {
        return ForControllerImplementation(expression);
    }

    private ILinkableAction ForControllerImplementation<TController, TDelegate>(Expression<Func<TController, TDelegate>> expression) {
        var linkableMethod = new LinkableAction(_urlHelper);

        var method = ((MethodCallExpression) expression.Body).Method;
        method.GetParameters().ToList().ForEach(p => linkableMethod.parameters.Add(new Parameter {
            name = p.Name,
            parameterInfo = p
        }));

        return linkableMethod;
    }
}

以及以下实现:

var myClass = new MyClass(urlHelper);
myClass.ForController<EventsController, int, IEnumerable<EventDto>>(c => c.GetEventsById);

其中GetEventsById具有以下签名:

IEnumerable<EventDto> GetEventsById(int id);

我遇到了以下错误:
无法将类型为'System.Linq.Expressions.UnaryExpression'的对象强制转换为类型'System.Linq.Expressions.MethodCallExpression'。
有以下两个问题需要解决:
1.如何将表达式转换为适当的类型以获取给定表达式的MethodInfo?
2.在上面的示例中,TDelegate在运行时是“Func<int,IEnumerable<EventDto>>”,因为它是一个委托,为什么我不能从表达式中获取MethodInfo?

请看我的回答。问题在于您的委托签名不匹配。因此,表达式主体被表示为一元(转换)。 - Paul Easter
2个回答

8
问题在于 MethodCallExpression 必须实际上是一个方法。考虑以下代码:
public static void Main()
{
    Express(str => str.Length);
    Console.ReadLine();
}


static void Express(Expression<Func<String, Int32>> expression)
{
    // Outputs: PropertyExpression (Which is a form of member expression)
    Console.WriteLine(expression.Body.GetType()); 
    Console.ReadLine();
}

表达式在编译时确定,这意味着当我使用str => str.Length时,我在调用str的一个属性,因此编译器会将其解析为一个MemberExpression
如果我将我的 lambda 改成这个样子:
Express(str => str.Count());

然后编译器会理解我在对字符串对象 str 调用 Count() 方法,因此它会将其解析为一个 MethodCallExpression ... 因为实际上它是一个方法。
注意,这意味着你不能真正地将一个表达式从一种类型转换为另一种类型,就像你不能将一个 String 转换为 Int32。你可以进行解析,但我想你明白这并不真正是一种转换...
...话虽如此,你可以从头开始创建一个 MethodCallExpression,这在某些情况下非常有用。例如,让我们来构建一个 lambda 表达式:
(str, startsWith) => str.StartsWith(startsWith)


(1) 首先,我们需要开始构建两个参数:(str, startsWith) => ...

// The first parameter is type "String", and well call it "str"
// The second parameter also type "String", and well call it "startsWith"
ParameterExpression str = Expression.Parameter(typeof(String), "str");
ParameterExpression startsWith = Expression.Parameter(typeof(String), "startsWith");


(2) 然后,在右侧,我们需要构建:str.StartsWith(startsWith)。首先,我们需要使用反射来绑定到 StringStartsWith(...) 方法,该方法需要一个类型为 String 的单个输入参数,如下所示:

// Get the method metadata for "StartsWith" -- the version that takes a single "String" input.
MethodInfo startsWithMethod = typeof(String).GetMethod("StartsWith", new [] { typeof(String) });


(3) 现在我们有了绑定元数据,我们可以使用 MethodCallExpression 来实际调用方法,像这样:

//This is the same as (...) => str.StartsWith(startsWith);
// That is: Call the method pointed to by "startsWithMethod" bound above. Make sure to call it
// on 'str', and then use 'startsWith' (defined above as well) as the input.
MethodCallExpression callStartsWith = Expression.Call(str, startsWithMethod, new Expression[] { startsWith });


(4) 现在我们有了左侧的 (str, startsWith) 和右侧的 str.StartsWith(startsWith)。现在我们只需要将它们合并成一个 lambda 表达式。最终代码:

// The first parameter is type "String", and well call it "str"
// The second parameter also type "String", and well call it "startsWith"
ParameterExpression str = Expression.Parameter(typeof(String), "str");
ParameterExpression startsWith = Expression.Parameter(typeof(String), "startsWith");

// Get the method metadata for "StartsWith" -- the version that takes a single "String" input.
MethodInfo startsWithMethod = typeof(String).GetMethod("StartsWith", new[] { typeof(String) });

// This is the same as (...) => str.StartsWith(startsWith);
// That is: Call the method pointed to by "startsWithMethod" bound above. Make sure to call it
// on 'str', and then use 'startsWith' (defined above as well) as the input.
MethodCallExpression callStartsWith = Expression.Call(str, startsWithMethod, new Expression[] { startsWith });

// This means, convert the "callStartsWith" lambda-expression (with two Parameters: 'str' and 'startsWith', into an expression
// of type Expression<Func<String, String, Boolean>
Expression<Func<String, String, Boolean>> finalExpression =
    Expression.Lambda<Func<String, String,  Boolean>>(callStartsWith, new ParameterExpression[] { str, startsWith });

// Now let's compile it for extra speed!
Func<String, String, Boolean> compiledExpression = finalExpression.Compile();

// Let's try it out on "The quick brown fox" (str) and "The quick" (startsWith)
Console.WriteLine(compiledExpression("The quick brown fox", "The quick")); // Outputs: "True"
Console.WriteLine(compiledExpression("The quick brown fox", "A quick")); // Outputs: "False"

更新 好的,也许这样做会有所帮助:

class Program
{
        public void DoAction()
        {
            Console.WriteLine("actioned");
        }

        public delegate void ActionDoer();

        public void Do()
        {
            Console.ReadLine();
        }

        public static void Express(Expression<Func<Program, ActionDoer>> expression)
        {
            Program program = new Program();
            Func<Program, ActionDoer> function = expression.Compile();
            function(program).Invoke();
        }

        [STAThread]
        public static void Main()
        {
            Express(program => program.DoAction);
            Console.ReadLine();
        }
}

更新: 无意中发现了一些内容。考虑以下代码:
    public static String SetPropertyChanged<T>(Expression<Func<T, Object>> expression)
    {
        UnaryExpression convertExpression = (UnaryExpression)expression.Body;
        MemberExpression memberExpression = (MemberExpression)convertExpression.Operand;
        return memberExpression.Member.Name;

        ...
    }

输入是一个简单的 WPF Lambda 表达式:
base.SetPropertyChanged(x => x.Visibility);

由于我正在将其投影到一个Object中,我注意到Visual Studio将其转换为UnaryExpression,我认为这是你遇到的相同问题。如果您设置断点并检查实际表达式(在我的情况下),它会说x => Convert(x.Visibility)。问题在于Convert(实际上只是转换为当前未知类型的强制转换)。您所要做的就是删除它(如我在上面的代码中使用Operand成员所示),然后您就可以全部解决了。也许你会有你的MethodCallExpression


我得说,这个解释非常好,能够很好地说明为什么我会出现错误。非常感谢你的帮助。 - bflemi3
1
我猜你是想这样做,所以你需要使用反射来绑定到实际的方法,然后构造一个MethodCallExpression/自己编译。示例展示了如何完成此操作。跟着示例做一遍,之后就会很容易理解如何做了。 - sircodesalot
如果它是一个委托,确保将方法标记为where TDelegate : Delegate - sircodesalot
是的,即使您传递了委托,该委托本身也不知道要指向什么,因此最终您将不得不将其指向某个方法...这将使我们回到起点。本质上,使用lambda传递的任何“名称”都必须手动绑定到实际的MethodInfo,然后构建为MethodCallExpression / Compiled。基本上,这是因为实际上您没有传递方法,因此需要自行设置绑定。 - sircodesalot
1
通读这篇答案后,非常出色。再次感谢您抽出时间与我一起阅读! - bflemi3
显示剩余12条评论

2
我认为答案比所有这些都简单。问题在于你的lambda表达式签名如下:
Expression<Func<TController, Func<T1, T2>>> expression

你的委托签名是:
Func<TController, Func<T1, T2>>
or
Func<T1, T2> function(TController controller){}

该函数接受一个TController作为输入并返回一个委托(Func<T1, T2>);而您传递的实现lambda表达式(c => c.GetEventsById)的签名为:

Expression<Func<TController, T1>> expression

因此,您编译的 lambda 委托签名为:

Func<EventsController, int> 
or
int function(EventsController controller){}

因为它表示委托转换,所以在主体中得到了UnaryExpression(如果尝试编译/调用它 -> Expression.Compile().Invoke()可能会引发异常)。使签名匹配,您的表达式主体将成为methodCallExpression。


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