Fundamentals – Part 3

07/22/2025
3 minute read

In Part 1 and Part 2, we explored core concepts like value types, reference types, and collections. In Part 3, we tackle something that seems simple but hides a lot under the surface: the foreach loop.

What are the actual requirements for a type to be used in a foreach loop?

C# allows you to use foreach with any type that implements IEnumerable or IEnumerable<T>. This includes arrays, lists, dictionaries, hash sets, and more. The only requirement is that your type must expose a GetEnumerator() method that returns an object with:

  • A MoveNext() method that advances the enumerator.
  • A Current property to access the current item.
  • Optionally, a Dispose() method (for using support).

Even if your type doesn't implement IEnumerable, the compiler will accept it as long as it follows this pattern — meaning "duck typing" applies here.


public class MyCustomCollection
{
    public MyEnumerator GetEnumerator() => new MyEnumerator();
    public struct MyEnumerator
    {
        private int _index;
        public int Current => _index;
        public bool MoveNext() => ++_index < 3;
    }
}


What does the compiler do when it sees a foreach loop?

This is where things get interesting. foreach is syntactic sugar. The compiler rewrites it into a while loop using the

//enumerator pattern:
foreach (var number in numbers)
{
    Console.WriteLine(number);
}
//Is rewritten roughly as:
using (var enumerator = numbers.GetEnumerator())
{
    while (enumerator.MoveNext())
    {
        var number = enumerator.Current;
        Console.WriteLine(number);
    }
}

Understanding this helps you debug issues, especially around deferred execution or modification during enumeration.


How does yield return work under the hood?

When you use yield return, the compiler generates a state machine behind the scenes. Each yield return marks a pause point, and the method returns an enumerator object that tracks the current state.

IEnumerable<int> GetNumbers()
{
    yield return 1;
    yield return 2;
    yield return 3;
}

This is effectively transformed by the compiler into a custom enumerator class with MoveNext, Current, and the appropriate internal state to resume execution between calls.

This is incredibly powerful for creating lazy, memory-efficient sequences without manually writing enumerators.


What are the nuances of deferred execution with foreach?

Many LINQ methods (Select, Where, etc.) use deferred execution — the query isn’t evaluated until you start enumerating it. This means side effects and data changes can produce unexpected behavior if you’re not careful.

var query = myList.Where(x => x > 10);

// List is modified before enumeration myList.Add(99);

// Deferred execution happens here foreach (var item in query) { Console.WriteLine(item); }

This design allows powerful chaining of operations, but you need to know when evaluation occurs. If you need to “lock in” the result, use .ToList() or .ToArray() to force immediate evaluation.


Can I modify a collection inside a foreach loop?

Short answer: no — at least not safely. Most built-in collections will throw an InvalidOperationException if modified during iteration:

foreach (var item in myList)
{
    myList.Remove(item); // ❌ Runtime error
}

//To safely modify a collection, use a temporary list: var toRemove = myList.Where(x => x.ShouldBeRemoved).ToList();

foreach (var item in toRemove) { myList.Remove(item); }


Does foreach behave differently with value types?

Yes. With value types (like struct), foreach gives you a copy of the item — not the original:

foreach (var point in pointList)
{
    point.X = 42; // This modifies a copy, not the actual element in the list
}

//To modify value types, use indexing instead: for (int i = 0; i < pointList.Count; i++) { pointList[i].X = 42; // ✅ This works }

Wrap-up

The foreach loop hides a rich ecosystem of compiler tricks and design patterns — from syntactic sugar, to custom enumerators, to deferred execution and value-type quirks. If you’ve ever taken it for granted, now you know better. In the next part of this series, we’ll keep digging deeper into C# fundamentals that help you write clearer, more performant code.

An error has occurred. This application may no longer respond until reloaded. Reload x