2012-01-19 62 views
3

有人可以请解释我在这里失踪。基于我的基本理解,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,它不使用变量的当前值?

+0

它与后期绑定无关... –

回答

3

有几个不同的东西在这里变得困惑。

后期绑定:这是它被编译之后确定的代码的含义。例如,如果编译器检查x类型的对象是否具有DoStuff()方法(考虑扩展方法和默认参数),然后在其输出的代码中产生调用,或者使用编译器失败,则0123ree是早期绑定的否则错误。如果搜索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<int, bool>,并且该lambda表达式捕获int变量。 Where对此捕获一无所知,但Func确实如此。

这就是他们在对待cutoff时表现如此不同的原因。

+0

谢谢乔恩。我知道后期绑定,但我正在处理多件事,并在那里使用了错误的术语,而不是延迟执行。但是,阅读你的答案仍然很有趣。谢啦。 – mchicago

+0

在'Take'的示例实现中,参数异常不会在您指出的地方抛出。直到第一个MoveNext,您所写的代码都不会运行。你需要一个外部方法完成参数验证并调用一个用yield编写的辅助方法。 –

+0

@GideonEngelberth感谢您的收获。我想简明扼要,因为它的目的是为了阅读而不是运行,但它相当于打败了我的观点。仔细看看真实情况,我意识到它应该将负数视为零,但我认为我会将该错误留在那里,因为它不会打败我的论点。 –

1

Take不带一个lambda,而是一个整数,因此它在更改原始变量时不能更改。

6

您看到的行为来自于评估LINQ函数参数的不同方式。 Where方法接收一个lambda,它通过引用捕获值cutoff。它根据需求进行评估,因此当时的价值为cutoff

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

注意:此处的后期绑定一词有点不正确。后期绑定通常是指在运行时与编译时确定表达式绑定的成员的过程。在C#中,您可以通过dynamic或反射来完成此操作。 LINQ根据需求评估零件的行为称为延迟执行。

+0

谢谢JaredPar。 – mchicago