迭代器

如果要返回一个其元素可供调用方逐个使用的序列,而无需首先在集合中生成完整的结果,请选择迭代器。 此方法适合处理生成的值、带有自定义控制流的筛选操作,以及调用方可能提前停止读取的序列。 与已完成的集合相比,迭代器改变的是工作发生的时机,而不只是代码的写法。

Tip

本文介绍面向了解方法、循环和集合的开发人员的同步迭代器。 有关完整的 yield 语法和限制,请参阅 yield 语句参考。

迭代器方法逐个生成值序列中的值。 本文重点介绍返回 IEnumerable<T> 的迭代器方法,调用方通常使用 foreach 语句来使用其值。

生成元素 yield return

使用 yield return 提供序列中的下一个元素。 以下迭代器从传感器中选择可接受的温度读数。 在此方案中,-1 标记着传感器数据的结束。

int[] readings = [21, 27, 24, -1, 19];

IEnumerable<int> acceptedReadings =
    SelectTemperatures(readings, maximum: 25);

Console.WriteLine("Sequence created.");

foreach (int temperature in acceptedReadings)
{
    Console.WriteLine($"Accepted: {temperature}°C");
}

static IEnumerable<int> SelectTemperatures(
    IEnumerable<int> readings,
    int maximum)
{
    foreach (int reading in readings)
    {
        Console.WriteLine($"Checking {reading}°C");

        if (reading == -1)
        {
            yield break;
        }

        if (reading <= maximum)
        {
            yield return reading;
        }
    }
}

SelectTemperatures 返回值 IEnumerable<int>,表示值序列 int 。 每个 yield return reading 都为 foreach 循环提供一个值。 调用方处理该值后,迭代器将继续处理下一个输入项。

对大多数新式 C# 代码使用通用 IEnumerable<T> 形式。 类型参数(如 int)告知调用方序列包含的元素类型。

以 yield break 结束该序列

使用 yield break 可在控制流到达方法末尾之前结束迭代器。 在此示例中,sentinel 值 -1 意味着没有更多可用读数:

if (reading == -1)
{
    yield break;
}

当执行到达其正文末尾时,迭代器也会自然结束。 当条件应立即停止元素生产时使用 yield break 。

通过 消耗迭代器

迭代器公开与许多 .NET 集合相同的 IEnumerable<T> 结构。 调用方无需知道序列如何生成其元素即可使用 foreach :

foreach (int temperature in SelectTemperatures(readings, 25))
{
    Console.WriteLine($"Accepted: {temperature}°C");
}

调用方在循环向前推进时请求每个元素。 迭代器在请求之间保持其位置,并在前一个 yield return之后继续。

了解延迟元素生成

调用迭代器方法不会立即运行其函数体。 当调用方开始枚举时,该方法开始生成元素。 此行为称为 延迟执行。

该示例会先打印 Checking ...,然后再打印任何 Sequence created. 消息。 仅当 foreach 循环请求其第一个元素时,迭代器才会开始检查读数。

延迟执行具有实际效果:

  • 调用方可以在迭代器生成每个元素之前开始处理。
  • 迭代器可以使用 yield break 提前停止。
  • 从不枚举序列的调用方不会运行迭代器正文。
  • 每次新的遍历都会再次执行该迭代器。

在方法的用途和文档中明确延迟执行。 读者无法仅凭 IEnumerable<T> 推断执行时机,因为迭代器和集合都可以公开该接口。 使元素生成避免产生无关的副作用,因为调用方可能会稍后或多次枚举该序列。 该示例的诊断消息说明了该时序。

选择迭代器或集合

当值自然地逐个到达,或调用方不需要立即获得完整结果时,返回迭代器。 当调用方需要稳定的快照、索引访问或重复枚举而不重新计算元素时,返回完整的集合。

这两种方法都可以返回相同的元素。 迭代器将元素的生成交由调用方控制;生成集合的方法在返回之前完成该工作。

如果要对每个项执行操作而不返回序列,请使用普通循环。 对于标准筛选器或转换,首选 LINQ 操作,例如 Where 或 Select;当自定义元素生成逻辑作为一系列语句更清晰时,编写迭代器。

迭代器方法可与 LINQ 组合,因为两者都使用 IEnumerable<T>。 例如,调用方可以添加另一个筛选器:

var comfortableReadings = SelectTemperatures(readings, 25)
    .Where(static temperature => temperature >= 18);

迭代器和 LINQ 操作将保持延迟执行,直到代码枚举 comfortableReadings 为止。

有关异步序列,请参阅 “生成和使用异步流”。 对于自定义枚举模式和所有 yield 限制,请使用 语言参考。

另请参阅