将一个方法传递给LINQ查询

10

我目前在做的一个项目中,我们有许多静态表达式,当我们调用它们的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提供程序等...但我不太清楚为什么会起作用。

这里发生了什么?


3
它起作用了!但我们不知道为什么。我希望更多的开发者有这种态度。即使是基本的事实,你明白自己不明白,也让你跻身前10%。 - usr
usefulExpression.Invoke 这段代码不应该编译通过。你是否缺少了某些代码? Expression<Func<SomeDataType, bool>> 没有 Invoke 成员。 - usr
是的,事实上,我有意省略了很多代码。这就是为什么有注释的原因。我可以在一秒钟内在C# pad中编写可运行的代码...等一下... - Sean Newell
这篇SO帖子似乎更深入地阐述了底层的情况。 - Sean Newell
请确保在提问时提供一个[mcve]。 - Enigmativity
4个回答

34

当你不理解某些东西的时候,提问是很好的选择,但问题在于有时候你并不确定对方究竟不理解哪个部分。我希望能够在这里帮到你,而不是告诉你一些你已经知道的东西,却没有真正回答你的问题。

让我们回到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(1.0, 2.0) 来调用它。

现在,在.NET中我们有重载,这意味着有可能会有多个东西与 CompareDoubles 相关。这不是一个问题,因为如果我们有了例如 public int CompareDoubles(double x, double y, double z){…},编译器将知道你可能只想将另一个 CompareDoubles 分配给 dele,因此它是明确的。尽管如此,在上下文之外,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中的语法糖,背后仍然存在一个方法,尽管它有一个“无法言说的名称”(一个在.NET名称中有效但不在C#名称中有效的名称,因此C#名称不能与其冲突)。如果经常创建方法,只需使用特定委托一次,它会很方便。

再向前迈进一步,在.NET 3.5中具有协变性和逆变性(对委托非常有用),FuncAction委托(根据类型重用相同的名称,而不是拥有一堆非常相似的不同委托),随之而来的是C#3,其中包含lambda表达式。

现在,这些在某种情况下有点像匿名方法,但在另一个情况下又不像。

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

var func = (int i) => i * 2;
var可以根据被赋值的内容推断出其类型,但是lambda表达式则是通过其被赋予的含义来确定其类型,因此这种情况是不明确的。
Func<int, int> func = i => i * 2;

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

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

这反过来缩写为类似于:

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

但它也可以被用作:

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

这是一个简写形式:

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

同时在.NET 3.5中,我们也使用了Linq。 Linq大量使用了这两者中的一种。事实上,Expressions被认为是Linq的一部分,并且位于命名空间中。请注意,我们在此处得到的对象是要执行的操作的描述(将参数乘以二并给出结果),而不是如何执行操作的描述。

现在,Linq有两种主要操作方式。一种是在IQueryableIQueryable<T>上进行操作,另一种是在内存序列中的IEnumerableIEnumerable<T>上进行操作。

我们可以在其中之间移动。我们可以使用AsQueryableIEnumerable<T>转换为IQueryable<T>,这将为我们提供该可枚举集合的包装器,并且我们可以通过将其视为一个可枚举集合来将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 调用以及源查询的表达式的一部分,并返回一个包装该表达式的对象。换句话说,对 queryable 的 Select 的调用返回一个表示对 queryable 的 Select 的调用的对象!

至于这个对象将用于什么取决于提供程序。数据库提供程序将其转换为 SQL,可枚举集合在表达式上调用 Compile() 来创建委托,然后我们回到了上面第一个版本的 Select,等等。

但考虑到这些历史,让我们再次逆向追溯历史。lambda 表示可以是表达式也可以是委托(如果是表达式,则可以通过 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<T>的Select只接受表达式,因此提供程序可以尝试计算如何将其转换为其处理方式(例如,在数据库中执行SQL)。IEnumerable<T>的Select只接受委托,因此它们可以在.NET应用程序本身中执行。我们可以通过Compile()从前者转到后者(当查询实际上是包装的可枚举时),但是我们无法从后者转到前者:我们没有办法将委托变成除“调用此委托”以外的任何其他东西的表达式,这不是可以转换为SQL的内容。

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

95%的时间最好避免这种情况。因此,在带有支持数据库的查询时,如果您得到“转换为方法组”的建议,您应该想到“哎呀!那实际上是一个委托。为什么是一个委托?我能不能改变它成为一个表达式?”只有剩余的5%的时间,您应该认为“如果我只传递方法名,那就会稍微缩短一些”。 (此外,使用方法组而不是委托防止了编译器可以做的委托缓存,因此可能不太有效率)。

好了,我希望我在所有这些过程中涵盖了您不理解的部分,或者至少在这里有一部分内容可以让您指出并说:“那一部分,那就是我不理解的部分”。


3
哇,我从这个回答中学到了很多。解释得非常清晰和透彻!非常感谢。 - RoadBump
@RoadBump 你好。我在倒数第二段添加了一句话,提到了一个当时没有考虑到的性能影响。 - Jon Hanna

1
Select(SomeModelClass.FromDbEntity)

这里使用的是Enumerable.Select,这不是你想要的。这将从“可查询的LINQ”转换为对象LINQ。这意味着数据库无法执行此代码。
.Where(sdt => usefulExpression.Invoke(sdt))

这里,我假设你的意思是 .Where(usefulExpression)。这将表达式传递到查询底层的表达式树中。LINQ提供程序可以翻译此表达式。
当你执行这样的实验时,请使用SQL Profiler查看通过网络传输的SQL语句。确保查询的所有相关部分都是可翻译的。

我不确定数据库是否无法执行该代码。我刚刚在我的开发数据库上运行了这段代码,它全部都执行了。我还没有查看SQL分析器和生成的代码,所以我肯定需要更详细地查看它们之间的区别,但是调用表达式并将表达式作为方法组传递似乎都可以工作,我正在思考它们之间的差异。 - Sean Newell
这会从数据库中查询所有内容,然后使用LINQ to objects运行该方法。L2S或EF不参与处理。可以使用SQL Profiler查找。 - usr

1
我不想让你失望,但这并没有什么魔法。我建议你非常小心地使用这种“新方法”。在VS中悬停函数时,始终检查其结果。请记住,IQueryable “继承”IEnumerable ,而Queryable包含与Enumerable相同名称的扩展方法,唯一的区别是前者使用Expression >,而后者只使用Func <..>。
因此,每当您在IQueryable 上使用Func或方法组时,编译器将选择Enumerable重载,从而在静默切换LINQ到实体和LINQ到对象上下文之间进行切换。但是两者之间存在巨大的区别-前者在数据库中执行,而后者在内存中执行。
关键是尽可能长时间地保持在IQueryable 上下文中,因此应优先考虑“旧方法”。例如,从您的示例中。
.Where(sdt => sdt.someCondition == true && false || true)

或者

.Where(ManyExpressions.UsefulExpression)

或者

.Where(usefulExpression)

但不是。
.Where(sdt => usefulExpression.Invoke(sdt))

而且从不

.Select(SomeModelClass.FromDbEntity)

-1

这个解决方案让我感到有些不安。其中主要的问题是:

  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

真正的魔力在于这些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表达式树上的我的帖子


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接