假设我有一个复杂的lambda表达式,如下所示:
x => x.A.HasValue || (x.B.HasValue && x.C == q) || (!x.C.HasValue && !x.A.HasValue) || //...expression goes on
我希望将其用作Expression<Func<T,bool>
(例如Linq-To-Entities)Queryable.Where方法。我还想在Enumerable.Where方法中使用它,但Where方法只接受Func<T,bool>
,而不是Expression<Func<T,bool>
。
lambda语法本身可用于生成 Expression<Func<T,bool>>
或Func<T,bool>
(或任何委托类型),但在此上下文中它无法生成不止一次。
例如,我可以写:
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 =&gt; xA == xB),而不是将它物理地复制到具有两种不同返回类型的两个单独方法中尽管编译器能够将其编译成任何一个。
换句话说,如果我想在Queryable
方法中使用lambda表达式,那么我必须使用Expression
方法签名。但是,当我这样做时,我不能像我一样轻松地将它用作Func
,我只是将方法返回类型声明为Func
。相反,我现在必须在Compile
上调用Expression
,然后担心手动缓存结果:
static Func<Pair,bool> _cachedFunc;
public Func<Pair,bool> PairMatchesFunc()
{
if (_cachedFunc == null)
_cachedFunc = PairMatchesExpression().Compile();
return _cachedFunc;
}
是否有解决此问题的方法,以便我可以更通用的方式使用lambda表达式,而不会在编译时将其锁定为特定类型?
答案 0 :(得分:1)
您可以创建一个包装类。像这样:
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));
答案 1 :(得分:1)
不幸的是,我无法在编译时从同一个lambda中获得Func
和Expression
。但是,您至少可以将差异封装起来,并且您也可以将Func
的编译推迟到第一次使用时。这是一个能够充分利用并满足您需求的解决方案,即使它并不能完全满足您的需求(Expression
和Func
的编译时评估[DelegateConstraint]
)。
请注意,使用where T : Delegate
属性(来自Fody.ExtraConstraints),没有可以正常工作,但是使用它,您将获得构造函数参数的编译时检查。这些属性使得这些类的行为类似于它们具有约束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) { }
}
,即使在ILE中 支持
Expression<Func<>>
隐式转换的优点在于,在预期特定Func<>
或public partial class MyObject {
public int Value { get; set; }
}
的情境中,您根本不需要做任何事情,只需使用< / em>它。
现在,给定一个对象:
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
请注意,您
但我喜欢将两者分开(因为现在你可以使用VersatileLambda
来获得除WhereConstraint
之外的其他内容。 (而这种差异在很大程度上解决了我对迭戈的回答。)使用VersatileLambda
现在看起来像这样(你可以看看为什么我把它包裹起来):
bool
我已经确认这适用于VersatileLambda
以及var vl = new VersatileLambda<Func<MyObject, bool>>(o => o.Value > 5);
,正确地将lambda表达式投影到SQL中,正如运行SQL事件探查器所证明的那样。
另外,你可以用一些无法用lambdas完成的表达式做一些非常酷的事情。看看这个:
IEnumerable
您可以创建IQueryable
的重载,以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
。现在你真的很开心了。
答案 2 :(得分:-2)
这是一个解决方法。它为表达式生成一个显式类(因为编译器无论如何都会使用需要函数闭包的lambda表达式),而不仅仅是一个方法,它在静态构造函数中编译表达式,因此它没有任何竞争可能导致多次汇编的条件。这种解决方法仍会因Compile调用而导致额外的运行时延迟,否则可以将其卸载到构建时,但至少可以保证只使用此模式运行一次。
给出表达式中使用的类型:
public class SomeClass
{
public int A { get; set; }
public int? B { get; set; }
}
构建一个内部类而不是一个方法,将它命名为方法的任何名称:
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 );
}
作为一个内部类,它可以被赋予一个访问级别,就像一个方法一样,就像一个方法一样被访问,甚至像一个方法一样折叠一下,所以如果你能站起来看一下这个类而不是一个方法,这是一个功能性的解决方法。它甚至可以被制作成代码模板。