The yield keyword is one of C#’s more underused features, letting you build custom, lazily-evaluated sequences without manually implementing IEnumerator.
This collection of 25 C# iterator exercises covers writing your own yield return methods, building infinite sequences, and understanding how iterators generate values on demand instead of all at once.
Each exercise includes a Practice Problem, Purpose, Hint, and a full Solution with Explanation, so you understand exactly when and why lazy evaluation matters.
Also, See: C# Exercises with 22 topic-wise sets and 620+ practice questions.
What You’ll Practice
- Fundamentals:
yield returnandyield break. - Custom Sequences: Building your own
IEnumerable<T>methods. - Lazy Evaluation: Understanding deferred execution versus eager loading.
- Practical Use: Infinite sequences and filtered iteration pipelines.
+ Table Of Contents (25 Exercises)
Table of contents
- Exercise 1: Generate Even Numbers with Yield Return
- Exercise 2: Count Down with an Early Exit
- Exercise 3: Filter Vowels from a String
- Exercise 4: Generate an Infinite Sequence Lazily
- Exercise 5: Generate Powers of Two
- Exercise 6: Take Every Nth Element from an Array
- Exercise 7: Stop an Iterator at a Specific Word
- Exercise 8: Reimplement LINQ’s Where Method
- Exercise 9: Reimplement LINQ’s Select Method
- Exercise 10: Generate the Fibonacci Sequence
- Exercise 11: Calculate a Running Total (Prefix Sum)
- Exercise 12: Split a Collection into Chunks
- Exercise 13: Track State Between Two Signal Markers
- Exercise 14: Flatten a Nested Collection
- Exercise 15: Generate Adjacent Pairs from a Sequence
- Exercise 16: Visualize Deferred Execution
- Exercise 17: Observe Finally Block Behavior on Early Exit
- Exercise 18: Build a Zero-Allocation Custom Enumerator
- Exercise 19: Read a Large File Line by Line
- Exercise 20: Interleave Two Sequences
- Exercise 21: Simulate a Paginated API Fetcher
- Exercise 22: Build a Peekable Iterator
- Exercise 23: Loop Through a Collection Forever
- Exercise 24: Chain Multiple Iterators into a Pipeline
- Exercise 25: Stream Data Asynchronously with IAsyncEnumerable
Exercise 1: Generate Even Numbers with Yield Return
Practice Problem: Create a method GetEvens(int max) that uses yield return to generate even numbers from 0 up to max.
Purpose: This exercise helps you practice writing a basic iterator method using yield return, which lets a method produce a sequence of values one at a time without building a list up front.
Given Input: max = 10
Expected Output: Even Numbers Up To 10 = 0, 2, 4, 6, 8, 10
▼ Hint
- Declare the method’s return type as
IEnumerable<int>, sinceyield returnrequires the method to return an iterator type likeIEnumerable<T>. - Use a
forloop starting at 0 that increases by 2 on each pass, callingyield return iinstead of collecting the values into a list.
▼ Solution & Explanation
Explanation:
IEnumerable<int> GetEvens(int max): A method containingyield returnis automatically compiled into a state machine that implementsIEnumerable<int>behind the scenes.yield return i: Produces the current value to the caller and pauses execution right there, resuming from that exact point the next time a value is requested.new List<int>(GetEvens(max)): Fully enumerates the iterator into a concrete list so it can be joined into a single output string.
Exercise 2: Count Down with an Early Exit
Practice Problem: Write an iterator Countdown(int start) that counts down from the starting number to 1, and then uses yield break to stop execution early if a negative number is passed.
Purpose: This exercise helps you practice using yield break to end an iterator immediately, producing an empty sequence without ever entering the main loop.
Given Input: start = 5
Expected Output: Countdown from 5 = 5, 4, 3, 2, 1
▼ Hint
- Check
if (start < 0)as the very first statement in the method, and useyield break;there to exit before the loop even starts. - For the normal case, use a
forloop starting atstartand decrementing down to 1, callingyield return ieach time.
▼ Solution & Explanation
Explanation:
if (start < 0) yield break;: Immediately ends the iterator without producing any values whenever a negative starting number is passed in.for (int i = start; i >= 1; i--): Counts downward from the starting value to 1 inclusive, yielding each number along the way.- Guard clause style: Placing the
yield breakcheck before the loop keeps the invalid input case separate from the main counting logic.
Exercise 3: Filter Vowels from a String
Practice Problem: Create a method FilterVowels(string input) that iterates through a string and yields only the vowels found in it.
Purpose: This exercise helps you practice combining yield return with a filtering condition inside a foreach loop over individual characters.
Given Input: input = "Programming"
Expected Output: Vowels = o, a, i
▼ Hint
- Store the set of vowels, upper and lower case, in a small string like
"aeiouAEIOU", then use.Contains()to check each character against it. - Loop through the input string with a
foreachoverchar, callingyield return characteronly when it is a vowel.
▼ Solution & Explanation
Explanation:
foreach (char character in input): Iterates through the string one character at a time, since astringis itself enumerable as a sequence ofcharvalues.vowels.Contains(character): Checks whether the current character appears anywhere in the small reference string of vowels.yield return character: Only runs when the condition is true, so consonants and other characters are simply skipped without interrupting the loop.
Exercise 4: Generate an Infinite Sequence Lazily
Practice Problem: Write an infinite loop iterator InfiniteNumbers() that yields numbers starting from 1 and increments forever. (Demonstrates how yield evaluates lazily).
Purpose: This exercise helps you practice lazy evaluation, where an iterator with no natural end point is still safe to use as long as the caller only ever asks for a limited number of values.
Given Input: The first 5 values requested from an iterator that would otherwise run forever.
Expected Output: First Five Numbers = 1, 2, 3, 4, 5
▼ Hint
- Use a
while (true)loop inside the iterator method, yielding the current number and then incrementing it on every pass. - Call
.Take(5)on the result before enumerating it, since evaluating an infinite iterator directly into a list would never finish.
▼ Solution & Explanation
Explanation:
while (true) { yield return current; current++; }: The loop genuinely has no exit condition, but that is safe because each value is only produced when the caller actually asks for the next one.InfiniteNumbers().Take(5): Stops pulling values from the iterator after exactly 5 have been produced, so the underlying infinite loop never actually completes, it is simply abandoned once enough values exist.- Lazy evaluation: Calling
InfiniteNumbers()alone does not run any code inside the method yet, execution only begins once something starts enumerating the result, such as.Take(5).ToList().
Exercise 5: Generate Powers of Two
Practice Problem: Implement a method GetPowersOfTwo(int exponentLimit) that yields 2 raised to the power of n for each step until it reaches the limit (2^0, 2^1, 2^2, and so on).
Purpose: This exercise helps you practice yielding a computed value on each iteration, rather than yielding the loop variable itself directly.
Given Input: exponentLimit = 5
Expected Output: Powers of Two (up to 2^5) = 1, 2, 4, 8, 16, 32
▼ Hint
- Loop
exponentfrom 0 up to and includingexponentLimit, and yieldMath.Pow(2, exponent)cast to a whole number type on each pass. - Use
longas the return type instead ofint, since powers of two can grow large quickly for bigger exponent limits.
▼ Solution & Explanation
Explanation:
Math.Pow(2, exponent): Calculates 2 raised to the current exponent, returning adoublethat is then cast down tolong.yield return (long)Math.Pow(2, exponent): Yields the computed power rather than the raw loop variable, showing that the yielded value does not have to match the loop counter directly.exponent <= exponentLimit: Includes the limit itself in the sequence, so anexponentLimitof 5 produces 6 total values, from 2^0 through 2^5.
Exercise 6: Take Every Nth Element from an Array
Practice Problem: Write a method TakeEveryNth<T>(T[] array, int n) that iterates through an array and yields every n-th element.
Purpose: This exercise helps you practice writing a generic iterator method that works with any element type, controlling the loop’s step size instead of always moving one index at a time.
Given Input: numbers = { 10, 20, 30, 40, 50, 60, 70, 80, 90 }, n = 3
Expected Output: Every 3rd Element = 10, 40, 70
▼ Hint
- Use a
forloop with an index variable that increases bynon each pass instead of by 1, so it lands on every n-th position starting from index 0. - Make the method generic with
<T>, so it works on arrays of any type, not just integers.
▼ Solution & Explanation
Explanation:
for (int i = 0; i < array.Length; i += n): Steps through the array n positions at a time instead of one at a time, landing on index 0, then n, then 2n, and so on.TakeEveryNth<T>(T[] array, int n): Being generic overTmeans this same method works equally well on an array of strings, objects, or any other type.yield return array[i]: Produces the element at the current stepped index without needing to build an intermediate array or list of results.
Exercise 7: Stop an Iterator at a Specific Word
Practice Problem: Create an iterator that yields words from a list but uses yield break the moment it encounters a specific “stop word” (e.g., "END").
Purpose: This exercise helps you practice using yield break partway through a loop, rather than only at the very start, to end the sequence as soon as a certain condition is met.
Given Input: words = { "Apple", "Banana", "END", "Cherry", "Date" }, stopWord = "END"
Expected Output: Words Before Stop = Apple, Banana
▼ Hint
- Inside the
foreachloop, check if the current word equals the stop word before yielding it, and callyield break;if it matches. - Only reach the
yield returnstatement for words that are not the stop word, so the stop word itself is never included in the output.
▼ Solution & Explanation
Explanation:
if (word == stopWord) yield break;: Ends the iterator entirely the moment the stop word is reached, so “Cherry” and “Date” are never even visited by the loop.yield return word;: Only runs for words that come before the stop word is encountered.- Ordering matters: Checking for the stop word before the
yield returnline ensures the stop word itself is excluded from the final sequence.
Exercise 8: Reimplement LINQ’s Where Method
Practice Problem: Write your own extension method MyWhere<T>(this IEnumerable<T> source, Func<T, bool> predicate) using yield return to filter elements.
Purpose: This exercise helps you practice recreating LINQ’s Where() from scratch, revealing that it is really just an iterator method filtering elements with yield return underneath the familiar syntax.
Given Input: numbers = { 1, 2, 3, 4, 5, 6, 7, 8 }, predicate n => n % 2 == 0
Expected Output: Even Numbers = 2, 4, 6, 8
▼ Hint
- Loop through
sourcewith aforeach, and callyield return itemonly whenpredicate(item)returnstrue. - Since the method uses the
thismodifier on its first parameter, it can be called directly on anyIEnumerable<T>, exactly like the realWhere().
▼ Solution & Explanation
Explanation:
this IEnumerable<T> source, Func<T, bool> predicate: Extends any sequence with a filtering method that accepts a condition function, exactly mirroring the signature of the built-inWhere().if (predicate(item)) yield return item;: Only produces elements for which the supplied condition function returns true, skipping everything else.numbers.MyWhere(n => n % 2 == 0): Called fluently on a list, exactly as you would use the realWhere()method from LINQ.
Exercise 9: Reimplement LINQ’s Select Method
Practice Problem: Write your own extension method MySelect<TSource, TResult>(this IEnumerable<TSource> source, Func<TSource, TResult> selector) to transform elements.
Purpose: This exercise helps you practice recreating LINQ’s Select(), this time with two separate generic type parameters since the output type can differ from the input type.
Given Input: numbers = { 1, 2, 3, 4 }, selector n => n * n
Expected Output: Squares = 1, 4, 9, 16
▼ Hint
- Loop through
sourcewith aforeach, callingyield return selector(item)for every element, with no filtering condition involved. - Use two type parameters,
TSourcefor the input element type andTResultfor the transformed output type, sinceSelect()can change the type entirely.
▼ Solution & Explanation
Explanation:
TSourceandTResult: Two separate type parameters allow the method to transform a sequence of one type into a sequence of a completely different type, not just filter the original type.yield return selector(item): Yields the transformed result of applying the selector function to each element, rather than the original element itself.numbers.MySelect(n => n * n): In this call bothTSourceandTResulthappen to beint, but they could just as easily differ, for example projecting integers into strings.
Exercise 10: Generate the Fibonacci Sequence
Practice Problem: Implement a GetFibonacci(int count) iterator. Maintain the internal state of the previous two numbers to yield the sequence up to the specified count.
Purpose: This exercise helps you practice how an iterator method can maintain its own running state between each yield return, without needing any external variables or a class field.
Given Input: count = 8
Expected Output: First 8 Fibonacci Numbers = 0, 1, 1, 2, 3, 5, 8, 13
▼ Hint
- Keep two local variables,
previousandcurrent, starting at 0 and 1, updating both together on every iteration. - Loop
counttimes, yieldingpreviouseach time, then computing the next Fibonacci number asprevious + currentbefore shifting both variables forward.
▼ Solution & Explanation
Explanation:
int previous = 0; int current = 1;: Initializes the two running values needed to compute each subsequent Fibonacci number.yield return previous;: Yields the current Fibonacci number before it gets updated for the next iteration.previous = current; current = next;: Shifts both tracked values forward by one position in the sequence, ready for the following iteration.- Persisted state: Because this is a compiler generated state machine,
previousandcurrentretain their values automatically between each paused and resumed call, with no extra class fields required.
Exercise 11: Calculate a Running Total (Prefix Sum)
Practice Problem: Create an iterator RunningTotal(IEnumerable<int> numbers) that yields the cumulative sum at each step. For input [1, 2, 3], it yields 1, then 3, then 6.
Purpose: This exercise helps you practice keeping a running accumulator inside an iterator, yielding an updated value on every pass instead of yielding the raw input elements.
Given Input: numbers = { 1, 2, 3 }
Expected Output: Running Totals = 1, 3, 6
▼ Hint
- Declare a
totalvariable outside the loop, initialized to 0, so its value persists across everyyield return. - Inside the
foreach, add the current number tototalfirst, then yield the updatedtotal, not the original number.
▼ Solution & Explanation
Explanation:
int total = 0;: Declared once, outside the loop, so the compiler generated state machine keeps this value alive between every paused and resumed iteration.total += number; yield return total;: Adds the current element to the accumulator before yielding it, so each yielded value reflects the sum of everything seen so far.- Input flexibility: Accepting
IEnumerable<int>rather thanList<int>means this method works on arrays, other iterators, or any other sequence of integers.
Exercise 12: Split a Collection into Chunks
Practice Problem: Write an iterator Chunk<T>(IEnumerable<T> source, int size) that breaks a flat collection into smaller arrays or lists of the specified size.
Purpose: This exercise helps you practice building up a temporary buffer inside an iterator and yielding it whenever it reaches a target size, then starting a fresh buffer for the next group.
Given Input: numbers = { 1, 2, 3, 4, 5, 6, 7 }, size = 3
Expected Output:
Chunk = 1, 2, 3 Chunk = 4, 5, 6 Chunk = 7
▼ Hint
- Keep a
List<T>buffer, adding one element per pass, andyield returnit once its count reachessize, then replace it with a brand new empty list. - After the main loop ends, check if the buffer still has any leftover elements and yield that final partial chunk too, since it may not have reached full size.
▼ Solution & Explanation
Explanation:
if (currentChunk.Count == size): Yields the buffer the instant it reaches the requested chunk size, then immediately starts a fresh, empty buffer for the next group.if (currentChunk.Count > 0) yield return currentChunk;: Handles the final, possibly incomplete chunk after the loop finishes, so no trailing elements are silently dropped.IEnumerable<List<T>>: The return type is a sequence of lists, since each yielded value is itself an entire group of elements rather than a single item.
Exercise 13: Track State Between Two Signal Markers
Practice Problem: Create a method that reads a list of transactional logs and only yields logs that occur after a "START" signal and before a "STOP" signal.
Purpose: This exercise helps you practice maintaining a simple boolean flag inside an iterator to track whether the current position falls within a target range defined by marker values.
Given Input: logs = { "Init", "START", "Login", "Purchase", "STOP", "Logout" }
Expected Output: Logs Between START and STOP = Login, Purchase
▼ Hint
- Track an
isRecordingboolean, flipping it totruewhen the loop reaches"START"and back tofalseat"STOP". - Use
continueafter handling the marker values themselves, so the markers are never yielded, only the logs strictly between them.
▼ Solution & Explanation
Explanation:
bool isRecording = false;: A simple flag that represents whether the iterator is currently inside the START and STOP range.if (log == "START") { isRecording = true; continue; }: Flips the flag on and skips to the next log without yielding the marker itself.if (isRecording) yield return log;: Only produces logs while the flag is currently true, meaning strictly between a START and its matching STOP.
Exercise 14: Flatten a Nested Collection
Practice Problem: Write an iterator Flatten<T>(IEnumerable<IEnumerable<T>> nestedList) that takes a list of lists and yields a single, flat stream of elements.
Purpose: This exercise helps you practice nesting one foreach loop inside another within an iterator method, yielding from the innermost loop to collapse two levels of structure into one.
Given Input: nestedNumbers = { { 1, 2, 3 }, { 4, 5 }, { 6, 7, 8, 9 } }
Expected Output: Flattened = 1, 2, 3, 4, 5, 6, 7, 8, 9
▼ Hint
- Use an outer
foreachto walk through each inner list, then a nested innerforeachto walk through each element within that inner list. - Call
yield return itemfrom inside the innermost loop, so every element from every sub-list ends up in a single continuous output sequence.
▼ Solution & Explanation
Explanation:
foreach (IEnumerable<T> innerList in nestedList): Walks through each individual sub-collection contained in the outer sequence.foreach (T item in innerList) { yield return item; }: Yields every element from the current sub-collection before the outer loop moves on to the next one.IEnumerable<IEnumerable<T>>: The parameter type explicitly represents a sequence of sequences, which is exactly what “a list of lists” means in generic terms.
Exercise 15: Generate Adjacent Pairs from a Sequence
Practice Problem: Implement ToPairs<T>(IEnumerable<T> source) that yields tuples of adjacent elements. For [1, 2, 3, 4], it yields (1, 2), (2, 3), and (3, 4).
Purpose: This exercise helps you practice manually driving an IEnumerator<T> with MoveNext() inside an iterator method, useful whenever the logic needs to look at two elements at once instead of just one.
Given Input: numbers = { 1, 2, 3, 4 }
Expected Output:
(1, 2) (2, 3) (3, 4)
▼ Hint
- Get the enumerator manually with
source.GetEnumerator(), callMoveNext()once up front to read the very first element, and exit early withyield breakif the sequence is empty. - Loop by calling
MoveNext()again for each subsequent element, yielding a tuple of the previous and current values, then updatingpreviousbefore the next pass.
▼ Solution & Explanation
Explanation:
using (IEnumerator<T> enumerator = source.GetEnumerator()): Manually obtains the sequence’s own enumerator, giving direct control over when to advance instead of relying on an automaticforeach.if (!enumerator.MoveNext()) yield break;: Immediately ends the iterator with no output if the source sequence turns out to be empty.yield return (previous, current);: Produces a value tuple pairing the previously seen element with the one just read, thenpreviousis updated so the next pair overlaps correctly.
Exercise 16: Visualize Deferred Execution
Practice Problem: Write an iterator that prints a message to the console inside the yield return loop. Write a test program that calls the method but doesn’t loop through it with foreach to prove that code isn’t executed until iteration begins.
Purpose: This exercise helps you practice observing deferred execution directly, by printing markers before, during, and after the call to confirm exactly when the iterator’s own code actually runs.
Given Input: None, the program itself demonstrates the timing through console output.
Expected Output:
Calling GetNumbersWithLogging()... Method call returned, but no logging has happened yet. Now starting to iterate: Iterator method started executing. About to yield 1 Received 1 About to yield 2 Received 2 About to yield 3 Received 3
▼ Hint
- Store the result of calling the iterator method in a variable first, and print a message right after that call but before any
foreachbegins. - Notice that none of the iterator’s own
Console.WriteLine()calls appear until theforeachloop actually starts pulling values.
▼ Solution & Explanation
Explanation:
IEnumerable<int> numbers = GetNumbersWithLogging();: This line only creates the compiler generated state machine object, it does not run any code inside the method body yet.- Deferred start: “Iterator method started executing.” only prints once the
foreachloop callsMoveNext()for the first time, which is the true starting point of the method’s logic. - Interleaved output: “About to yield” and “Received” alternate for each number, showing that execution pauses inside the iterator after every single
yield returnand resumes only when the next value is requested.
Exercise 17: Observe Finally Block Behavior on Early Exit
Practice Problem: Create an iterator that wraps its logic in a try-finally block. Observe and document when the finally block executes if the consumer breaks out of the foreach loop early.
Purpose: This exercise helps you practice understanding that breaking out of a foreach loop over an iterator still triggers any finally block inside it, since the loop calls Dispose() on the enumerator behind the scenes.
Given Input: A loop that breaks as soon as it receives the number 3, out of a possible 5.
Expected Output:
Received 1 Received 2 Received 3 Breaking out of the loop early. Finally block executed: cleanup ran.
▼ Hint
- Wrap the entire
forloop containing theyield returnstatements in atryblock, with afinallyblock printing a cleanup message underneath it. - Break out of the consuming
foreachloop partway through, then notice that thefinallymessage still appears immediately afterward, even though theforloop never reached its natural end.
▼ Solution & Explanation
Explanation:
try { ... } finally { ... }: Guarantees the cleanup message prints whenever the iterator stops running, whether it finishes naturally or is abandoned partway through.break;inside the consumingforeach: Causes the compiler generatedforeachcode to callDispose()on the iterator’s enumerator as it exits, and thatDispose()call is what triggers the pendingfinallyblock.- Timing: The cleanup message appears right after “Breaking out of the loop early.” rather than at the very end of the program, since disposal happens immediately when the loop is exited.
Exercise 18: Build a Zero-Allocation Custom Enumerator
Practice Problem: Avoid IEnumerable entirely. Implement a custom GetEnumerator() method on a custom struct that returns a custom struct enumerator with MoveNext() and a Current property. (This prevents boxing allocations).
Purpose: This exercise helps you practice the “duck typing” pattern that foreach actually relies on: any type with a public GetEnumerator() method returning something with MoveNext() and Current works in a foreach loop, with no interface required at all.
Given Input: range = new NumberRange(1, 5)
Expected Output:
Number = 1 Number = 2 Number = 3 Number = 4 Number = 5
▼ Hint
- Define a
NumberRangestruct storing a start and end value, with a publicGetEnumerator()method that returns a separateRangeEnumeratorstruct. - The
RangeEnumeratorstruct needs aCurrentproperty and aMoveNext()method that advances an internal counter and returns whether a valid value is available.
▼ Solution & Explanation
Explanation:
public RangeEnumerator GetEnumerator(): The compiler only requires a method with this exact name and shape, it never checks whetherNumberRangeactually implementsIEnumerable.public int Current => current;andpublic bool MoveNext(): These two members are exactly whatforeachlooks for on the returned enumerator type, matching the pattern without implementing any interface.- Zero allocation: Since both
NumberRangeandRangeEnumeratorarestructtypes used directly rather than through anIEnumerableorIEnumeratorinterface reference, no boxing allocation occurs on the heap when theforeachloop runs.
Exercise 19: Read a Large File Line by Line
Practice Problem: Write a method ReadLines(string filePath) that opens a StreamReader and yields lines one by one. Ensure the file stream is properly disposed of if the caller stops iterating early.
Purpose: This exercise helps you practice combining yield return with a using block around a file handle, so the file is guaranteed to close whether the caller reads every line or abandons the loop partway through.
Given Input: filePath = "data.txt", assumed to contain a line with the text "STOP" partway through the file.
Expected Output: Each line prints as it is read, until a line containing "STOP" is found, at which point the loop breaks early.
Note: since this program reads an actual file from disk, the real output depends entirely on data.txt‘s contents and would throw a FileNotFoundException if that file does not exist at the given path.
▼ Hint
- Wrap the
StreamReaderin ausingblock that surrounds the entire loop, so itsDispose()runs automatically once the iterator stops, for any reason. - Use a
while ((line = reader.ReadLine()) != null)loop, callingyield return linefor each one, sinceReadLine()returnsnullonce the end of the file is reached.
▼ Solution & Explanation
Explanation:
using (StreamReader reader = new StreamReader(filePath)): Wrapping the entire loop in this block ensures the file handle is released whenever the iterator stops, whether that happens by reaching the end of the file or by the consumer breaking out early.break;in the consuming loop: Just like the earliertry-finallyexercise, breaking out of theforeachtriggers disposal of the iterator, which in turn runs theusingblock’s cleanup and closes the file.- Memory efficiency: Because lines are yielded one at a time instead of being loaded into a list all at once, this approach can handle files far larger than what would comfortably fit in memory.
Exercise 20: Interleave Two Sequences
Practice Problem: Implement Interleave<T>(IEnumerable<T> first, IEnumerable<T> second) which alternates yielding an element from the first collection, then the second collection, until both are exhausted.
Purpose: This exercise helps you practice manually driving two enumerators side by side within a single iterator, handling the case where one sequence runs out before the other.
Given Input: first = { 1, 3, 5 }, second = { 2, 4, 6, 8, 10 }
Expected Output: Interleaved = 1, 2, 3, 4, 5, 6, 8, 10
▼ Hint
- Get an enumerator for each sequence and call
MoveNext()on both once before entering the main loop, tracking whether each one still has more elements. - Inside a loop that continues while either sequence still has elements, yield from whichever ones are still available, advancing each enumerator independently right after it is used.
▼ Solution & Explanation
Explanation:
bool firstHasNext = firstEnumerator.MoveNext();: Primes both enumerators before the loop starts, capturing whether each one has at least a first element available.while (firstHasNext || secondHasNext): Keeps looping as long as at least one of the two sequences still has remaining elements, not requiring both to still be active.- Uneven lengths: Once
firstruns out after its third element,firstHasNextbecomes false and its block is simply skipped on every remaining pass, lettingsecondfinish yielding its own leftover values on its own.
Exercise 21: Simulate a Paginated API Fetcher
Practice Problem: Simulate fetching data from a paginated API. Write an iterator that yields items one by one, but under the hood, it fetches the next page of 100 items only when the current page is completely consumed.
Purpose: This exercise helps you practice a real world use of deferred execution, where an expensive operation like a network call only happens exactly when it is actually needed, and not a moment sooner.
Given Input: pageSize = 100, totalPages = 3, but only the first 5 items are ever consumed.
Expected Output:
Fetching page 1 from the API... Consumed item 1 Consumed item 2 Consumed item 3 Consumed item 4 Consumed item 5
Note: “Fetching page 2” and “Fetching page 3” never print at all, since consuming only 5 of the 100 items on page 1 never forces the iterator to request the next page.
▼ Hint
- Write a separate
FetchPage()helper method that simulates the network call and prints a message every time it runs, so its calls are easy to spot in the output. - Inside the iterator, call
FetchPage()once per page in an outer loop, then yield each item from that page in an inner loop before moving on to fetch the next page.
▼ Solution & Explanation
Explanation:
FetchPage(pageNumber, pageSize): Simulates the actual API call, only running when the outerforloop inside the iterator reaches a new page number.- Lazy fetching: Because the inner
foreachonly yields items from the page already in memory, page 2 is never fetched at all if the caller stops consuming before exhausting page 1. - Real world parallel: This is exactly the mechanism many client libraries use to page through large API results without ever loading more data than the caller actually asked for.
Exercise 22: Build a Peekable Iterator
Practice Problem: Create a custom iterator class that allows the consumer to “Peek” at the next element in the sequence without advancing the actual cursor of the iteration.
Purpose: This exercise helps you practice building a small stateful wrapper class around a plain IEnumerator<T>, buffering a single lookahead value so it can be inspected without being consumed.
Given Input: numbers = { 10, 20, 30 }
Expected Output:
Current = 10 Peeked = 20 Current = 20 Current = 30
▼ Hint
- Store a
hasBufferedflag and abufferedValuefield.Peek()should only advance the underlying enumerator if nothing is already buffered. - Your own
MoveNext()equivalent should first check the buffer, returning and clearing it if present, and only fall back to advancing the underlying enumerator directly when the buffer is empty.
▼ Solution & Explanation
Explanation:
Peek(): Advances the underlying enumerator only if nothing is buffered yet, storing the value it finds rather than handing it directly to the caller as the current element.MoveNext(out T current): Checks the buffer first, if a peeked value is waiting there it is returned and cleared, otherwise the method falls through to pulling a fresh value from the underlying enumerator.- Why the trace works: Calling
Peek()after reading 10 buffers 20 without consuming it, so the very nextMoveNext()still returns that same buffered 20 rather than skipping ahead to 30.
Exercise 23: Loop Through a Collection Forever
Practice Problem: Implement an iterator LoopForever<T>(IEnumerable<T> source) that cycles through the collection indefinitely, starting back at the beginning when it reaches the end.
Purpose: This exercise helps you practice wrapping a finite sequence inside an infinite outer loop, similar in spirit to the earlier infinite numbers exercise but repeating existing data instead of generating new values.
Given Input: colors = { "Red", "Green", "Blue" }, taking the first 8 values.
Expected Output: First 8 Items = Red, Green, Blue, Red, Green, Blue, Red, Green
▼ Hint
- Wrap a
foreach (T item in source)loop inside an outerwhile (true), so once the inner loop reaches the end ofsourceit simply starts over from the beginning. - Since this iterator never ends on its own, always call
.Take(n)on it before enumerating, exactly as with any other infinite iterator.
▼ Solution & Explanation
Explanation:
while (true) { foreach (T item in source) { yield return item; } }: The outer loop has no exit condition, so once the innerforeachfinishes one full pass throughsource, it restarts automatically from the first element again.LoopForever(colors).Take(8): Pulls exactly 8 values total, which is enough to wrap around the 3 element list and start a third partial cycle.- Same safety rule as infinite sequences: Enumerating this iterator directly into a list without a
Take()call first would never terminate, exactly like the earlierInfiniteNumbers()exercise.
Exercise 24: Chain Multiple Iterators into a Pipeline
Practice Problem: Create a system where multiple iterator methods are chained together (e.g., ReadInput() -> FilterInvalid() -> TransformData() -> CleanOutput()). Write a test demonstrating how an item passes through the entire pipeline before the next item is processed.
Purpose: This exercise helps you practice how chained iterators behave as a “pull” pipeline: each stage only asks the previous stage for a new value when it actually needs one, so a single item flows all the way through before the next item is even read.
Given Input: rawItems = { "12", "abc", "45", "-3", "78" }
Expected Output:
[ReadInput] Read: 12 [FilterInvalid] Passed: 12 [TransformData] 12 -> 24 [CleanOutput] Formatted: Result: 24 Final: Result: 24 --- [ReadInput] Read: abc [FilterInvalid] Rejected: abc [ReadInput] Read: 45 [FilterInvalid] Passed: 45 [TransformData] 45 -> 90 [CleanOutput] Formatted: Result: 90 Final: Result: 90 --- [ReadInput] Read: -3 [FilterInvalid] Rejected: -3 [ReadInput] Read: 78 [FilterInvalid] Passed: 78 [TransformData] 78 -> 156 [CleanOutput] Formatted: Result: 156 Final: Result: 156 ---
▼ Hint
- Each stage should accept the previous stage’s
IEnumerable<T>as a parameter andforeachover it, printing a log line and yielding whenever an item passes through that stage. - Compose the whole chain by nesting the calls:
CleanOutput(TransformData(FilterInvalid(ReadInput()))), then enumerate only the outermost result inMain().
▼ Solution & Explanation
Explanation:
- Pull, not push: The outer
foreachinMain()requests a value fromCleanOutput, which requests one fromTransformData, which requests one fromFilterInvalid, which requests one fromReadInput, all the way down the chain before anything is printed. - Rejected items never reach later stages: When
FilterInvalidrejects “abc”, it does not yield anything, so it silently loops around to pull the next raw item fromReadInputitself, withoutTransformDataorCleanOutputever being involved for that particular value. - One item at a time end to end: Notice “12” fully completes its journey through all four stages, printing “Final: Result: 24”, before “abc” is even read by
ReadInput, confirming the pipeline processes one item completely before starting the next.
Exercise 25: Stream Data Asynchronously with IAsyncEnumerable
Practice Problem: Upgrade your iterator skills to modern C#. Write an async iterator FetchDataAsync() that uses await Task.Delay() to simulate network latency, combined with yield return to stream data asynchronously.
Purpose: This exercise helps you practice IAsyncEnumerable<T>, which combines everything from regular iterators with async/await, letting a method yield values over time instead of only one at a time synchronously.
Given Input: Three simulated records, each delayed by 500 milliseconds before being yielded.
Expected Output:
Received Record A at 10:15:32 Received Record B at 10:15:33 Received Record C at 10:15:33
Note: this requires C# 8 or later along with .NET Core 3.0 or later, since IAsyncEnumerable<T> and await foreach did not exist in earlier versions. The exact timestamps will differ every time the program runs, but each line should appear roughly 500 milliseconds after the previous one.
▼ Hint
- Mark the method as
static async IAsyncEnumerable<string> FetchDataAsync(), combining theasynckeyword with anIAsyncEnumerable<T>return type. - Inside the loop,
await Task.Delay(500)before eachyield returnto simulate network latency between items. - Consume it with
await foreach (string item in FetchDataAsync())instead of a regularforeach, since the sequence itself is now asynchronous.
▼ Solution & Explanation
Explanation:
async IAsyncEnumerable<string> FetchDataAsync(): Marks this as an asynchronous iterator, allowingawaitandyield returnto be used together inside the same method body.await Task.Delay(500): Pauses the method asynchronously before producing each item, simulating the latency of a real network request without blocking any thread while waiting.await foreach (string item in FetchDataAsync()): The asynchronous counterpart to a regularforeach, awaiting each item as it becomes available instead of requiring the entire sequence to be ready up front.

Leave a Reply