有没有办法捕获 lambda 表达式,这样它就不会在编译时被迫采用表达式或委托类型的身份?

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?

假设我有一个复杂的 lambda 表达式如下:

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

我想在(例如 Linq-To-Entities)Queryable.Where method. I also want to use it in the Enumerable.Where 方法中将其用作 Expression<Func<T,bool>,但 Where 方法只接受 Func<T,bool>,不接受 Expression<Func<T,bool>.

lambda 语法本身可用于生成 Expression<Func<T,bool>>Func<T,bool>(或与此相关的任何委托类型),但在this context 它不能一次生成多个。

比如我可以这样写:

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

尽可能简单地写:

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

问题是 我不能 在两种方式中使用完全相同的 lambda 表达式(即 x => x.A == x.B),而不尽管编译器能够将其编译为任何一种,但仍将其物理复制为具有两种不同 return 类型的两个独立方法。

换句话说,如果我想在 Queryable 方法中使用 lambda 表达式,那么我必须使用 Expression 方法签名。但是,一旦我这样做了,我就不能将它用作 Func 就像我刚刚将方法 return 类型声明为 Func.相反,我现在必须在 Expression 上调用 Compile,然后像这样手动缓存结果:

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

是否有解决此问题的方法,以便我可以以更通用的方式使用 lambda 表达式,而不会在编译时将其锁定为特定类型?

这是一种解决方法。它为表达式生成一个显式的 class (因为编译器无论如何都会对需要函数闭包的 lambda 表达式进行处理),而不仅仅是一个方法,并且它在静态构造函数中编译表达式,因此它不会没有任何可能导致多次编译的竞争条件。由于编译调用,此解决方法仍然会导致额外的 运行 时间延迟,否则可能会卸载到构建时,但至少可以保证 运行 只有一次使用此模式。

给定一个要在表达式中使用的类型:

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

构建一个内部 class 而不是一个方法,将其命名为您可以命名的方法:

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

然后不使用 Where( SomeClassMeetsConditionName() ),您只需传递 SomeClassMeetsConditionName 后跟 .Delegate.Expression,具体取决于上下文:

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

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

作为一个内在class,可以像方法一样被赋予访问级别,像方法一样被访问,甚至像方法一样一下子崩溃,所以如果你能站着看class 而不是方法,这是一种功能性解决方法。甚至可以做成代码模板。

您可以创建一个包装器 class。像这样:

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

然后会这样使用:

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

不幸的是,我看不出有什么办法可以在编译时从同一个 lambda 中真正获得 FuncExpression。但是,您至少可以封装掉差异,还可以将 Func 的编译推迟到第一次使用时。这是一个充分利用事物并可能满足您需求的解决方案,即使它并没有完全满足您真正想要的(Expression 和 [=17= 的编译时评估) ]).

请注意,如果不使用 使用 [DelegateConstraint] 属性(来自 Fody.ExtraConstraints),这可以正常工作,但是使用它,您将进行编译时检查的构造函数参数。这些属性使 classes 表现得像它们具有约束 where T : Delegate,目前 C# 不支持它,即使 ILE 支持它(不是确定我说的是否正确,但你明白了)。

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

隐式转换的美妙之处在于,在需要特定 Expression<Func<>>Func<> 的上下文中,您根本不需要做任何事情,只需 使用它。

现在,给定一个对象:

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

在数据库中是这样表示的:

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

然后它是这样工作的:

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

如果隐式转换不合适,您可以显式转换为目标类型:

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

请注意,您 真的 不需要 WhereConstraint class,或者您可以通过移动其内容来删除 VersatileLambdaWhereConstraint,但我喜欢将两者分开(因为现在您可以将 VersatileLambda 用于 returns 而不是 bool 的东西)。 (这种差异在很大程度上是我的回答与 Diego 的不同之处。)使用 VersatileLambda 现在看起来像这样(你可以看到我为什么包装它):

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

我已经确认这对 IEnumerableIQueryable 都非常有效,正确地将 lambda 表达式投影到 SQL,正如 运行 SQL 分析器。

此外,您可以使用 lambda 无法完成的表达式来做一些非常酷的事情。看看这个:

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

您可以创建 Chain 的重载,将 VersatileLambda 作为第一个参数,returns 作为 VersatileLambda。现在你真火了。