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
foreachloop variablesreceives 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;
}
forgives you the indexiso you can write back todata[i].foreachdoes not let you assign to the iteration variable; to modify elements, preferforwith an index.
Iterating a List<T>
Lists preserve insertion order and support indexing. Choose based on your needs:
- Use
foreachwhen reading each element. - Use
forwhen 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
ito 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
entryhas a.Keyand.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.Keysiterates just the keys;http.Valuesiterates 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
forloop 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
whileloop that checks.Count. - Avoid mixing
foreachwithDequeue/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
}
continuejumps to the next iteration immediately.breakexits 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 withforand 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.foreachcompiles 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.
foreachis typically the clearest option for read-only passes. - Use
forfor index access. When you must modify elements in arrays or lists, an index-basedforloop 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:
forwith 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.