将方法传递给LINQ查询

在我正在进行的项目中,我们有许多静态表达式,当我们在它们上面调用Invoke方法并将lambda表达式的参数传递给它时,我们必须在本地范围内使用变量。

今天,我们声明了一个静态方法,其参数正是查询所期望的类型。 所以,我的同事和我正在搞乱,看看我们是否可以在查询的Select语句中使用此方法来执行项目,而不是在整个对象上调用它,而不将其带入本地范围。

它奏效了! 但我们不明白为什么。

想象一下像这样的代码

// old way public static class ManyExpressions { public static Expression<Func UsefulExpression { get { // TODO implement more believable lies and logic here return (sdt) => sdt.someCondition == true && false || true; } } } public class ARealController : BaseController { /* many declarations of important things */ public ARealClass( /* many ninjected in things */) { /* many assignments */ } public JsonNet getSomeInfo(/* many useful parameter */) { var usefulExpression = ManyExpressions.UsefulExpression; // the db context is all taken care of in BaseController var result = db.SomeDataType .Where(sdt => usefulExpression.Invoke(sdt)) .Select(sdt => new { /* grab important things*/ }) .ToList(); return JsonNet(result); } } 

然后你就可以做到这一点!

 // new way public class SomeModelClass { /* many properties, no constructor, and very few useful methods */ // TODO come up with better fake names public static SomeModelClass FromDbEntity(DbEntity dbEntity) { return new SomeModelClass { /* init all properties here*/ }; } } public class ARealController : BaseController { /* many declarations of important things */ public ARealClass( /* many ninjected in things */) { /* many assignments */ } public JsonNet getSomeInfo(/* many useful parameter */) { // the db context is all taken care of in BaseController var result = db.SomeDataType .Select(SomeModelClass.FromDbEntity) // TODO; explain this magic .ToList(); return JsonNet(result); } } 

因此,当ReSharper提示我这样做时(通常不会这样,因为这种匹配代理所期望的类型的条件通常不会得到满足),它会说转换为方法组。 我有点模糊地理解一个方法组是一组方法,并且C#编译器可以负责将方法组转换为LINQ提供程序的显式类型和适当的重载,而不是……但是我很模糊为什么这完全奏效。

这里发生了什么?

当你不理解某些问题时,问一个问题是很好的,但问题是很难知道某个人不理解哪一个问题。 我希望我能在这里提供帮助,而不是告诉你一堆你知道的东西,而不是实际回答你的问题。

让我们回到Linq之前的日子,在表达之前,在lambda之前,甚至在匿名代表之前。

在.NET 1.0中,我们没有任何这些。 我们甚至没有仿制药。 我们确实有代表。 委托与函数指针相关(如果您知道C,C ++或具有此类的语言)或函数作为参数/变量(如果您了解Javascript或具有此类的语言)。

我们可以定义一个委托:

 public delegate int MyDelegate(double someValue, double someOtherValue); 

然后将其用作字段,属性,变量,方法参数的类型或作为事件的基础。

但当时实际为代表提供值的唯一方法是引用实际方法。

 public int CompareDoubles(double x, double y) { if (x < y) return -1; return y < x ? 1 : 0; } MyDelegate dele = CompareDoubles; 

我们可以使用dele.Invoke(1.0, 2.0)或简写dele.Invoke(1.0, 2.0)来调用它。

现在,因为我们在.NET中有重载,所以我们可以有不止一个CompareDoubles引用的东西。 这不是问题,因为如果我们还有例如public int CompareDoubles(double x, double y, double z){…} ,编译器可能知道你只能意味着将其他CompareDoubles分配给dele所以它是明确的。 尽管如此,在上下文中, CompareDoubles意味着一个方法,它接受两个double参数并返回一个int ,在该上下文之外, CompareDoubles意味着具有该名称的所有方法的组。

因此, 方法组就是我们所说的。

现在,使用.NET 2.0我们得到了generics,这对代表很有用,同时在C#2中我们得到了匿名方法,这也很有用。 从2.0开始,我们现在可以做到:

 MyDelegate dele = delegate (double x, double y) { if (x < y) return -1; return y < x ? 1 : 0; }; 

这部分只是来自C#2的语法糖,在幕后仍然有一个方法,虽然它有一个“不可言喻的名称”(一个名称有效作为.NET名称,但无效作为C#名称,所以C#名字不能与它发生冲突)。 如果通常情况下,创建方法只是为了让它们与特定的委托使用一次,这很方便。

