Lazy Evaluation Semantics
Understand deferred execution of iterators.
Lazy Evaluation Semantics is a free C# Academy lesson on CoddyKit — lesson 3 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the C# Academy learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.
Deferred Execution
Iterators and LINQ queries use deferred execution: defining the query does almost nothing. The work only happens when you actually enumerate the result, item by item.
Nothing Runs Until You Enumerate
Calling an iterator method does not run its body. The body starts executing only when a foreach (or another consumer) pulls the first value.
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);
}
}Values Are Produced One at a Time
Each iteration runs just enough of the iterator to produce the next value. This pull-based model keeps memory low even for huge sequences.
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);
}
}Re-enumeration Runs Again
A deferred sequence has no cached result. Enumerating it twice executes the iterator twice, which matters if the source changes or the work is expensive.
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);
}
}Capturing Live State
Because execution is deferred, an iterator reads its source at enumeration time, not at definition time. Changes made after defining the query are visible.
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;
}
}Forcing Immediate Execution
To snapshot results, materialize the sequence with ToList() or ToArray(). This runs the iterator once and stores the values, decoupling from later source changes.
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;
}
}Short-Circuiting Saves Work
Because consumers pull values lazily, an operation like first match can stop early. The producer never computes values that are never requested.
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);
}
}Deferred Exceptions
An exception inside an iterator is not thrown when the method is called, but when enumeration reaches the failing line. This can surprise you if you expect validation up front.
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"); }
}
}Lazy Pipelines Compose Cheaply
Chaining lazy operators does not run them repeatedly. Each value flows through the whole pipeline once, on demand, which is efficient and memory friendly.
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);
}
}When to Materialize
Materialize when you will enumerate multiple times, when the source may change, or when you must capture results before disposing a resource. Otherwise, stay lazy to save memory and work.
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());
}
}Try It Yourself
Observe deferred execution directly: a query is defined, the source mutates, and only at enumeration time do the changes appear in the results.
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));
}
}Quick Check
Reason about deferred execution.
Recap
Lazy (deferred) evaluation runs a sequence only when enumerated.
- Defining a query does no work; consuming it does.
- Re-enumerating re-runs the iterator.
- Sources are read at enumeration time, so later changes are visible.
- Exceptions surface during enumeration.
- Use
ToList()/ToArray()to materialize when needed.
Frequently asked questions
Is the “Lazy Evaluation Semantics” lesson free?
Yes — the full text of “Lazy Evaluation Semantics” is free to read here on the web, and the C# Academy course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the C# Academy course, upgrade to CoddyKit PRO.
What will I learn in “Lazy Evaluation Semantics”?
Understand deferred execution of iterators. You practise C# Academy with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.
Do I need any experience to start C# Academy?
No prior experience is required. C# Academy on CoddyKit is structured for beginners through advanced learners; this is — lesson 3 of 4, so you can start here or from the beginning and move at your own pace.
How long does the “Lazy Evaluation Semantics” lesson take?
Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.
Can I write and run code in this C# Academy lesson?
Yes. Every C# Academy lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.
All lessons in this course
- Iterator Methods with yield return
- yield break and Early Termination
- Lazy Evaluation Semantics
- Custom Enumerables