有没有办法捕获 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 中真正获得 Func
和 Expression
。但是,您至少可以封装掉差异,还可以将 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,或者您可以通过移动其内容来删除 VersatileLambda
到 WhereConstraint
,但我喜欢将两者分开(因为现在您可以将 VersatileLambda
用于 returns 而不是 bool
的东西)。 (这种差异在很大程度上是我的回答与 Diego 的不同之处。)使用 VersatileLambda
现在看起来像这样(你可以看到我为什么包装它):
var vl = new VersatileLambda<Func<MyObject, bool>>(o => o.Value > 5);
我已经确认这对 IEnumerable
和 IQueryable
都非常有效,正确地将 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
。现在你真火了。
假设我有一个复杂的 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 中真正获得 Func
和 Expression
。但是,您至少可以封装掉差异,还可以将 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,或者您可以通过移动其内容来删除 VersatileLambda
到 WhereConstraint
,但我喜欢将两者分开(因为现在您可以将 VersatileLambda
用于 returns 而不是 bool
的东西)。 (这种差异在很大程度上是我的回答与 Diego 的不同之处。)使用 VersatileLambda
现在看起来像这样(你可以看到我为什么包装它):
var vl = new VersatileLambda<Func<MyObject, bool>>(o => o.Value > 5);
我已经确认这对 IEnumerable
和 IQueryable
都非常有效,正确地将 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
。现在你真火了。