将方法传递给LINQ查询

Sea*_*ell 4 c# linq method-group

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

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

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

想象一下像这样的代码

// old way
public static class ManyExpressions {
   public static Expression<Func<SomeDataType, bool> 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<ImportantDataResult> 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);
   }
}
Run Code Online (Sandbox Code Playgroud)

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

// 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<SomeModelClass> 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);
   }
}
Run Code Online (Sandbox Code Playgroud)

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

这里发生了什么?

Jon*_*nna 19

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

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

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

我们可以定义一个委托:

public delegate int MyDelegate(double someValue, double someOtherValue);
Run Code Online (Sandbox Code Playgroud)

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

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

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

MyDelegate dele = CompareDoubles;
Run Code Online (Sandbox Code Playgroud)

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

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

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

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

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

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

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

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

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

var func = (int i) => i * 2;
Run Code Online (Sandbox Code Playgroud)

var 从分配给它的内容中找出它意味着什么,但lamdas从它们被赋予的内容中找出它们的含义,所以这是模棱两可的.

这可能意味着:

Func<int, int> func = i => i * 2;
Run Code Online (Sandbox Code Playgroud)

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

Func<int, int> func = delegate(int i){return i * 2;};
Run Code Online (Sandbox Code Playgroud)

这反过来又是简写:

int <>SomeNameImpossibleInC# (int i)
{
  return i * 2;
}
Func<int, int> func = <>SomeNameImpossibleInC#;
Run Code Online (Sandbox Code Playgroud)

但它也可以用作:

Expression<Func<int, int>> func = i => i * 2;
Run Code Online (Sandbox Code Playgroud)

这是简写​​:

Expression<Func<int, int>> func = Expression.Lambda<Func<int, int>>(
  Expression.Multiply(
    param,
    Expression.Constant(2)
  ),
  param
);
Run Code Online (Sandbox Code Playgroud)

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

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

我们可以从一个移动到另一个.我们可以把一个IEnumerable<T>成一个IQueryable<T>AsQueryable这将给我们对枚举的包装,我们可以把IQueryable<T>IEnumerable<T>只是把它当作一个,因为IQueryable<T>导出的IEnumerable<T>.

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

public static IEnumerable<TResult> Select(this IEnumerable<TSource> source, Func<TSource, TResult> selector)
{
  foreach(TSource item in source) yield return selector(item);
}
Run Code Online (Sandbox Code Playgroud)

另一方面,可查询版本的工作原理是使表达式树Expression<TSource, TResult>成为表达式的一部分,该表达式包括对Select可查询源的调用,并返回包装该表达式的对象.换句话说,对queryable的调用Select返回一个表示对queryable的调用的对象Select!

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

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

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

public string IntString(int num) { return num.ToString(); }
Run Code Online (Sandbox Code Playgroud)

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

Enumerable.Range(0, 10).Select(i => IntString(i));
Run Code Online (Sandbox Code Playgroud)

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

public string MyAnonymousMethod(int i){return IntString(i);}
Run Code Online (Sandbox Code Playgroud)

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

Enumerable.Range(0, 10).Select(IntString);
Run Code Online (Sandbox Code Playgroud)

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

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

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

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

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

  • 哇。我从这个答案中学到了很多。如此清晰和彻底的解释!非常感谢 (3认同)