惰性求值语义
了解迭代器的延迟执行。
惰性求值语义 是 CoddyKit 上的免费 C# Academy 课时。 这是第 3 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 C# Academy 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 C# Academy 课程共包含 4 节课。
延迟执行
迭代器和 LINQ 查询采用延迟执行:定义 query 几乎不会做任何事情。只有实际逐项枚举结果时,工作才会发生。
枚举之前不会执行任何操作
调用迭代器方法不会运行其主体。只有 foreach(或其他消费者)提取第一个值时,主体才开始执行。
using System;
using System.Collections.Generic;
public class Program
{
static IEnumerable<int> Numbers()
{
Console.WriteLine("-- iterator body started --");
yield return 1;
yield return 2;
}
public static void Main()
{
var seq = Numbers();
Console.WriteLine("query created, body not run yet");
foreach (var n in seq) Console.WriteLine(n);
}
}值会逐个生成
每次迭代只会执行迭代器中足以生成下一个值的部分。这种基于拉取的模型即使面对超大序列也能保持较低的内存占用。
using System;
using System.Collections.Generic;
public class Program
{
static IEnumerable<int> Trace()
{
for (int i = 1; i <= 3; i++)
{
Console.WriteLine("producing " + i);
yield return i;
}
}
public static void Main()
{
foreach (var n in Trace())
Console.WriteLine("consumed " + n);
}
}重新枚举会再次执行
延迟序列没有缓存的结果。枚举两次会执行迭代器两次;如果 source 发生变化或工作开销很大,这一点很重要。
using System;
using System.Collections.Generic;
public class Program
{
static int _calls = 0;
static IEnumerable<int> Counter()
{
_calls++;
yield return _calls;
}
public static void Main()
{
var seq = Counter();
foreach (var n in seq) Console.WriteLine(n);
foreach (var n in seq) Console.WriteLine(n);
Console.WriteLine("total runs: " + _calls);
}
}捕获实时状态
由于执行是延迟的,迭代器会在枚举时而非定义时读取 source。定义 query 后所做的更改都能被看到。
using System;
using System.Collections.Generic;
public class Program
{
public static void Main()
{
var list = new List<int> { 1, 2 };
IEnumerable<int> doubled = Double(list);
list.Add(3); // added before enumeration
foreach (var n in doubled) Console.WriteLine(n);
}
static IEnumerable<int> Double(IEnumerable<int> nums)
{
foreach (var n in nums) yield return n * 2;
}
}强制立即执行
若要快照结果,请使用 ToList() 或 ToArray() 物化序列。这样会运行迭代器一次并存储这些值,使其与 source 后续更改脱钩。
using System;
using System.Collections.Generic;
using System.Linq;
public class Program
{
public static void Main()
{
var list = new List<int> { 1, 2 };
List<int> snapshot = Double(list).ToList(); // executed now
list.Add(99);
Console.WriteLine(string.Join(",", snapshot));
}
static IEnumerable<int> Double(IEnumerable<int> nums)
{
foreach (var n in nums) yield return n * 2;
}
}短路执行可节省工作量
因为消费者会惰性地拉取值,类似查找第一个匹配项的操作可以提前停止。生成者永远不会计算未被请求的值。
using System;
using System.Collections.Generic;
using System.Linq;
public class Program
{
static IEnumerable<int> Numbers()
{
for (int i = 1; ; i++)
{
Console.WriteLine("checking " + i);
yield return i;
}
}
public static void Main()
{
int firstBig = Numbers().First(n => n > 3);
Console.WriteLine("found " + firstBig);
}
}延迟抛出异常
迭代器中的异常不会在调用方法时抛出,而是在枚举到出错的行时抛出。如果您期待在前置阶段完成验证,这可能会让您感到意外。
using System;
using System.Collections.Generic;
public class Program
{
static IEnumerable<int> Risky(int divisor)
{
yield return 10 / divisor; // throws only during enumeration
}
public static void Main()
{
var seq = Risky(0);
Console.WriteLine("no error yet");
try { foreach (var n in seq) Console.WriteLine(n); }
catch (DivideByZeroException) { Console.WriteLine("caught during enumeration"); }
}
}惰性处理链的组合成本很低
链接惰性运算符不会让它们反复运行。每个值都会按需流经整个 pipeline 一次,因此效率高且节省内存。
using System;
using System.Collections.Generic;
using System.Linq;
public class Program
{
public static void Main()
{
var result = Enumerable.Range(1, 1000000)
.Where(n => n % 2 == 0)
.Select(n => n * n)
.Take(3);
foreach (var n in result) Console.WriteLine(n);
}
}何时进行物化
在您将多次枚举、source 可能发生更改,或必须在释放资源前捕获结果时进行物化。否则保持惰性,以节省内存和工作量。
using System;
using System.Collections.Generic;
using System.Linq;
public class Program
{
public static void Main()
{
var evens = Enumerable.Range(1, 10).Where(n => n % 2 == 0).ToList();
// Safe to enumerate many times now
Console.WriteLine("count: " + evens.Count);
Console.WriteLine("sum: " + evens.Sum());
}
}亲自尝试
直接观察延迟执行:定义 query,修改 source,只有在枚举时,更改才会出现在结果中。
using System;
using System.Collections.Generic;
using System.Linq;
public class Program
{
public static void Main()
{
var source = new List<int> { 1, 2, 3 };
var query = source.Where(n => n > 1); // not executed yet
source.Add(10);
source.Remove(2);
// Executes now, reflecting all changes
Console.WriteLine(string.Join(", ", query));
}
}快速检查
思考延迟执行的行为。
回顾
惰性(延迟)求值只会在序列被枚举时运行。
- 定义 query 不会执行任何工作;使用它才会执行。
- 重新枚举会重新运行迭代器。
- 所有 source 都会在枚举时读取,因此之后的更改可以被看到。
- 异常会在枚举期间出现。
- 需要时使用
ToList()/ToArray()进行物化。
常见问题解答
「惰性求值语义」课时是免费的吗?
是的 — 「惰性求值语义」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 C# Academy 课程的其余内容,请升级到 CoddyKit PRO。 C# Academy 课程共包含 4 节课。
「惰性求值语义」这节课中我会学到什么?
了解迭代器的延迟执行。 你通过在浏览器中直接运行的动手代码来练习 C# Academy,全天候 AI 导师会在你学习这节课的过程中回答你的问题。
学习 C# Academy 需要有经验吗?
无需任何先前经验。CoddyKit 上的 C# Academy 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 3 节课,共 4 节。
「惰性求值语义」课时需要多长时间?
大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。
我能在这节 C# Academy 课中编写并运行代码吗?
能。每节 C# Academy 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。