将方法传递给LINQ查询

时间:2016-02-15 22:53:41

标签: 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);
   }
}

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

// 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提供程序的显式类型和适当的重载,但不是......但是我是模糊了为什么这完全有效。

这里发生了什么?

4 个答案:

答案 0 :(得分:18)

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

让我们回到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表示一个方法,它接受两个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;
};

这部分只是来自C#2的语法糖,在幕后仍有一种方法,尽管它有一个不可言喻的名字&#34; (作为.NET名称有效但作为C#名称无效的名称,因此C#名称不能与之冲突)。如果通常情况下,创建方法只是为了让它们与特定的代表一起使用,那么它很方便。

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

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

这就是我们无法做到的原因:

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

var解决了它分配给它的意义,但是lamdas从他们被分配到的内容中找出它们的含义,所以这是不明确的。

这可能意味着:

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的一部分,并且位于System.Linq.Expressions命名空间中。请注意,我们在这里得到的对象是我们想要做的事情的描述(取参数,乘以2,给我们结果)而不是如何做。

现在,Linq以两种主要方式运作。在IQueryableIQueryable<T>以及IEnumerableIEnumerable<T>上。前者定义了在&#34;提供商&#34;上使用的操作。正是&#34;提供商做了什么&#34;由该提供者决定,后者在内存中的值序列中定义相同的操作。

我们可以从一个移动到另一个。我们可以使用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的调用和源查询的表达式的一部分,并返回一个对象包装那个表达。换句话说,对可查询的Select的调用会返回一个对象,该对象代表对可查询的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的建议&#34;转换为方法组&#34;或者它的措辞(我不会自己使用ReSharper)。

这里有一点需要注意。 IQueryable<T>选择只接受表达式,因此提供程序可以尝试解决如何将其转换为执行操作的方式(例如,针对数据库的SQL)。 IEnumerable<T>选择只接受委托,以便可以在.NET应用程序本身中执行。我们可以使用Compile()从前者转到后者(当可查询实际上是一个包装可枚举的时候),但是我们不能从后者转到前者:我们没有办法接受一名代表并将其转变为一种表达方式,这意味着除了&#34;以外的任何其他方式称这个代表&#34;这不是可以变成SQL的东西。

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

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

在那里,我希望我能够涵盖你在所有这些过程中无法理解的内容,或者至少在这里你可以指出并说出那里的那一点,那是我不会理解的#34;

答案 1 :(得分:1)

我不想让你失望,但根本就没有魔法。我建议你对这个&#34;新方式&#34;非常小心。

始终通过将其悬停在VS中来检查功能的结果。请记住IQueryable<T>&#34;继承&#34; IEnumerable<T>以及Queryable包含与Enumerable具有相同名称的扩展方法,唯一的区别是前者与Expression<Func<...>>一起使用,而后者只与Func<..>一起使用1}}。

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

关键是要在LINQ to Objects上下文中保持尽可能长的时间,所以&#34;旧方式&#34;应该是首选。例如。来自你的例子

IQueryable<T>

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

.Where(ManyExpressions.UsefulExpression)

但不是

.Where(usefulExpression)

永远不会

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

答案 2 :(得分:0)

Select(SomeModelClass.FromDbEntity)

这使用Enumerable.Select,这不是你想要的。这转换为&#34; queryable-LINQ&#34;进入LINQ对象。这意味着数据库无法执行此代码。

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

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

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

答案 3 :(得分:0)

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

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

每当您处理实体框架时,您都可以阅读&#34; ToList()&#34; as&#34;将整个内容复制到内存中。&#34;所以&#34; ToList()&#34;应该只在最后一秒完成。

考虑一下:在处理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也可以被抛入变量中。这是我在我的一个项目中使用的方法:

    /// <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()。

享受!

哦,如果您希望看到更完善的解决方案,请查看此信息,尽管来自不同的背景。 My Post on Linq Expression Trees