【问题标题】:Passing a method to a LINQ query将方法传递给 LINQ 查询
【发布时间】:2016-02-15 22:53:41
【问题描述】:

在我目前正在进行的一个项目中,我们有许多静态表达式,当我们在它们上调用 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 提供程序的显式类型和适当的重载等等......但我对为什么这完全有效。

这是怎么回事?

【问题讨论】:

  • And it worked! But we do not understand why. 我希望更多的开发者有这种态度。即使是你理解你不理解的基本事实也会让你进入前 10%。
  • usefulExpression.Invoke 这不应该编译。你错过了一些代码吗? Expression&lt;Func&lt;SomeDataType, bool&gt;&gt; 没有 Invoke 成员。
  • @usr 是的,其实我故意省略了很多代码。因此,cmets。我可以在一秒钟内在 C# pad 中编写一些可运行的东西... h/o...
  • This SO 的帖子似乎揭示了幕后发生的事情。
  • 请确保在提问时提供minimal reproducible example

标签: c# linq method-group


【解决方案1】:

当您不理解某事时提出问题很好,但问题是很难知道某人不理解的内容。我希望我能在这里提供帮助,而不是告诉你一堆你知道的东西,而不是真正回答你的问题。

让我们回到 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 的语法糖,在幕后仍然有一个方法,尽管它有一个“不可描述的名称”(一个可以作为 .NET 名称但作为 C# 名称无效的名称,因此 C# 名称不能与之冲突)。如果像通常的情况一样,创建方法只是为了让它们与特定的委托一起使用一次,那会很方便。

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

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

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

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,它大量使用了这两者。实际上,Expressions 被认为是 Linq 的一部分,并且位于 System.Linq.Expressions 命名空间中。请注意,我们在这里得到的对象是对我们想要做什么的描述(取参数,将它乘以 2,给我们结果),而不是如何去做。

现在,Linq 以两种主要方式运行。在IQueryableIQueryable&lt;T&gt; 以及IEnumerableIEnumerable&lt;T&gt; 上。前者定义了要在“提供者”上使用的操作,而“提供者做什么”取决于该提供者,而后者定义了对内存中值序列的相同操作。

我们可以从一个移动到另一个。我们可以使用AsQueryableIEnumerable&lt;T&gt; 转换为IQueryable&lt;T&gt;,这将为我们提供一个可枚举的包装器,我们可以将IQueryable&lt;T&gt; 转换为IEnumerable&lt;T&gt;,只需将其视为一个,因为IQueryable&lt;T&gt;派生自IEnumerable&lt;T&gt;

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

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

另一方面,可查询版本的工作原理是从Expression&lt;TSource, TResult&gt; 获取表达式树,使其成为包含对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 的建议是“转换为方法组”或者它的措辞(我自己不使用 ReSharper)。

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

现在,当我们使用像 i =&gt; i * 2 这样的 lambda 表达式时,它在与 IQueryable&lt;T&gt; 一起使用时将是一个表达式,而在与 IEnumerable&lt;T&gt; 一起使用时将是一个委托,因为重载决议规则有利于具有可查询的表达式(作为一种类型,它可以处理两者,但表达式形式适用于最派生的类型)。如果我们显式地给它一个委托,无论是因为我们在某处键入它为Func&lt;&gt; 还是它来自一个方法组,那么接受表达式的重载不可用,而使用那些接受委托的重载。这意味着它不会被传递到数据库,而是直到那时的 linq 表达式成为“数据库部分”,它被调用,其余的工作在内存中完成。

95% 的时间最好避免。因此,95% 的情况下,如果您使用数据库支持的查询获得“转换为方法组”的建议,您应该会想“哦,哦!那实际上是一个委托。为什么是委托?我可以将其更改为表达式吗? ”。只有剩下的 5% 的时间你会认为“如果我只传递方法名称会稍微短一些”。 (此外,使用方法组而不是委托会阻止缓存编译器可以执行的委托,因此效率可能会降低)。

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

【讨论】:

  • 哇。我从这个答案中学到了很多东西。如此清晰和彻底的解释!非常感谢
  • @RoadBump 欢呼。我在倒数第二段中添加了一个关于我当时没有想到的性能影响的句子。
【解决方案2】:
Select(SomeModelClass.FromDbEntity)

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

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

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

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

【讨论】:

  • 我不确定数据库是否无法执行该代码。我实际上只是用我的开发数据库上的数据运行了这段代码......并且它全部执行了。我没有查看 SQL 分析器和发出的代码,所以我肯定需要更详细地查看它,但是调用表达式和传递表达式作为方法组似乎都有效,我正在思考它们的区别.
  • 这会查询数据库中的所有内容,然后使用 LINQ to 对象运行该方法。 L2S 或 EF 不参与处理。使用 SQL Profiler 找出答案。
【解决方案3】:

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

始终通过将函数悬停在 VS 中来检查函数的结果。请记住 IQueryable&lt;T&gt; "inherits" IEnumerable&lt;T&gt;Queryable 包含与 Enumerable 同名的扩展方法,唯一的区别是前者与 Expression&lt;Func&lt;...&gt;&gt; 一起使用,而后者仅与 @987654326 一起使用@。

因此,无论何时您在 IQueryable&lt;T&gt; 上使用 Funcmethod group,编译器都会选择 Enumerable 重载,从而默默地从 LINQ to Entities 切换到 LINQ to Objects 上下文。但两者之间存在巨大差异——前者在数据库中执行,后者在内存中执行。

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

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

.Where(ManyExpressions.UsefulExpression)

.Where(usefulExpression)

但不是

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

永远不会

.Select(SomeModelClass.FromDbEntity)

【讨论】:

    【解决方案4】:

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

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

    每当您处理实体框架时,您都可以将“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()。

    享受吧!

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2014-08-23
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-02-21
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多