Iterating Through Collections - Looping Over Arrays, Lists, and More

Vaibhav • September 10, 2025

Once you can create and initialize collections, the next essential skill is iteration - visiting each element to read it, transform it, or make decisions. In C#, you’ll most often iterate with foreach and for. In this article, we’ll explore practical patterns for arrays, List<T>, Dictionary<TKey,TValue>, HashSet<T>, and the queue/stack collections. We’ll also cover safe removal during iteration, nested loops, and a brief look at enumerators so you understand what foreach does behind the scenes.

Why iterate?

Iteration turns a collection from “stored data” into “useful results.” You iterate to print items, compute totals, search for a value, or transform elements. Good iteration code is clear about three things:

  • What you’re visiting (the collection).
  • How you’re visiting (foreach vs. for and why).
  • What you’re doing with each element (read/modify/remove).

foreach - the simplest way to visit every element

foreach is designed for readability. It automatically gets an enumerator, advances through the collection, and exposes each element in turn.

var primes = new List<int> { 2, 3, 5, 7, 11 };

foreach (int p in primes)
{
    Console.WriteLine(p);
}
  • var primes = ... creates and seeds a list of integers.
  • foreach (int p in primes) visits each integer in order and places it into the loop variable p.
  • Inside the loop, we print each value.

Iterating arrays: foreach vs. for

Arrays are indexable, so both foreach and for are common. Use foreach for read-only passes; use for when you need the index or plan to modify elements by index.

Read-only pass with foreach

int[] scores = { 10, 20, 30, 40 };

foreach (int s in scores)
{
    Console.WriteLine(s);
}
  • The foreach loop variable s receives each array element in sequence.
  • This is ideal for printing, aggregating, or searching without changing the array.

Modifying elements with for

int[] data = { 1, 2, 3, 4, 5 };

// Double each element in place
for (int i = 0; i < data.Length; i++)
{
    data[i] = data[i] * 2;
}
  • for gives you the index i so you can write back to data[i].
  • foreach does not let you assign to the iteration variable; to modify elements, prefer for with an index.

Iterating a List<T>

Lists preserve insertion order and support indexing. Choose based on your needs:

  • Use foreach when reading each element.
  • Use for when you need the index or plan to update elements or remove by index.
var names = new List<string> { "Ada", "Linus", "Grace" };

// Read all
foreach (string name in names)
{
    Console.WriteLine(name);
}

// Update by index
for (int i = 0; i < names.Count; i++)
{
    names[i] = names[i].ToUpper();
}
  • The first loop prints items in the order they were added.
  • The second loop uses i to replace each list element with an uppercased version.

Iterating a Dictionary<TKey,TValue>

Dictionaries store key/value pairs. You can iterate entries, just keys, or just values.

Entries (KeyValuePair<TKey,TValue>)

var http = new Dictionary<int, string>
{
    [200] = "OK",
    [404] = "Not Found",
    [500] = "Server Error"
};

foreach (KeyValuePair<int, string> entry in http)
{
    Console.WriteLine(entry.Key + ": " + entry.Value);
}
  • Each entry has a .Key and .Value.
  • This form is best when you need both.

Keys only / Values only

// Keys
foreach (int code in http.Keys)
{
    Console.WriteLine(code);
}

// Values
foreach (string text in http.Values)
{
    Console.WriteLine(text);
}
  • http.Keys iterates just the keys; http.Values iterates just the values.
  • Use whichever makes your intent clearest.

Iterating a HashSet<T>

A set contains unique items and does not support indexing. You typically iterate it with foreach:

var features = new HashSet<string> { "DarkMode", "Offline", "Export" };

foreach (string f in features)
{
    Console.WriteLine(f);
}
  • Use sets when you care about membership (Contains) more than order.
  • Because there’s no index, a for loop doesn’t apply.

Iterating Queue<T> and Stack<T>

For these structures, there are two common patterns: enumerate without modifying, or process by removing items until empty.

Read without modifying

var q = new Queue<string>();
q.Enqueue("A"); q.Enqueue("B"); q.Enqueue("C");

foreach (string item in q)
{
    Console.WriteLine(item); // A, then B, then C
}

var s = new Stack<int>();
s.Push(1); s.Push(2); s.Push(3);

foreach (int n in s)
{
    Console.WriteLine(n); // 3, then 2, then 1
}
  • Enumerating a Queue<T> shows items in FIFO order.
  • Enumerating a Stack<T> shows items from top to bottom (LIFO).

Process and remove

// Dequeue until empty
while (q.Count > 0)
{
    string next = q.Dequeue();
    Console.WriteLine("Processing " + next);
}

