Is there a way to capture a lambda expression so that it's not compile-time forced to take on an identity as either an Expression or Delegate type?

你离开我真会死。 提交于 2019-12-23 17:11:26

问题


Suppose I have a complex lambda expression as follows:

x => x.A.HasValue || (x.B.HasValue && x.C == q) || (!x.C.HasValue && !x.A.HasValue) || //...expression goes on

I want to use this as an Expression<Func<T,bool> in (e.g. Linq-To-Entities) Queryable.Where method. I also want to use it in the Enumerable.Where method, but the Where method only accepts a Func<T,bool>, not an Expression<Func<T,bool>.

The lambda syntax itself can be used to generate either an Expression<Func<T,bool>> or a Func<T,bool> (or any delegate type for that matter), but in this context it cannot generate more than one at once.

For example, I can write:

public Expression<Func<Pair,bool>> PairMatchesExpression()
{
    return x => x.A == x.B;
}

as easily as I can write:

public Func<Pair,bool> PairMatchesDelegate()
{
    return x => x.A == x.B;
}

The problem is that I cannot use the same exact lambda expression (i.e. x => x.A == x.B) in both ways, without physically duplicating it into two separate methods with two different return types, in spite of the compiler's ability to compile it into either one.

In other words, if I'd like to use the lambda expression in the Queryable methods, then I have to use the Expression method signature. Once I do that however, I cannot use it as a Func as easily as I could have had I just declared the method return type as Func. Instead, I now have to call Compile on the Expression and then worry about caching the results manually like so:

static Func<Pair,bool> _cachedFunc;
public Func<Pair,bool> PairMatchesFunc()
{
    if (_cachedFunc == null)
        _cachedFunc = PairMatchesExpression().Compile();
    return _cachedFunc;
}

Is there a solution to this problem so that I can use the lambda expression in a more general way without it being locked down to a particular type at compile-time?


回答1:


Unfortunately, I can see no way to truly get, at compile time, a Func and an Expression from the same lambda. However, you could at least encapsulate away the difference, and you can also defer the compilation of the Func until the first time it's used. Here's a solution that makes the best of things and may meet your needs, even though it doesn't quite go all the way to what you really wanted (compile-time evaluation of both the Expression and the Func).

Please note that this works fine without using the [DelegateConstraint] attribute (from Fody.ExtraConstraints), but with it, you will get compile-time checking of the constructor parameter. The attributes make the classes act like they have a constraint where T : Delegate, which is not currently supported in C#, even though it is supported in the ILE (not sure if I'm saying that right, but you get the idea).

public class VersatileLambda<[DelegateConstraint] T> where T : class {
    private readonly Expression<T> _expression;
    private readonly Lazy<T> _funcLazy;

    public VersatileLambda(Expression<T> expression) {
        if (expression == null) {
            throw new ArgumentNullException(nameof(expression));
        }
        _expression = expression;
        _funcLazy = new Lazy<T>(expression.Compile);
    }

    public static implicit operator Expression<T>(VersatileLambda<T> lambda) {
        return lambda?._expression;
    }

    public static implicit operator T(VersatileLambda<T> lambda) {
        return lambda?._funcLazy.Value;
    }

    public Expression<T> AsExpression() { return this; }
    public T AsLambda() { return this; }
}

public class WhereConstraint<[DelegateConstraint] T> : VersatileLambda<Func<T, bool>> {
    public WhereConstraint(Expression<Func<T, bool>> lambda)
        : base(lambda) { }
}

The beauty of the implicit conversion is that in contexts where a specific Expression<Func<>> or Func<> is expected, you don't have to do anything at all, just, use it.

Now, given an object:

public partial class MyObject {
    public int Value { get; set; }
}

That is represented in the database like so:

CREATE TABLE dbo.MyObjects (
    Value int NOT NULL CONSTRAINT PK_MyObjects PRIMARY KEY CLUSTERED
);

Then it works like this:

var greaterThan5 = new WhereConstraint<MyObject>(o => o.Value > 5);

// Linq to Objects
List<MyObject> list = GetObjectsList();
var filteredList = list.Where(greaterThan5).ToList(); // no special handling

// Linq to Entities
IQueryable<MyObject> myObjects = new MyObjectsContext().MyObjects;
var filteredList2 = myObjects.Where(greaterThan5).ToList(); // no special handling

If implicit conversion isn't suitable, you can cast explicitly to the target type:

var expression = (Expression<Func<MyObject, bool>>) greaterThan5;

Note that you don't really need the WhereConstraint class, or you could get rid of VersatileLambda by moving its contents to WhereConstraint, but I liked making the two separate (as now you can use VersatileLambda for something that returns other than a bool). (And this difference is largely what sets apart my answer from Diego's.) Using VersatileLambda as it is now looks like this (you can see why I wrapped it):