向前推进一点,在.NET 3.5中有协方差和逆变(很好的与代表) FuncAction委托(非常适合根据类型重用相同的名称,而不是拥有一堆通常非常相似的不同委托)随之而来的是C#3,它有lambda表达式。

现在,这些在一次使用中有点像匿名方法,但在另一种用途中则不然。

这就是我们做不到的原因:

 var func = (int i) => i * 2; 

var解决了它所分配给它的含义,但是lamdas从它们被赋予的内容中找出它们的含义,所以这是模棱两可的。

这可能意味着:

 Func func = i => i * 2; 

在这种情况下,它的简写:

 Func func = delegate(int i){return i * 2;}; 

这反过来又是简写:

 int <>SomeNameImpossibleInC# (int i) { return i * 2; } Func func = <>SomeNameImpossibleInC#; 

但它也可以用作:

 Expression> func = i => i * 2; 

这是简写​​:

 Expression> func = Expression.Lambda>( Expression.Multiply( param, Expression.Constant(2) ), param ); 

而且我们在.NET 3.5中也有Linq,它们大量使用这两种方法。 实际上,表达式被认为是Linq的一部分,并且位于System.Linq.Expressions命名空间中。 请注意,我们在这里得到的对象是我们想要做的事情的描述(取参数,乘以2,给我们结果)而不是如何做。

现在,Linq以两种主要方式运作。 在IQueryableIQueryable以及IEnumerableIEnumerable 。 前者定义了在“提供者”上使用的操作,其中“提供者所做的”是由该提供者决定的,后者定义了对内存中值序列的相同操作。

我们可以从一个移动到另一个。 我们可以将IEnumerable转换为带有AsQueryableIQueryable ,这将为我们提供可枚举的包装器,我们可以将IQueryable转换为IEnumerable只需将其视为一个,因为IQueryable派生自IEnumerable

可枚举的表单使用委托。 Select如何工作的简化版本(这个版本遗漏了许多优化,我正在跳过错误检查并在间接中确保立即发生错误检查)将是:

 public static IEnumerable Select(this IEnumerable source, Func selector) { foreach(TSource item in source) yield return selector(item); } 

另一方面,可查询版本通过从Expression获取表达式树Expression使其成为包含对Select的调用和源查询的表达式的一部分,并返回包装该表达式的对象。 换句话说,对可查询的Select的调用返回一个对象,该对象表示对可查询的Select的调用!

究竟做了什么取决于提供商。 数据库提供程序将它们转换为SQL,枚举在表达式上调用Compile()以创建委托,然后我们回到上面的Select的第一个版本,依此类推。

但是,历史考虑,让我们再次回顾历史。 lambda可以表示表达式或委托(如果是表达式,我们可以Compile()Compile()以获得相同的委托)。 委托是一种通过变量指向方法的方法,而方法是方法组的一部分。 所有这些都建立在第一个版本的技术之上,只能通过创建方法然后传递它来调用。

现在,假设我们有一个方法,它接受一个参数并有结果。

 public string IntString(int num) { return num.ToString(); } 

现在让我们说我们在lambda选择器中引用它:

 Enumerable.Range(0, 10).Select(i => IntString(i)); 

我们有一个lambda为委托创建一个匿名方法,而匿名方法又调用一个具有相同参数和返回类型的方法。 在某种程度上,如果我们有:

 public string MyAnonymousMethod(int i){return IntString(i);} 

MyAnonymousMethod在这里有点无意义; 所有这一切都是调用IntString(i)并返回结果,所以为什么不首先调用IntString并切断通过该方法:

 Enumerable.Range(0, 10).Select(IntString); 

我们通过获取基于lambda的委托并将其转换为方法组,删除了一个不必要的(虽然参见下面关于委托缓存的注释)间接级别。 因此ReSharper建议“转换为方法组”或者说它的措辞(我不自己使用ReSharper)。

这里有一些值得注意的事情。 IQueryable的Select只接受表达式,因此提供程序可以尝试解决如何将其转换为执行内容的方式(例如,针对数据库的SQL)。 IEnumerable的Select只接受委托,因此它们可以在.NET应用程序本身中执行。 我们可以使用Compile()从前者到后者(当可查询实际上是一个包装可枚举的时候),但是我们不能从后者转到前者:我们没有办法让代表和转向它成为一个表达式,意味着“调用此委托”以外的任何东西,这不是可以变成SQL的东西。

