有人可以解释一下我在这里缺少的东西。基于我的基本理解,将在使用结果时计算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 :(得分: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";
这非常直观,可以打印2
和abc
而非3
和xyz
。但是在匿名函数和lambda表达式中,当我们使用变量时,我们将其“捕获”为变量,因此我们最终将使用调用委托时的值:
int i = 2;
string s = "abc";
Action λ = () =>
{
Console.WriteLine(i);
Console.WriteLine(s);
};
i = 3;
s = "xyz";
λ();
创建λ
不会使用i
和s
的值,但会创建一组有关如何处理i
和{{1}的说明调用s
时。只有在发生这种情况时才会使用λ
和i
的值。
将所有内容放在一起:在任何情况下,您都没有任何后期绑定。这与你的问题无关。
两者都延迟执行。对s
的调用和对Take
的调用都会返回可枚举的对象,这些对象在枚举时会对Where
起作用。
只有一个你有一个捕获的变量。对arr
的调用直接将整数传递给Take
,Take
使用该值。对Take
的调用传递从lambda表达式创建的Where
,该lambda表达式捕获Func<int, bool>
变量。 int
对此捕获一无所知,但Where
确实如此。
这就是两个人在处理Func
时表现如此不同的原因。
答案 2 :(得分:1)
Take
不接受lambda,而是取整数,因此在更改原始变量时它不会改变。