Clarify the observable contract
A lazy operation owns the enumerators it creates even when its consumer stops before the source ends. This paired-stream sum stops at the shorter input and disposes both enumerators through using declarations inside the iterator. Inputs are ordinary enumerable sequences, and the consumer takes only one output. The task concerns lifecycle and consumption boundaries, not another eager array-merge algorithm.
The return value is an enumerable recipe; resources are acquired when enumeration begins. Each call that obtains a new enumerator owns separate traversal state. The paired operation must release both acquired enumerators whether it reaches the shorter source, throws during addition or is disposed early by its consumer.
Walk through the verified program
Enumeration obtains both source enumerators, advances them once and yields one plus ten as eleven. Take one then stops, disposing the outer iterator; its using scopes dispose both sources, whose finally blocks set the two flags. The remaining values two and twenty are never requested. Merely creating the deferred sequence would not have executed either source.
Take one disposes the outer iterator when it stops. Compiler-generated iterator cleanup executes the using scopes, which in turn run the source finally blocks. That is why both flags are true before the second print. The code does not need to enumerate the unused second pair merely to trigger cleanup.
using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
bool leftDisposed=false,rightDisposed=false;
IEnumerable<int> Left() { try { yield return 1; yield return 2; } finally { leftDisposed=true; } }
IEnumerable<int> Right() { try { yield return 10; yield return 20; } finally { rightDisposed=true; } }
Console.WriteLine("pair:"+string.Join(",",Pairs(Left(),Right()).Take(1)));
Console.WriteLine("disposed:"+leftDisposed.ToString().ToLowerInvariant()+","+rightDisposed.ToString().ToLowerInvariant());
static IEnumerable<int> Pairs(IEnumerable<int> left,IEnumerable<int> right) {
using var a=left.GetEnumerator(); using var b=right.GetEnumerator();
while(a.MoveNext() && b.MoveNext()) yield return checked(a.Current+b.Current);
}
Account for time, storage and allocations
For p yielded pairs, work is O(p) with constant auxiliary iterator state, excluding resources owned by the sources. Because the condition advances the left side first, exhaustion of the right side can consume one extra left value. That detail matters for one-shot or side-effecting inputs.
Consumption and production counts can differ at the shorter boundary. The left MoveNext executes before the right one, so an exhausted right side can leave one extra left value consumed but not yielded. Reversing the condition would change which source can advance extra, not make the asymmetry disappear automatically.
Review assumptions and likely defects
Disposing an enumerable object is not the same as disposing the enumerators it creates. Returning a lazy query also extends source-resource lifetimes beyond method return. This sample does not make non-repeatable streams replayable or promise that no source side effect occurs during MoveNext.
A caller can still leak resources by obtaining an enumerator and never disposing or completing it. The API pattern makes correct ownership possible but cannot force every consumer to behave correctly. Also, two successful enumerations of an array-backed fixture do not prove that a file or network source is repeatable.
Practice one: discriminate edge cases
Use a shorter right sequence and count left-side MoveNext observations. Hint: left is evaluated first in the short-circuit condition. Then stop before the second output and verify both finally blocks still run.
Add counters immediately before each source yield, then use a one-element right source and consume the paired operation fully. Hint: observe whether left's second element was requested before right exhaustion was known. Keep early-consumer disposal as a separate test so consumption semantics and resource release are independently verified.
Practice two: defend an extension
Design a lazy API over files or network records with cancellation and explicit ownership. Decide whether callers can enumerate twice, which exceptions escape during iteration and whether a snapshot alternative is worth its allocation and early resource release.
A resource-backed API should document that the returned enumerator owns the open resource until disposal and should support cancellation at appropriate asynchronous operations. A snapshot alternative can close resources before returning at the price of retained data. Neither choice is universally superior; it depends on result size and consumer ownership discipline.
Evaluate correctness and interview reasoning
A rigorous interview explanation identifies acquisition time, disposal responsibility and the exact effect of early Take. It derives the eleven result and both cleanup flags from actual iterator control flow. It also notices the shorter-input extra-read asymmetry, a practical issue hidden by many simple Zip-like examples. A senior answer distinguishes the enumerable recipe from each acquired enumerator and considers a snapshot alternative when callers cannot safely retain resources. This lesson therefore tests lifecycle reasoning and API contracts, while using ordinary BCL iterators that run in the portable console environment.
A working example
using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
bool leftDisposed=false,rightDisposed=false;
IEnumerable<int> Left() { try { yield return 1; yield return 2; } finally { leftDisposed=true; } }
IEnumerable<int> Right() { try { yield return 10; yield return 20; } finally { rightDisposed=true; } }
Console.WriteLine("pair:"+string.Join(",",Pairs(Left(),Right()).Take(1)));
Console.WriteLine("disposed:"+leftDisposed.ToString().ToLowerInvariant()+","+rightDisposed.ToString().ToLowerInvariant());
static IEnumerable<int> Pairs(IEnumerable<int> left,IEnumerable<int> right) {
using var a=left.GetEnumerator(); using var b=right.GetEnumerator();
while(a.MoveNext() && b.MoveNext()) yield return checked(a.Current+b.Current);
}
Expected output
pair:11 disposed:true,true
Use a shorter right sequence and count left-side MoveNext observations. Hint: left is evaluated first in the short-circuit condition. Then stop before the second output and verify both finally blocks still run.
Further reading
Interview questions for this lesson
- Explain: Who owns the enumerators inside the lazy paired-stream operation, and what happens when the consumer stops early?
- Trace: Trace the complete program for own enumerators when consumers stop early. State the exact stdout, then justify the state transitions and relevant edge conditions.
- Design: Design a lazy API over files or network records with cancellation and explicit ownership. Decide whether callers can enumerate twice, which exceptions escape during iteration and whether a snapshot alternative is worth its allocation and early resource release.