9

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

4 回答 4

32

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

让我们回到 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<T>IEnumerableIEnumerable<T>。前者定义了要在“提供者”上使用的操作,而“提供者所做的”取决于该提供者,后者定义了对内存中值序列的相同操作。

我们可以从一个移动到另一个。我们可以把 anIEnumerable<T>变成一个IQueryable<T>with AsQueryablewhich 会给我们一个可枚举的包装,我们可以把它IQueryable<T>变成一个IEnumerable<T>just 把它当作一个,因为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 的建议是“转换为方法组”或者它的措辞(我自己不使用 ReSharper)。

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

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

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

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

于 2016-02-16T01:54:36.187 回答
1

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

始终通过将函数悬停在 VS 中来检查函数的结果。请记住,IQueryable<T>“继承”IEnumerable<T>Queryable包含与 同名的扩展方法,Enumerable唯一的区别是前者与 一起使用,Expression<Func<...>>而后者仅与 一起使用Func<..>

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

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

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

或者

.Where(ManyExpressions.UsefulExpression)

或者

.Where(usefulExpression)

但不是

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

永不

.Select(SomeModelClass.FromDbEntity)
于 2016-02-16T02:16:30.400 回答
1
Select(SomeModelClass.FromDbEntity)

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

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

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

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

于 2016-02-15T23:00:10.417 回答
-1

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

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

享受!

哦,如果您想看到一个更彻底的解决方案,请查看这个,尽管来自不同的上下文。 我在 Linq 表达式树上的帖子

于 2016-02-15T23:41:32.670 回答