Unity C# 爆破计划(十五):LINQ 与迭代器

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


十五、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 容器的迭代器很类似:

  1. 用迭代器的 Current 属性来访问当前元素(类似于 STL 迭代器的解引用);

  2. MoveNext() 方法来让迭代器指向下一个元素(类似于 STL 迭代器的自增运算符);

    MoveNext() 会返回一个 bool,表示操作是否成功:当调用该方法之前就已经指向迭代器中的最后一个元素时,方法就会返回 false。

  3. 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 两种方法:

  1. Take(int e) 方法用于获得容器中的前 e 个元素组成的新容器;
  2. 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.

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值