将方法传递给 LINQ 查询

Passing a method to a LINQ query

在我目前正在处理的一个项目中,我们有许多静态表达式,当我们调用它们的 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);
   }
}

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

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

因此,当 ReSharper 提示我执行此操作时(这种情况并不常见,因为匹配委托所期望的类型的条件通常不满足),它说转换为方法组。我有点模糊地理解方法组是一组方法,C# 编译器可以负责将方法组转换为 LINQ 提供程序的显式类型和适当的重载,而不是......但我很模糊为什么这完全有效。

这是怎么回事?

Select(SomeModelClass.FromDbEntity)

这使用了 Enumerable.Select,这不是您想要的。这会从 "queryable-LINQ" 过渡到 LINQ to objects。这意味着数据库无法执行此代码。

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

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

当您执行这样的实验时,请使用 SQL Profiler 来查看 SQL 通过网络传输的内容。确保查询的所有相关部分都是可翻译的。

这个解决方案给我带来了一些危险信号。其中的关键是:

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

每当你处理 Entity Framework 时,你都可以将 "ToList()" 读作 "Copy the whole thing into memory." 所以 "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

真正的神奇之处在于,这些 lambda 表达式也可以放入变量中。这是我在我的一个项目中使用的一种方法:

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

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

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

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

尽情享受吧!

哦,如果你想看到一个更彻底开发的解决方案,请检查这个,尽管来自不同的上下文。

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

让我们回到 Linq 出现之前、表达式出现之前、lambda 出现之前,甚至是匿名委托出现之前的日子。

在 .NET 1.0 中我们没有这些。我们甚至没有仿制药。我们确实有代表。委托与函数指针相关(如果您了解 C、C++ 或此类语言)或作为 argument/variable 的函数(如果您了解 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) 或 shorthand dele(1.0, 2.0).

调用它

现在,因为我们在 .NET 中有重载,我们可以有不止一个 CompareDoubles 引用的东西。这不是问题,因为如果我们也有例如public int CompareDoubles(double x, double y, double z){…} 编译器可能知道您可能只是想将另一个 CompareDoubles 分配给 dele 所以它是明确的。不过,虽然在上下文中 CompareDoubles 表示采用两个 double 参数和 return 一个 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;
};

这部分只是来自 C#2 的语法糖,在幕后仍然有一个方法,尽管它有一个 "unspeakable name"(一个作为 .NET 名称有效但作为 .NET 无效的名称C# 名称,因此 C# 名称不能与其冲突)。如果像通常情况一样创建方法只是为了让它们与特定委托一起使用一次,那将很方便。

更进一步,在 .NET 3.5 中,FuncAction 委托具有协变和逆变(非常适合委托)(非常适合根据类型重用相同的名称,而不是而不是拥有一堆通常非常相似的不同委托),随之而来的是具有 lambda 表达式的 C#3。

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

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

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

var 根据分配给它的内容计算出它的含义,但是 lamda 根据分配给它的内容计算出它们是什么,所以这是模棱两可的。

这可能意味着:

Func<int, int> func = i => i * 2;

在这种情况下 shorthand 用于:

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

这又是 shorthand 类似于:

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

但也可以用作:

Expression<Func<int, int>> func = i => i * 2;

shorthand 用于:

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

而且我们在 .NET 3.5 中也有大量使用这两者的 Linq。事实上,Expressions 被认为是 Linq 的一部分,并且在 System.Linq.Expressions 命名空间中。请注意,我们在这里得到的对象是对我们想要完成的事情的描述(获取参数,将其乘以二,给我们结果)而不是如何做。

现在,Linq 以两种主要方式运行。在 IQueryableIQueryable<T> 以及 IEnumerableIEnumerable<T> 上。前者定义了要在 "a provider" 上使用的操作,而 "a provider does" 取决于该提供者,后者定义了对内存中值序列的相同操作。

我们可以从一个转移到另一个。我们可以把 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);
}

另一方面,可查询版本的工作方式是从 Expression<TSource, TResult> 中获取表达式树,使其成为表达式的一部分,其中包括对 Select 的调用以及可查询的源,并且 returns 是一个包装该表达式的对象。因此,换句话说,对可查询的 Select return 的调用是一个表示对可查询的 Select!

的调用的对象

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

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

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

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

现在假设我们在 lambda 选择器中引用了它:

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

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

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

MyAnonymousMethod 在这里有点无意义;它所做的只是调用 IntString(i) 和 return 结果,所以为什么不首先调用 IntString 并停止执行该方法:

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

我们采用基于 lambda 的委托并将其转换为方法组,从而消除了不必要的(尽管请参阅下面有关委托缓存的说明)间接级别。因此 ReSharper 的建议 "Convert to Method Group" 或者它的措辞(我自己不使用 ReSharper)。

虽然这里有一些需要注意的地方。 IQueryable<T> 的 Select 只接受表达式,因此提供者可以尝试找出如何将其转换为它做事的方式(例如 SQL 针对数据库)。 IEnumerable<T> 的 Select 只接受委托,因此它们可以在 .NET 应用程序本身中执行。我们可以使用 Compile() 从前者转到后者(当可查询对象实际上是一个包装的可枚举对象时),但我们不能从后者转到前者:我们没有办法接受委托并把它变成一个表达式,除了 "call this delegate" 之外的任何东西,这不是可以变成 SQL.

的东西

现在,当我们使用像 i => i * 2 这样的 lambda 表达式时,它在与 IQueryable<T> 一起使用时将是一个表达式,而在与 IEnumerable<T> 一起使用时将是一个委托,因为重载解析规则有利于表达式with queryable(作为一种类型,它可以处理两者,但表达式形式适用于派生程度最高的类型)。如果我们明确地给它一个委托,无论是因为我们在某处键入它作为 Func<> 还是它来自方法组,那么采用表达式的重载不可用,并且使用那些采用委托的重载。这意味着它不会传递给数据库,而是到那时为止的 linq 表达式变成 "database part" 并且它被调用,其余工作在内存中完成。

95% 的时间最好避免。因此,95% 的情况下,如果您通过数据库支持的查询获得 "convert to method group" 的建议,您应该考虑 "uh oh! that's actually a delegate. Why is that a delegate? Can I change it to be an expression?"。只有剩下的 5% 的时间你应该思考 "that'll be slightly shorter if I just pass in the method name"。 (此外,使用方法组而不是委托会阻止编译器缓存委托,否则可能会降低效率)。

到这里,我希望我涵盖了您在所有这些过程中不理解的部分,或者至少这里有一点您可以指着说 "that bit there, that's the bit I don't grok"。

我不想让你失望,但根本没有魔法。我建议您对此 "new way" 非常小心。

始终通过将鼠标悬停在 VS 中来检查函数的结果。请记住 IQueryable<T> "inherits" IEnumerable<T>Queryable 包含与 Enumerable 同名的扩展方法,唯一的区别是前者与Expression<Func<...>> 而后者只是 Func<..>

因此,无论何时您在 IQueryable<T> 上使用 Funcmethod group,编译器都会选择 Enumerable 重载,从而默默地从 LINQ to Entities 切换到 LINQ to Objects上下文。但是两者之间有一个巨大的区别——前者在数据库中执行,而后者在内存中执行。

关键是要在 IQueryable<T> 上下文中停留尽可能长的时间,因此应该首选 "old way"。例如。从你的例子

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

.Where(ManyExpressions.UsefulExpression)

.Where(usefulExpression)

但不是

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

永远不会

.Select(SomeModelClass.FromDbEntity)