var vl = new VersatileLambda<Func<MyObject, bool>>(o => o.Value > 5);

I have confirmed that this works perfectly for IEnumerable as well as IQueryable, properly projecting the lambda expression into the SQL, as proven by running SQL Profiler.

Also, you can do some really cool things with expressions that can't be done with lambdas. Check this out:

public static class ExpressionHelper {
    public static Expression<Func<TFrom, TTo>> Chain<TFrom, TMiddle, TTo>(
        this Expression<Func<TFrom, TMiddle>> first,
        Expression<Func<TMiddle, TTo>> second
    ) {
        return Expression.Lambda<Func<TFrom, TTo>>(
           new SwapVisitor(second.Parameters[0], first.Body).Visit(second.Body),
           first.Parameters
        );
    }

    // this method thanks to Marc Gravell   
    private class SwapVisitor : ExpressionVisitor {
        private readonly Expression _from;
        private readonly Expression _to;

        public SwapVisitor(Expression from, Expression to) {
            _from = from;
            _to = to;
        }

        public override Expression Visit(Expression node) {
            return node == _from ? _to : base.Visit(node);
        }
    }
}

var valueSelector = new Expression<Func<MyTable, int>>(o => o.Value);
var intSelector = new Expression<Func<int, bool>>(x => x > 5);
var selector = valueSelector.Chain<MyTable, int, bool>(intSelector);

You can create an overload of Chain that takes a VersatileLambda as the first parameter, and returns a VersatileLambda. Now you're really sizzling along.




回答2:


You could create a wrapper class. Something like this:

public class FuncExtensionWrap<T>
{
    private readonly Expression<Func<T, bool>> exp;
    private readonly Func<T, bool> func;

    public FuncExtensionWrap(Expression<Func<T, bool>> exp)
    {
        this.exp = exp;
        this.func = exp.Compile();
    }

    public Expression<Func<T, bool>> AsExp()
    {
        return this;
    }

    public Func<T, bool> AsFunc()
    {
        return this;
    }

    public static implicit operator Expression<Func<T, bool>>(FuncExtensionWrap<T> w)
    {
        if (w == null)
            return null;
        return w.exp;
    }

    public static implicit operator Func<T, bool>(FuncExtensionWrap<T> w)
    {
        if (w == null)
            return null;
        return w.func;
    }
}

And then it would be used like this:

static readonly FuncExtensionWrap<int> expWrap = new FuncExtensionWrap<int>(i => i == 2);

// As expression
Expression<Func<int, bool>> exp = expWrap;
Console.WriteLine(exp.Compile()(2));

// As expression (another way)
Console.WriteLine(expWrap.AsExp().Compile()(2));

// As function
Func<int, bool> func = expWrap;
Console.WriteLine(func(1));

// As function(another way)
Console.WriteLine(expWrap.AsFunc()(2));



回答3:


Here is one workaround. It generates an explicit class for the expression (as the compiler would do under the hood anyway with lambda expressions that require a function closure) instead of just a method, and it compiles the expression in a static constructor so it doesn't have any race conditions that could result in multiple compilations. This workaround still incurs an additional runtime delay as a result of the Compile call which could otherwise be offloaded to build-time, but at least it's guaranteed to run only once using this pattern.

Given a type to be used in the expression:

public class SomeClass
{
    public int A { get; set; }
    public int? B { get; set; }
}

Build an inner class instead of a method, naming it whatever you would have named the method:

static class SomeClassMeetsConditionName
{
    private static Expression<Func<SomeClass,bool>> _expression;
    private static Func<SomeClass,bool> _delegate;
    static SomeClassMeetsConditionName()
    {
        _expression = x => (x.A > 3 && !x.B.HasValue) || (x.B.HasValue && x.B.Value > 5);
        _delegate = _expression.Compile();
    }
    public static Expression<Func<SomeClass, bool>> Expression { get { return _expression; } }
    public static Func<SomeClass, bool> Delegate { get { return _delegate; } }
}

Then instead of using Where( SomeClassMeetsConditionName() ), you simply pass SomeClassMeetsConditionName followed by either .Delegate or .Expression, depending on the context:

public void Test()
{
    IEnumerable<SomeClass> list = GetList();
    IQueryable<SomeClass> repo = GetQuery();

    var r0 = list.Where( SomeClassMeetsConditionName.Delegate );
    var r1 = repo.Where( SomeClassMeetsConditionName.Expression );
}

As an inner class, it could be given an access level just like a method and accessed just like a method, and even collapsed all at once like a method, so if you can stand to look at the class instead of a method, this is a functional workaround. It could even be made into a code template.



来源:https://stackoverflow.com/questions/33399863/is-there-a-way-to-capture-a-lambda-expression-so-that-its-not-compile-time-forc

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!