十五、LINQ 与迭代器
Covers:LINQ
LINQ 基础
我们经常需要从某个数据库(或数据源)查询满足特定条件的内容。这里的数据库或数据源,可以是支持 SQL 的数据库如 MySQL 等,也可以是某个 HTTP 请求地址,最简单的情况下也可以是一个简单的数组。
例如:一副扑克牌由 4 个花色、每个花色 13 张牌,外加大小王组成。要打印出除大小王以外所有的牌面,使用 LINQ 我们可以这样写:
using System;
using System.Linq;
namespace LearnLinq
{
class Program
{
static void Main(string[] args)
{
string[] suits =
{
"clubs", "diamonds", "hearts", "spades"
};
string[] ranks =
{
"ace", "2", "3", "4", "5",
"6", "7", "8", "9", "10",
"jack", "queen", "king"
};
var deck = from s in suits
from r in ranks
select new { Suit = s, Rank = r };
foreach (var card in deck)
{
Console.WriteLine(card);
}
}
}
}
首先我们定义了两个简单的数据库 suits 和 ranks(其实就是 string 数组),接着使用 LINQ 创建了一个 结果容器,最后从容器中取出每个元素并打印。
from 与 select 查询
注意其中的第 21~23 行,我们使用了 LINQ 的 查询表达式 语法,如果你了解一点儿数据库,就会发现它很像是一段 SQL 查询。查询表达式 总是从一个 from 子句开始,以一个 select 或 group 子句结束。
当我们使用两个 form 时,就会形成“select many”映射,它会把多个查询的结果合并成一个序列。
自然,熟悉 C# 语法风格的你会很“看不惯”这种内联在定义式中、神似另一种语言的东西。因此 C# 还提供了更严谨的函数调用语法,你可以在上面的 查询式语法 和下面的 调用式语法 之间灵活选择,它们产生的结果相同:
var deck = suits.SelectMany(s => ranks.Select(r => new {Suit = s, Rank = r}));
观察上面的例子,我们可以总结出 from 和 select 查询的语法:
-
在查询式语法
from ELEM in SOURCE select RESULT中,将数据源 SOURCE 中的任一元素(用数据库术语说,是任一条记录)用 ELEM 指代,并指定查询结果的每条记录为表达式 RESULT(该表达式中应当含有 ELEM 变量); -
调用式语法的对应形式为:
SOURCE.Select(ELEM => RESULT),当嵌套时,外层的 Select 均要用SelectMany代替。ELEM => RESULT是 Lambda 表达式,如果让你感到陌生,可以复习一下本文第十三章的“Lambda”一节。由于
Select的调用参数是一个 Lambda 表达式,我们其实可以定义更长的 Lambda 执行体,在其中执行一些其它操作,这是调用式 LINQ 给我们带来的额外的灵活性。
二言以蔽之:from 关键字(在调用式语法中不需要)用于指明要查询的数据源并定义元素的指代符号;select 关键字(在调用式语法中对应 Select()、SelectMany())用于获取结果并结束查询。
where 查询
在用 select 获取查询结果之前,我们还可以用 where 子句对返回的结果序列进行筛选。例如,我们可以从上述扑克牌中筛选出四种花色的 J、Q 和 K 牌,而不要其他牌:
var deck = from s in suits
from r in ranks
where r.Length > 3
select new { Suit = s, Rank = r };
或:
var deck = suits.SelectMany(s => ranks.Where(r => r.Length > 3).Select(r => new {Suit = s, Rank = r}));
where 查询的语法:
- 查询式语法为
where COND,其中 COND 是一个布尔类型的表达式,它可以包含在 from 中定义过的元素指代符号(ELEM),当数据源中的某个元素令 COND 为真时,该元素就被选取,否则就被排除,不包含在查询结果中; - 调用式语法为
.Where(ELEM => COND),其中ELEM => COND是一个返回布尔类型的 Lambda 表达式。
LINQ 还提供其他的查询,可以在 这里 找到详细文档,看一看就会了~
迭代器
你可能会关心前面例子中的 deck 是什么类型,它是一个 可枚举 类型:IEnumerable<T>,它在行为上像是一个序列容器,我们可以用一枚指向当前元素的“指针”来访问可枚举类型的对象,这个“指针”称为 迭代器(IEnumerator);调用可枚举对象的 GetEnumerator() 方法,即可获得一个匹配该“容器”的迭代器。
迭代器不仅是一种灵活的访问接口,也是 Unity 中实现协程的原理基础。
在官方 C# 文档中,用“迭代器”一词指代 IEnumerable 和 IEnumerator 这两个概念,并未加以区分;不过为清晰明确起见,本文加以区分。
迭代器的使用
迭代器的使用方法与 C++ 中 STL 容器的迭代器很类似:
-
用迭代器的
Current属性来访问当前元素(类似于 STL 迭代器的解引用); -
用
MoveNext()方法来让迭代器指向下一个元素(类似于 STL 迭代器的自增运算符);MoveNext()会返回一个 bool,表示操作是否成功:当调用该方法之前就已经指向迭代器中的最后一个元素时,方法就会返回 false。 -
用
Reset()方法来将迭代器复原到初始位置。迭代器的初始位置,是指向容器的“第一个元素之前”,此时访问
Current属性会得到null,需要调用一次MoveNext()才能使其真正指向第一个元素 —— 想想为什么要这样设计?
了解了迭代器的行为定义以后,我们就可以这样改写“扑克牌”例子的 24~27 行:
// Get an enumerator of the enumerable "deck":
var card = deck.GetEnumerator();
// Move the enumerator and access all elements in "deck":
while (card.MoveNext()) { Console.WriteLine(card.Current); }
上面的写法与前面 foreach 的写法等价,foreach 其实是 C# 的一个语法糖。
Take 与 Skip
可枚举容器具有 Take 和 Skip 两种方法:
Take(int e)方法用于获得容器中的前 e 个元素组成的新容器;Skip(int e)方法用于跳过容器中的前 e 个元素,返回剩下的元素组成的新容器。
迭代器方法
可枚举容器只是一个抽象的序列容器,我们可以任意指定其中包含 何种元素、如何排序。通过定义 迭代器方法 可以做到这一点。
迭代器方法是一类返回类型为 IEnumerable(常使用泛型 IEnumerable<T>)的方法,它定义该容器的迭代器的行为,即 MoveNext() 和 Current 返回何种值。
与一般方法不同的是,迭代器方法需要用 yield return 来返回,这是一种能够“保存和恢复现场”的返回方式:当函数遇到 yield return 时将会返回,而当该函数再次被调用时,将从上次返回的位置开始,继续向下执行(而不是从头开始执行)。
这种特殊的执行方法并没有用到什么新奇的运行时技术,它只是 C# 的一个语法糖,在编译时会被展开为一系列用于保存上次执行位置的变量和方法,在此不赘述该糖的展开版本。
例如一个最简单的迭代器方法:
static IEnumerable<string> Canines()
{
yield return "Dog";
yield return "Wolf";
yield return "Fox";
yield return "Hyena";
}
调用例:
foreach (var canine in Canines())
{
Console.WriteLine(canine);
}
上面的迭代器方法定义了一个可枚举容器,可以通过调用 Canines() 来取得该容器,它内含了 4 种犬科动物的名称,从该容器中可以依次取出狗、狼、狐狸和鬣狗 —— 这都是我们在迭代器方法中定义的内容。你很容易总结出,该容器的行为与一个 string 数组一致:
string [] canines = {"Dog", "Wolf", "Fox", "Hyena"};
下面用一个洗牌的例子(改自官方文档)展现迭代器方法的灵活性:
public static class Extensions
{
public static IEnumerable<T> InterleaveWith<T>
(this IEnumerable<T> first, IEnumerable<T> second)
{
var firstEnumerator = first.GetEnumerator();
var secondEnumerator = second.GetEnumerator();
bool firstHasRemaining, secondHasRemaining;
do
{
firstHasRemaining = firstEnumerator.MoveNext();
secondHasRemaining = secondEnumerator.MoveNext();
if (firstHasRemaining)
{
yield return firstEnumerator.Current;
}
if (secondHasRemaining)
{
yield return secondEnumerator.Current;
}
} while (firstHasRemaining || secondHasRemaining);
}
}
该迭代器方法是对 IEnumerable<T> 的扩展方法(见第七章“this 参数”一节),我们取得两个容器的迭代器,并通过 yield return 返回元素,将两个容器中的元素交织在一起,且调用本方法的那个容器中的元素(如果有)在先;当两个容器中有一个容器的元素已穷尽时,将连续输出另一个容器中的剩余元素。
我们在前面的扑克例子基础上调用这个例子:
static void Main(string[] args)
{
...
var top = deck.Take(26);
var bottom = deck.Skip(26);
foreach (var card in top.InterleaveWith(bottom))
{
Console.WriteLine(card);
}
}
上面的代码将把 deck 中的牌分成均等的两半(通过 Take() 和 Skip())top 和 bottom,然后调用我们的自定义迭代器函数,得到重新洗到一起的一副牌。
你可以在循环中增加一个计数,看看我们最后得到的是否为完整的 52 张牌。
及早计算与延迟计算
LINQ 采用了延迟计算技术,即:当进行诸如 Select 的查询时,并不真正产生所有查询结果,而是产生一个容器来封装这些结果,当实际访问该容器时,才真正取得这些结果。这有利有弊,对于数据源很少变动或从不变动,但却需要反复访问的查询来说,进行“及早计算”的效率更高。
我们可以用容器的 ToArray() 方法来完成及早计算处理,以前文中获取初始扑克牌组 deck 的查询为例,如果要在查询时立即构成查询结果,可以这样修改:
...
var deck = suits.SelectMany(s => ranks.Select(r => new { Suit = s, Rank = r })).ToArray();
...
同样,构建 top、bottom、洗牌后的牌组等容器时,也可以用 ToArray() 来进行及早计算。你会发现,这实际上是一种缓存处理。
T.B.C.

本文深入探讨C#中的LINQ(Language Integrated Query)与迭代器的使用,包括LINQ的基础语法,如from与select查询、where条件筛选,以及迭代器的创建和使用。通过示例展示了Take与Skip方法,以及如何通过自定义迭代器方法实现数据的交织。此外,还介绍了LINQ的延迟计算特性及其优缺点。

被折叠的 条评论
为什么被折叠?