现在,当我们使用像i => i * 2这样的lambda表达式时,当与IQueryable一起使用时它将是一个表达式,当与IEnumerable一起使用时,由于重载决策规则支持带有可查询的表达式(作为一个类型),它将是一个委托它可以处理两者,但表达式表单适用于派生类型最多的)。 如果我们明确地给它一个委托,无论是因为我们在某个地方键入它作为Func<>它还是来自一个方法组,那么表达式的重载不可用,并且使用了代理。 这意味着它不会被传递到数据库,而是直到那一点的linq表达式成为“数据库部分”,它被调用,其余的工作在内存中完成。

95%的时间最好避免。 因此,如果你通过数据库支持的查询得到“转换为方法组”的建议,95%的时间你应该想“呃哦!那实际上是一个委托。为什么这是委托?我可以把它改成表达吗? ”。 如果我只是传递方法名称,那么只有剩余的5%的时间你会认为“这会稍微缩短一点”。 (另外,使用方法组而不是委托可以防止编译器缓存委托,否则可能效率较低)。

在那里,我希望我在所有这些过程中覆盖了你不理解的那一点,或者至少在这里你可以指出并说“那里的那一点,那是我不会理解的”。

我不想让你失望,但根本就没有魔法。 我建议你对这种“新方式”要非常小心。

始终通过将其hover在VS中来检查function的结果。 请记住, IQueryable “inheritance” IEnumerable ,并且Queryable包含与Enumerable具有相同名称的扩展方法,唯一的区别是前者使用Expression>而后者使用只是用Func<..>

因此,只要你使用Funcmethod group不是IQueryable ,编译器就会选择Enumerable重载,从而默默地从LINQ to Entities切换LINQ to EntitiesLINQ to Objects上下文。 但两者之间存在巨大差异 – 前者在数据库中执行,而后者在内存中执行。

关键是要尽可能长时间地保留在IQueryable上下文中,因此应该首选“旧方法”。 例如,从你的例子

 .Where(sdt => sdt.someCondition == true && false || true) 

要么

 .Where(ManyExpressions.UsefulExpression) 

要么

 .Where(usefulExpression) 

但不是

 .Where(sdt => usefulExpression.Invoke(sdt)) 

永不

 .Select(SomeModelClass.FromDbEntity) 
 Select(SomeModelClass.FromDbEntity) 

这使用了Enumerable.Select ,这不是你想要的。 这从“queryable-LINQ”转换为LINQ to objects。 这意味着数据库无法执行此代码。

 .Where(sdt => usefulExpression.Invoke(sdt)) 

在这里,我假设你的意思.Where(usefulExpression) 。 这会将表达式传递给查询底层的表达式树。 LINQ提供程序可以翻译此表达式。

当您执行此类实验时,请使用SQL事件探查器查看SQL通过网络传输的内容。 确保查询的所有相关部分都是可翻译的。

这个解决方案为我提出了一些危险信号。 其中的关键是:

  var result = db.SomeDataType .Select(SomeModelClass.FromDbEntity) // TODO; explain this magic .ToList(); // < 

每当您处理Entity Framework时,您都可以将“ToList()”读作“将整个内容复制到内存中”。 所以“ToList()”应该只在最后一秒完成。

考虑一下:在处理EF时,你可以传递很多有用的对象:

  • 数据库上下文
  • 您要定位的特定数据集(例如context.Orders)
  • 针对上下文的查询:

 var query = context.Where(o => o.Customer.Name == "John") .Where(o => o.TxNumber > 100000) .OrderBy(o => o.TxDate); //I've pulled NO data so far! "var query" is just an object I can pass around //and even add on to! For example, I can now do this: query = query.ThenBy(o => o.Items.Description); //and now I've appended that to my query 

真正的魔力是那些lambdas也可以被抛入变量中。 这是我在我的一个项目中使用的方法:

  ///  /// Generates the Lambda "TIn => TIn.memberName [comparison] value" ///  static Expression> MakeSimplePredicate(string memberName, ExpressionType comparison, object value) { var parameter = Expression.Parameter(typeof(TIn), "t"); Expression left = Expression.PropertyOrField(parameter, memberName); return (Expression>)Expression.Lambda(Expression.MakeBinary(comparison, left, Expression.Constant(value)), parameter); } 

使用此代码,您可以编写如下内容:

 public GetQuery(string field, string value) { var query = context.Orders; var condition = MakeSimplePredicate(field, ExpressionType.Equal, value); return query.Where(condition); } 

最好的事情是,此时没有数据通话。 您可以根据需要继续添加条件。 当您准备好获取数据时,只需遍历它或调用ToList()即可。

请享用!

哦,如果您希望看到更完善的解决方案,请查看此信息,尽管来自不同的背景。 我在Linq表达树上的post