【问题标题】:Linq late binding confusionLinq 后期绑定混乱
【发布时间】:2012-01-20 08:05:14
【问题描述】:

有人可以解释一下我在这里缺少什么。根据我的基本理解,将在使用结果时计算 linq 结果,我可以在下面的代码中看到这一点。

 static void Main(string[] args)
 {
     Action<IEnumerable<int>> print = (x) =>
     {
         foreach (int i in x)
         {
             Console.WriteLine(i);
         }
     };

     int[] arr = { 1, 2, 3, 4, 5 };
     int cutoff = 1;
     IEnumerable<int> result = arr.Where(x => x < cutoff);
     Console.WriteLine("First Print");
     cutoff = 3;
     print(result);
     Console.WriteLine("Second Print");
     cutoff = 4;
     print(result);
     Console.Read();
}

输出:

第一次打印 1 2 第二次打印 1 2 3

现在我换了

arr.Where(x => x < cutoff); 

IEnumerable<int> result = arr.Take(cutoff); 

输出如下。

第一次打印 1 第二次打印 1

为什么用Take,它不使用变量的当前值?

【问题讨论】:

  • 跟后期绑定无关...

标签: c# linq


【解决方案1】:

您看到的行为来自评估 LINQ 函数的参数的不同方式。 Where 方法接收一个 lambda,它通过引用捕获值 cutoff。它是按需评估的,因此会在那时看到 cutoff 的值。

Take 方法(以及类似的方法如Skip)采用int 参数,因此cutoff 是按值传递的。使用的值是cutoff 在调用Take 方法时的值,而不是在评估查询时的值

注意:这里的术语后期绑定有点不正确。后期绑定通常是指表达式绑定的成员在运行时与编译时确定的过程。在 C# 中,您可以使用 dynamic 或反射来完成此操作。 LINQ 按需评估其部件的行为称为延迟执行。

【讨论】:

    【解决方案2】:

    这里有一些不同的东西会混淆。

    后期绑定:这是在编译后确定代码含义的地方。例如,如果编译器检查x 类型的对象是否具有DoStuff() 方法(也考虑扩展方法和默认参数),然后在它输出的代码中产生对它的调用,则x.DoStuff() 是早期绑定的,否则会因编译器错误而失败。如果DoStuff() 方法的搜索是在运行时完成的,并且如果没有DoStuff() 方法,则抛出运行时异常。各有利弊,C# 通常是早期绑定的,但支持后期绑定(最简单的是通过dynamic,但涉及反射的更复杂的方法也很重要)。

    延迟执行:严格来说,所有的 Linq 方法都会立即产生结果。但是,该结果是一个对象,它存储对可枚举对象的引用(通常是先前 Linq 方法的结果),当它本身被枚举时,它将以适当的方式处理。例如,我们可以编写自己的Take 方法为:

    private static IEnumerable<T> TakeHelper<T>(IEnumerable<T> source, int number)
    {
      foreach(T item in source)
      {
        yield return item;
        if(--number == 0)
          yield break;
      }
    }
    public static IEnumerable<T> Take<T>(this IEnumerable<T> source, int number)
    {
      if(source == null)
        throw new ArgumentNullException();
      if(number < 0)
        throw new ArgumentOutOfRangeException();
      if(number == 0)
        return Enumerable.Empty<T>();
      return TakeHelper(source, number);
    }
    

    现在,当我们使用它时:

    var taken4 = someEnumerable.Take(4);//taken4 has a value, so we've already done
                                        //something. If it was going to throw
                                        //an argument exception it would have done so
                                        //by now.
    
    var firstTaken = taken4.First();//only now does the object in taken4
                                            //do the further processing that iterates
                                            //through someEnumerable.
    

    捕获的变量:通常当我们使用一个变量时,我们会利用它的当前状态:

    int i = 2;
    string s = "abc";
    Console.WriteLine(i);
    Console.WriteLine(s);
    i = 3;
    s = "xyz";
    

    这很直观,打印的是 2abc 而不是 3xyz。但是,在匿名函数和 lambda 表达式中,当我们使用变量时,我们会将其“捕获”为变量,因此我们最终将使用调用委托时它所具有的值:

    int i = 2;
    string s = "abc";
    Action λ = () =>
    {
      Console.WriteLine(i);
      Console.WriteLine(s);
    };
    i = 3;
    s = "xyz";
    λ();
    

    创建λ 不使用is 的值,而是创建一组指令,说明在调用λ 时如何处理is。只有当这种情况发生时,才会使用 is 的值。

    将所有内容放在一起:在您的任何情况下,您都没有任何后期绑定。这与您的问题无关。

    在这两种情况下,您都延迟了执行。对Take 的调用和对Where 的调用都返回可枚举的对象,这些对象在枚举它们时将作用于arr

    只有一个你有一个捕获的变量。对Take 的调用将一个整数直接传递给TakeTake 使用该值。对 Where 的调用传递了一个从 lambda 表达式创建的 Func&lt;int, bool&gt;,并且该 lambda 表达式捕获了一个 int 变量。 Where 对这次捕获一无所知,但 Func 知道。

    这就是两人对待cutoff的方式如此不同的原因。

    【讨论】:

    • 谢谢乔恩。我知道后期绑定,但我正在处理多个事情并且在那里使用了错误的术语而不是延迟执行。但是阅读您的答案仍然很有趣。谢谢大佬。
    • Take 的示例实现中,不会在您指定的地方抛出参数异常。在第一个 MoveNext 之前,您编写的代码都不会运行。您需要有一个外部方法来进行参数验证并调用一个使用 yield 编写的辅助方法。
    • @GideonEngelberth 感谢您的关注。我想要简洁,因为它是为了阅读而不是运行,但它却违背了我所写的观点。仔细看看真实的东西,我意识到它应该只将负数视为零,但我认为我会将那个错误留在那里,因为它不会推翻我的论点。
    【解决方案3】:

    Take 不接受 lambda,而是一个整数,因此当您更改原始变量时它不会改变。

    【讨论】:

      猜你喜欢
      • 2014-09-24
      • 2014-03-30
      • 1970-01-01
      • 1970-01-01
      • 2011-04-24
      • 2012-04-13
      • 1970-01-01
      • 1970-01-01
      • 2010-09-21
      相关资源
      最近更新 更多