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 variablep
.- 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 variables
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 indexi
so you can write back todata[i]
.foreach
does not let you assign to the iteration variable; to modify elements, preferfor
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
withDequeue
/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 withfor
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 withMoveNext()
andCurrent
.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-basedfor
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)
withDequeue
/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.