// Pop until empty
while (s.Count > 0)
{
    int top = s.Pop();
    Console.WriteLine("Handling " + top);
}
  • When your goal is to process and clear the structure, use a while loop that checks .Count.
  • Avoid mixing foreach with Dequeue/Pop - changing the collection during enumeration is unsafe.

Breaking, continuing, and early exit

You can use break to stop iteration early and continue to skip to the next element.

var nums = new List<int> { 3, 9, 12, 27, 30 };

foreach (int n in nums)
{
    if (n % 2 == 0)
        continue;         // skip even numbers

    Console.WriteLine(n); // only odd numbers

    if (n > 20)
        break;            // stop once we see an odd number > 20
}
  • continue jumps to the next iteration immediately.
  • break exits the loop entirely.

Nested iteration (collections within collections)

Sometimes you need to walk a structure like List<List<T>>. Keep each loop focused and name variables carefully.

var groups = new List<List<string>>
{
    new List<string> { "Alice", "Bob" },
    new List<string> { "Charlie" },
    new List<string> { "Dana", "Eve", "Frank" }
};

int groupNumber = 1;
foreach (List<string> group in groups)
{
    Console.WriteLine("Group " + groupNumber + ":");

    foreach (string member in group)
    {
        Console.WriteLine(" - " + member);
    }

    groupNumber++;
}
  • The outer loop visits each group; the inner loop visits members inside that group.
  • Keeping variable names descriptive (group, member) prevents confusion.

Safe removal during iteration

Modifying a collection while a foreach is actively enumerating it will throw an exception. Use one of these patterns instead:

  • Collect items to remove in a separate list, then remove them after the loop.
  • For indexable collections like List<T>, iterate backwards with for and remove by index.

Remove by iterating backwards

var values = new List<int> { 1, 2, 3, 4, 5, 6 };

// Remove odd numbers safely
for (int i = values.Count - 1; i >= 0; i--)
{
    if (values[i] % 2 != 0)
    {
        values.RemoveAt(i);
    }
}
  • Iterating from the end ensures that removing an element doesn’t shift the indexes you haven’t visited yet.
  • This avoids the pitfalls of removing during foreach.

Two-pass removal (mark then remove)

var words = new List<string> { "alpha", "beta", "gamma", "delta" };
var toRemove = new List<string>();

foreach (string w in words)
{
    if (w.StartsWith("g"))
    {
        toRemove.Add(w); // mark for removal
    }
}

foreach (string w in toRemove)
{
    words.Remove(w); // perform removals after enumeration
}
  • The first pass identifies what to remove.
  • The second pass performs the removals safely because no active enumeration is in progress on words.

What foreach does behind the scenes (enumerators)

It can be helpful to see the mechanism once, even if you keep using foreach in practice. Most collections expose an enumerator object that knows how to step through elements.

var items = new List<int> { 10, 20, 30 };

var enumerator = items.GetEnumerator();
while (enumerator.MoveNext())
{
    int current = enumerator.Current;
    Console.WriteLine(current);
}
  • GetEnumerator() returns an object with MoveNext() and Current.
  • foreach compiles into similar code for you automatically, which is why it’s usually preferred.

Simple searching while iterating

You can search for an element by scanning until you find a match and then breaking early.

var numbers = new List<int> { 5, 12, 18, 21, 29 };

int target = 21;
bool found = false;

foreach (int n in numbers)
{
    if (n == target)
    {
        found = true;
        break; // stop once we find it
    }
}

Console.WriteLine(found ? "Found!" : "Not found.");
  • This pattern is straightforward for small collections.
  • Later, we’ll explore more specialized collection methods and algorithms, but the basic scan is a good starting point.

Small performance notes

  • Favor clarity first. foreach is typically the clearest option for read-only passes.
  • Use for for index access. When you must modify elements in arrays or lists, an index-based for loop communicates intent and avoids pitfalls.
  • Avoid repeated work. If you compute something expensive per element, store it in a temporary variable and reuse it inside the loop.

Checklist: choose the right iteration construct

  • Read-only over any collection: foreach.
  • Modify array/list elements: for with index.
  • Process and remove from queue/stack: while (Count > 0) with Dequeue/Pop.
  • Remove from a list safely: iterate backwards or collect then remove.
  • Nested data: nested loops with clear variable names.

Summary

Iteration is how you turn collections into results. Use foreach for clear, read-only passes; switch to for when you need an index or plan to modify elements. For dictionaries, iterate entries or just keys/values depending on what you need. Sets are naturally iterated with foreach, while queues and stacks are often processed with “until empty” loops that remove items. Avoid changing collections during foreach; instead, iterate backwards for lists or mark items to remove later. Keep loops small, name variables clearly, and extract helpers if the body grows. With these patterns, your code will be safe, readable, and maintainable as your programs and datasets grow.