返回 IEnumerable<T>, Yielding, 后期执行查询
要记住这个重点,许多标准查询操作是一个返回 IEnumerable<T> 的原型,并且我们认为 IEnumerable<T> 是一个序列,如果操作实际上不返该序列时,我们将该操作称为调用。当枚举生成一个序列的项时操作返回一个对象。它是在枚举返回对象过程的中,该查询实际上才执行,并生成一个序列输出项。这个方法的查询称为后执行查询。
当我使用批量生成时,你并不知道,我是指关于C# 2.0 生成关键字,新增到C# 语言中使得书写枚举更加容易。
例如,示例 3-2 代码
示例 3-2. 简单查询
string
[] presidents
=
{
"
Adams
"
,
"
Arthur
"
,
"
Buchanan
"
,
"
Bush
"
,
"
Carter
"
,
"
Cleveland
"
,
"
Clinton
"
,
"
Coolidge
"
,
"
Eisenhower
"
,
"
Fillmore
"
,
"
Ford
"
,
"
Garfield
"
,
"
Grant
"
,
"
Harding
"
,
"
Harrison
"
,
"
Hayes
"
,
"
Hoover
"
,
"
Jackson
"
,
"
Jefferson
"
,
"
Johnson
"
,
"
Kennedy
"
,
"
Lincoln
"
,
"
Madison
"
,
"
McKinley
"
,
"
Monroe
"
,
"
Nixon
"
,
"
Pierce
"
,
"
Polk
"
,
"
Reagan
"
,
"
Roosevelt
"
,
"
Taft
"
,
"
Taylor
"
,
"
Truman
"
,
"
Tyler
"
,
"
Van Buren
"
,
"
Washington
"
,
"
Wilson
"
};
IEnumerable
<
string
>
items
=
presidents.Where(p
=>
p.StartsWith(
"
A
"
));
foreach
(
string
item
in
items)
Console.WriteLine(item);
当代码行中包含使用 Where 操作的查询时,该行并没有被执行。而是返回一个对象。 它是在枚举返回对象的过程中,该查询实际上才执行。这意味着就有可能,直到输出序列枚举时,查询自身中发生的错误可能没有及时被检测到。
注意:直到输出序列枚举时查询中的错误可能都不会被检测到。
运行结果:
Adams
Arthur
该查询按照预期方式执行。但是,我会有意加入一个错误。下面的代码将尝试检索每个President的名称的第五个字符的索引。当枚举游标到一个长度少于五个字符项时,一个错误将出现。请记住,直到输出序列枚举时,这个错误都将不会发生。请看示例 3-3
示例 3-3. 一个含有异常的简单示例查询
string
[] presidents
=
{
"
Adams
"
,
"
Arthur
"
,
"
Buchanan
"
,
"
Bush
"
,
"
Carter
"
,
"
Cleveland
"
,
"
Clinton
"
,
"
Coolidge
"
,
"
Eisenhower
"
,
"
Fillmore
"
,
"
Ford
"
,
"
Garfield
"
,
"
Grant
"
,
"
Harding
"
,
"
Harrison
"
,
"
Hayes
"
,
"
Hoover
"
,
"
Jackson
"
,
"
Jefferson
"
,
"
Johnson
"
,
"
Kennedy
"
,
"
Lincoln
"
,
"
Madison
"
,
"
McKinley
"
,
"
Monroe
"
,
"
Nixon
"
,
"
Pierce
"
,
"
Polk
"
,
"
Reagan
"
,
"
Roosevelt
"
,
"
Taft
"
,
"
Taylor
"
,
"
Truman
"
,
"
Tyler
"
,
"
Van Buren
"
,
"
Washington
"
,
"
Wilson
"
};
IEnumerable
<
string
>
items
=
presidents.Where(s
=>
Char.IsLower(s[
4
]));
Console.WriteLine(
"
After the query.
"
);
foreach
(
string
item
in
items)
Console.WriteLine(item);
这段代码编译是正常的,但是运行后的结果如下:
After the query.
Adams
Arthur
Buchanan
Unhandled Exception: System.IndexOutOfRangeException: Index was outside the bounds
of the array.
…
注意该查询的输出结果。结果输出到第四项时异常发生。只是因为一个查询编译,并看上去运行并没有错误,不假设该查询是bug-free。
另外,因为这些返回 IEnumerable<T> 类型的查询是后期执行的,你可以调用代码来定义该查询。你使用枚举多次结果时,如果数据被改变,每次你枚举结果,你将获取不同结果。示例 3-4 展示了一个后期执行查询,不缓存该查询结果并在下一次枚举结果时改变数据的示例。
示例 2-4. 一个在枚举结果之间改变查询结果的示例。
//
Create an array of ints.
int
[] intArray
=
new
int
[] {
1
,
2
,
3
};
IEnumerable
<
int
>
ints
=
intArray.Select(i
=>
i);
//
Display the results.
foreach
(
int
i
in
ints)
Console.WriteLine(i);
//
Change an element in the source data.
intArray[
0
]
=
5
;
Console.WriteLine(
"
---------
"
);
//
Display the results again.
foreach
(
int
i
in
ints)
Console.WriteLine(i);
在我的说明中将得到更多技术,使得发生什么样的情况都将变的透明。当我调用Select操作时,返回一个存储了实现 IEnumerable<int> 类型的 ints 变量对象。到这里,实际上查询还没有执行,但是查询存储在 ints 的对象中。从技术上来讲,既然查询没有执行,实际上一个整形序列也不存在,但是在这种 Select 操作情况下,ints 对象知道如何通过执行查询分配给序列。
当我使用 foreach 语句第一次循环 ints 时,ints 执行查询并获取序列中的一个元素。
下来我在原整数数组中改变一个项目的值。然后我再次调用 foreach 语句循环。这将导致 ints 来再次执行该查询。因为我改变了原数组的项目值,并且因为 ints 是又一次被枚举,所以又一次执行查询,改变的项被返回。
运行结果如下:
1
2
3
---------
5
2
3
注意:即使我只调用查询一次,枚举后的结果都是不相同的。这进一步证实,该查询是后期执行的。如果他不是,这两个枚举的结果将会是相同的。这是有利弊的。如果你不希望操作后期执行,使用一个不返回 IEnumerable<T> 类型的转换操作, 使得这个查询不能后期执行(例如使用 ToArray、ToList、ToDictionary、ToLookup 来创建一个不同的数据结构)。如果数据源改变,缓存结果将不会改变。
示例 3-5 是在示例 3-4 中修改了所有查询返回一个 IEnumerable<int> 为 调用 ToList 操作返回一个 List<int> 。
示例 3-5. 返回一个 List,使得查询立即执行并返回缓存结果。
//
Create an array of ints.
int
[] intArray
=
new
int
[] {
1
,
2
,
3
};
List
<
int
>
ints
=
intArray.Select(i
=>
i).ToList();
//
Display the results.
foreach
(
int
i
in
ints)
Console.WriteLine(i);
//
Change an element in the source data.
intArray[
0
]
=
5
;
Console.WriteLine(
"
---------
"
);
//
Display the results again.
foreach
(
int
i
in
ints)
Console.WriteLine(i);
运行结果如下:
1
2
3
---------
1
2
3
注意:第二个枚举的结果和第一个枚举的结果相同。这是因为 ToList 方法不是后期执行的,并且实际上查询被立即执行调用。
一个技术讨论 什么是不同的 示例 3-5 和示例3-6 两者的返回值不同,在示例 3-5 中 Select 操作仍然是后期执行,但ToList 操作不是。在查询语句中当 ToList 操作被调用,返回 Select 操作立即执行的枚举对象,即整个查询被执行。
因为最近比较忙可能后面的内容翻译的将会缓慢,请大家谅解。