How to get "real"/"inner" Func delegate from lambda delegate

Viewed 54

What I'm trying to achieve

I'm trying to implement a method that compares two delegates for equality.
Ideally I dont want to have to specify the optional parameters while the compiler could figure them out.
So it would be nice if I could call it like shown here:

    public class Foo { public double Get(double d = 0.0) { return d; } }

    [TestMethod]
    public void TestMethod()
    {
        var foo = new Foo();
        bool equal = CompareDelegates(() => foo.Get(), () => foo.Get());
        // equal will be false here
    }

    private bool CompareDelegates(Func<double> d1, Func<double> d2)
    {
        return d1 == d2;
    }

With this implementation CompareDelegates will always return false...

What I already learned while researching into this

  • A delgate can be uniquely identified by its combination of Target and Method properties, so intuitively one would assume that the above example returns true.
  • The reason it doesn't is that the compiler creates an anonymous class when evaluating the lambdas. This class holds a field with a reference to foo and one method for each lambda. That leads to the two delegates having different Method properties which causes the equality test to fail.

The actual question

So my question is: Is there any chance to access the "actual" Target and Method of the lambdas?
With "actual" meaning the ones boxed inside the anonymous class. Thus being able to compare Target and Method.

What I already have

With the help of this I already made it work with the CompareDelegates taking the lambdas as Expression<Func<double>>.
Downside: The consumer would have to specify all the optional parameters, which I would like to avoid.
For reference (hoping it helps people with a similar problem that come across this post):

    private bool CompareDelegates(Expression<Func<double>> d1, Expression<Func<double>> d2)
    {
        var actual1 = GetActualTargetAndMethod(d1);
        var actual2 = GetActualTargetAndMethod(d2);
    
        return actual1.method == actual2.method && actual1.target == actual2.target;
        // return actual1 == actual2; // as of C#7.3
    }
    
    private (object target, MethodInfo method) GetActualTargetAndMethod(Expression<Func<double>> expression)
    {
        // The expression is a lambda expression with a method call body.
        var lambda = (LambdaExpression)expression;
        var methodCall = (MethodCallExpression)lambda.Body;
        // The method is called on a member of some instance.
        var member = (MemberExpression)methodCall.Object;
        // The member expression contains an instance of the anonymous class that
        // defines the member...
        var constant = (ConstantExpression)member.Expression;
        var anonymousClassInstance = constant.Value;
        // ...and the member itself.
        var calledClassField = (FieldInfo)member.Member;
        // With an instance of the anonymous class and the field, we can get its value.
        return (calledClassField.GetValue(anonymousClassInstance), methodCall.Method);
    }

And of course you could just do:

    private bool CompareDelegates(Func<double, double> d1, Func<double, double> d2)
    {
        return d1 == d2;
    }
    // being called like: equal = CompareDelegates(foo.Get, foo.Get);

Which looks nice and works with the optional parameters as well.
Downside: You'll have to write an overload for all optional parameters there may be.

Any suggestions would be appreciated.

1 Answers

After a while now I found a way which feels like a workaround, but finally worked for me. Still interested in any different approaches though!

private bool CompareDelegates<T>(T d1, T d2) where T : Delegate
{
    return d1 == d2;
}

Usage:

bool equal = CompareDelegates<Func<double,double>>(foo.Get, foo.Get); // returns true

With this solution you don't have to specify optional parameters and it works for any number of parameters that Foo.Get(...) might have. Unfortunately the compiler is not able to infer the type argument T by itself.

Note: where T : Delegate is available as of C#7.3, but seems to be possible for earlier versions as well.

Related