林克后期约束混乱

时间:2012-01-19 16:55:34

标签: c# linq

有人可以解释一下我在这里缺少的东西。基于我的基本理解,将在使用结果时计算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();
}

输出:

First Print
1
2
Second Print
1
2
3

现在我改变了

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

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

,输出如下。

First Print
1
Second Print
1

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

3 个答案:

答案 0 :(得分:6)

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

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

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

答案 1 :(得分:3)

这里有一些不同的东西让人感到困惑。

后期绑定:这是编译后确定代码含义的地方。例如,如果编译器检查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的值,但会创建一组有关如何处理i和{{1}的说明调用s时。只有在发生这种情况时才会使用λi的值。

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

两者都延迟执行。对s的调用和对Take的调用都会返回可枚举的对象,这些对象在枚举时会对Where起作用。

只有一个你有一个捕获的变量。对arr的调用直接将整数传递给TakeTake使用该值。对Take的调用传递从lambda表达式创建的Where,该lambda表达式捕获Func<int, bool>变量。 int对此捕获一无所知,但Where确实如此。

这就是两个人在处理Func时表现如此不同的原因。

答案 2 :(得分:1)

Take不接受lambda,而是取整数,因此在更改原始变量时它不会改变。