I still remember the first time a production bug came down to a tiny indexing mistake. I had a list of invoices, I walked it with a classic for loop, and a single off-by-one slipped into the logic. The system ran, but a single customer’s invoice went missing. That experience made me appreciate loops that remove the temptation to micromanage indexes. In C#, the foreach loop does exactly that: it asks the runtime for “the next element,” and your code focuses on the work you actually want to do. If you’re writing modern C# in 2026, you’ll see foreach everywhere—from processing domain entities to streaming data from APIs, to stepping through a span or an async sequence.
In the next sections, I’ll show you how foreach works under the hood, where it shines, where it can bite, and how to pair it with modern patterns like IAsyncEnumerable, records, pattern matching, and LINQ (without overusing it). I’ll also call out the mistakes I still see in code reviews, plus a few performance notes that matter once your collections get large or hot paths show up in profiling.
What foreach Really Does Under the Hood
At the surface, foreach looks like magic. You write:
var orders = GetPendingOrders();
foreach (var order in orders)
{
Process(order);
}
But the compiler translates that into a more explicit pattern. When you iterate a type that implements IEnumerable or IEnumerable, the compiler emits code that:
1) calls GetEnumerator()
2) loops while MoveNext() returns true
3) reads the Current element each iteration
4) disposes the enumerator (if needed)
In practice, the generated pattern is similar to this simplified version:
using var enumerator = orders.GetEnumerator();
while (enumerator.MoveNext())
{
var order = enumerator.Current;
Process(order);
}
This matters because it explains behavior like:
- why
foreachworks on arrays (arrays provide a specialized enumerator) - why
foreachworks on custom types even if you don’t implementIEnumerable, as long as you provide theGetEnumeratorpattern - why modifying a collection during iteration can throw an exception (the enumerator detects changes)
In my experience, knowing this translation saves time when debugging or optimizing. You can reason about allocations, dispose patterns, and whether you can safely mutate the underlying collection.
foreach with Arrays, Lists, and Spans
You’ll commonly use foreach with arrays and lists. The syntax is the same, but the performance characteristics differ slightly because arrays have a highly optimized enumerator. Lists have their own enumerator struct that is fast and allocation-free in most cases.
Here’s a practical example that cleans up phone numbers in a list, using a simple helper method:
using System;
using System.Collections.Generic;
public static class PhoneCleaner
{
public static List NormalizePhones(List phones)
{
var result = new List(phones.Count);
foreach (var phone in phones)
{
if (string.IsNullOrWhiteSpace(phone))
continue;
var normalized = phone
.Replace("(", "")
.Replace(")", "")
.Replace("-", "")
.Replace(" ", "");
result.Add(normalized);
}
return result;
}
}
When you need to iterate over Span or ReadOnlySpan, foreach also works, and it’s often the cleanest syntax for safe, slice-based processing. This is common in high-performance code, parsing text, or processing buffers.
using System;
public static class CsvParser
{
public static int CountCommas(ReadOnlySpan line)
{
var count = 0;
foreach (var ch in line)
{
if (ch == ‘,‘) count++;
}
return count;
}
}
I prefer foreach with spans because it’s expressive and the compiler emits efficient code. You avoid index arithmetic and reduce the risk of out-of-bounds access.
Custom Collections and the Enumerator Pattern
You can make foreach work with your own types without implementing IEnumerable, as long as you follow the enumerator pattern. This can be useful when you want a light-weight API or you’re working in performance-critical code where you want to avoid interface dispatch.
Here’s a small example: a custom collection of time slots that returns its own enumerator type.
using System;
public readonly struct TimeSlots
{
private readonly TimeSpan _start;
private readonly int _count;
private readonly TimeSpan _interval;
public TimeSlots(TimeSpan start, int count, TimeSpan interval)
{
_start = start;
_count = count;
_interval = interval;
}
public Enumerator GetEnumerator() => new Enumerator(start, count, _interval);
public struct Enumerator
{
private readonly TimeSpan _start;
private readonly TimeSpan _interval;
private readonly int _count;
private int _index;
public Enumerator(TimeSpan start, int count, TimeSpan interval)
{
_start = start;
_count = count;
_interval = interval;
_index = -1;
}
public TimeSpan Current => start + (interval * _index);
public bool MoveNext()
{
_index++;
return index < count;
}
}
}
public static class Demo
{
public static void PrintSlots()
{
var slots = new TimeSlots(TimeSpan.FromHours(9), 4, TimeSpan.FromMinutes(30));
foreach (var slot in slots)
{
Console.WriteLine(slot);
}
}
}
You can see the pattern: GetEnumerator(), MoveNext(), and Current. This is enough for foreach to work. I use this pattern occasionally when building a low-level library that needs to stay allocation-free and avoid interface overhead.
Mutability, Safety, and the “Modify While Iterating” Rule
One of the most common runtime errors I still see is “Collection was modified; enumeration operation may not execute.” That’s thrown when you change a collection (like List) while iterating it with foreach.
Here’s a failure example:
var tasks = new List { "email", "invoice", "archive" };
foreach (var task in tasks)
{
if (task == "invoice")
tasks.Remove(task); // Runtime exception
}
If you need to remove items, you have two reliable choices:
1) Iterate backward with a for loop and remove by index
2) Build a new list with the items you want
I generally recommend building a new list for clarity unless performance forces the issue.
var tasks = new List { "email", "invoice", "archive" };
var filtered = new List();
foreach (var task in tasks)
{
if (task != "invoice")
filtered.Add(task);
}
// Use filtered list
I also want to highlight a subtlety: you can mutate the objects inside the collection as long as you don’t change the collection itself. For instance, you can update properties of a Customer during iteration without issue, because the collection structure isn’t changing.
foreach with Dictionaries and Tuples
Dictionaries are another popular use case. foreach iterates key-value pairs (as KeyValuePair). For readability, I like deconstruction syntax:
using System.Collections.Generic;
public static class TaxCalculator
{
public static decimal SumTax(Dictionary stateTaxes)
{
decimal total = 0;
foreach (var (state, tax) in stateTaxes)
{
if (tax <= 0) continue;
total += tax;
}
return total;
}
}
Deconstruction makes your loop easier to scan. It’s also safer than constantly typing .Key and .Value—I’ve seen bugs caused by mixing those up when under time pressure.
foreach with IAsyncEnumerable and await foreach
Modern C# encourages asynchronous streams. If you’re calling APIs, streaming file lines, or reading events from a queue, you’ll likely use IAsyncEnumerable and await foreach.
Here’s a complete example that reads lines from a log stream asynchronously and filters them for errors:
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
public static class LogStream
{
public static async IAsyncEnumerable GetLinesAsync()
{
// Simulate async source
await Task.Delay(10);
yield return "INFO: Startup";
await Task.Delay(10);
yield return "ERROR: Disk full";
await Task.Delay(10);
yield return "INFO: Recovery";
}
}
public static class LogProcessor
{
public static async Task CountErrorsAsync()
{
int count = 0;
await foreach (var line in LogStream.GetLinesAsync())
{
if (line.StartsWith("ERROR"))
count++;
}
return count;
}
}
I use await foreach for streaming data pipelines, especially when the source is slow or infinite. It makes your code feel synchronous while still being non-blocking.
If you need cancellation, you can combine await foreach with WithCancellation and a CancellationToken. That’s a pattern I always include in production workloads where an operation might need to stop early.
Pattern Matching and foreach: Clearer Branching
Pattern matching improves readability inside loops. For instance, when processing a mixed list of domain messages, I often use switch with type patterns.
using System;
using System.Collections.Generic;
public interface IMessage { }
public record EmailMessage(string Address, string Subject) : IMessage;
public record SmsMessage(string Number, string Text) : IMessage;
public static class MessageSender
{
public static void Dispatch(IEnumerable messages)
{
foreach (var message in messages)
{
switch (message)
{
case EmailMessage email when email.Address.Contains("@"):
Console.WriteLine($"Email to {email.Address}: {email.Subject}");
break;
case SmsMessage sms when sms.Number.Length >= 10:
Console.WriteLine($"SMS to {sms.Number}: {sms.Text}");
break;
default:
Console.WriteLine("Unknown or invalid message");
break;
}
}
}
}
This style reads like a rules engine and keeps branching localized. I recommend it whenever you have multiple cases and you want to keep the logic close to the loop.
foreach vs for: A Practical Comparison
Here’s a quick side-by-side view I use when mentoring teams:
Traditional for
foreach —
Natural fit
Possible but risky
Depends on indexing
Fast
Not supported
await foreach I choose foreach by default for read-only traversal. I use for only when I need the index, or when I need to update the collection in place. Even then, I often ask myself if a new list might be cleaner and safer.
Common Mistakes I See in Code Reviews
1) Modifying the collection inside the loop
– Fix: build a new list or iterate backward with for.
2) Assuming foreach gives you the index
– If you need the index, track it explicitly:
int index = 0;
foreach (var order in orders)
{
Console.WriteLine($"{index}: {order.Id}");
index++;
}
3) Assuming foreach copies value types by reference
– When iterating a collection of structs, each item is a copy. Mutating it doesn’t change the original. Use ref foreach for certain scenarios, or update by index. For example:
using System;
public struct MeterReading
{
public int Value;
}
public static class Readings
{
public static void IncreaseAll(MeterReading[] readings)
{
for (int i = 0; i < readings.Length; i++)
{
readings[i].Value += 10; // Works because we update by index
}
}
}
4) Overusing LINQ when foreach is clearer
– LINQ is great, but a simple loop is often easier to debug and step through. If your logic has branching, side effects, or early exits, stick with a loop.
Performance Considerations That Matter in 2026
Most of the time, foreach is fast enough. But there are a few things I keep in mind for hot paths:
- Enumerator allocations: With modern C#, iterating
Listor arrays does not allocate. But iterating viaIEnumerablecan allocate if the enumerator is a reference type. If you’re performance-sensitive, use the concrete type.
- Interface dispatch: Iterating an
IEnumerablemay incur interface dispatch perMoveNext. If you care, useListorT[]in the loop signature.
- Bounds checks:
foreachover arrays and spans is often as fast asfor, and sometimes faster, because the compiler can remove bounds checks. I still benchmark if this is a hot path.
- Early exit:
breakandreturnwork as expected. If you want to bail out early,foreachhandles it cleanly and still disposes the enumerator if needed.
In real systems, performance often comes down to data structure choices and memory pressure, not whether you wrote for or foreach. But if profiling shows a loop is hot, then measure, don’t guess.
When I Use foreach vs When I Avoid It
I reach for foreach by default when:
- I’m iterating a collection to read or transform data
- I want code that is easy to scan and maintain
- I’m working with
IAsyncEnumerable
I avoid foreach when:
- I must update the collection in place
- I need to access neighboring elements by index
- I’m walking a list and removing items based on a condition
Here’s a practical example showing both patterns in one workflow. First, I read incoming data with foreach, then I trim a list using a for loop:
using System;
using System.Collections.Generic;
public static class InvoiceFlow
{
public static List FilterLargeInvoices(IEnumerable incoming)
{
var all = new List();
foreach (var amount in incoming)
{
if (amount > 0)
all.Add(amount);
}
// Remove small invoices in place
for (int i = all.Count - 1; i >= 0; i--)
{
if (all[i] < 1000m)
all.RemoveAt(i);
}
return all;
}
}
This combination keeps each loop focused on what it does best.
Real-World Scenarios Where foreach Excels
1) Processing API Results
When you hit an API and get a list of objects, you often need to validate, transform, and collect them. foreach is the clearest way to apply business rules without drowning in LINQ queries.
using System;
using System.Collections.Generic;
public record ApiOrder(string Id, decimal Total, bool IsPaid);
public static class ApiOrderHandler
{
public static List GetReadyToShipIds(IEnumerable apiOrders)
{
var ready = new List();
foreach (var order in apiOrders)
{
if (order.Total <= 0) continue;
if (!order.IsPaid) continue;
ready.Add(order.Id);
}
return ready;
}
}
This loop reads like a checklist, which is exactly what you want for business validation.
2) Streaming File Processing
When files are too big to load into memory, you typically stream them line by line. foreach integrates well with File.ReadLines and await foreach for async versions.
using System;
using System.Collections.Generic;
using System.IO;
public static class AuditScanner
{
public static int CountWarnings(string path)
{
int warnings = 0;
foreach (var line in File.ReadLines(path))
{
if (line.Contains("WARN", StringComparison.OrdinalIgnoreCase))
warnings++;
}
return warnings;
}
}
3) ETL Pipelines and Transform Steps
In data processing pipelines, each step often has validation logic, branching, or side effects like logging. That’s a good fit for explicit loops.
using System;
using System.Collections.Generic;
public record RawRow(string Region, decimal Revenue);
public record CleanRow(string Region, decimal Revenue, bool IsHighValue);
public static class Pipeline
{
public static List Clean(IEnumerable rows)
{
var result = new List();
foreach (var row in rows)
{
if (string.IsNullOrWhiteSpace(row.Region))
continue;
var normalizedRegion = row.Region.Trim().ToUpperInvariant();
var isHighValue = row.Revenue >= 10000m;
result.Add(new CleanRow(normalizedRegion, row.Revenue, isHighValue));
}
return result;
}
}
4) Domain Event Handling
Event-driven systems often have a batch of events that need different handling. Pattern matching inside foreach keeps it clear and maintainable.
using System;
using System.Collections.Generic;
public interface IDomainEvent { }
public record UserCreated(string UserId) : IDomainEvent;
public record UserLocked(string UserId) : IDomainEvent;
public static class EventRouter
{
public static void Handle(IEnumerable events)
{
foreach (var evt in events)
{
switch (evt)
{
case UserCreated created:
Console.WriteLine($"Welcome {created.UserId}");
break;
case UserLocked locked:
Console.WriteLine($"Locking user {locked.UserId}");
break;
default:
Console.WriteLine("Unknown event");
break;
}
}
}
}
5) UI Binding and View Models
When mapping a list of DTOs to view models, a simple loop avoids surprise allocations and keeps the mapping logic readable.
using System.Collections.Generic;
public record ProductDto(string Name, decimal Price);
public record ProductVm(string Name, string PriceLabel);
public static class VmMapper
{
public static List Map(IEnumerable products)
{
var result = new List();
foreach (var p in products)
{
var label = p.Price.ToString("C");
result.Add(new ProductVm(p.Name, label));
}
return result;
}
}
ref foreach and Avoiding Struct Copies
One subtle performance trap: when you iterate over a collection of structs, each item is a copy. If the struct is large, this can add overhead. Modern C# lets you use ref foreach in certain cases to iterate by reference.
Here’s the idea with a large struct representing a sensor reading:
using System;
public struct SensorReading
{
public long Ticks;
public double Value;
public double Calibration;
}
public static class SensorAnalytics
{
public static double SumValues(SensorReading[] readings)
{
double sum = 0;
// Readonly ref avoids copying each struct
foreach (ref readonly var r in readings)
{
sum += r.Value;
}
return sum;
}
}
Use this sparingly. It’s great in hot paths or when profiling reveals heavy struct copying. But readability matters too, so don’t make every loop a ref foreach without evidence.
foreach and IDisposable Enumerators
When an enumerator implements IDisposable, foreach will automatically call Dispose at the end. That’s a big deal when iterating resources like file system enumerators or database cursors.
This detail explains why foreach is safe with many streaming APIs—cleanup is automatic even if you break early.
A subtle edge case: if you write a custom enumerator and implement IDisposable, make sure it’s safe to call Dispose multiple times. The compiler might generate a try/finally to ensure disposal.
foreach with yield return
If you use yield return to build iterators, your method becomes a state machine that returns an enumerator. The nice part is you can test and stream data with almost no ceremony.
using System;
using System.Collections.Generic;
public static class SequenceBuilder
{
public static IEnumerable RangeBy(int start, int count, int step)
{
int current = start;
for (int i = 0; i < count; i++)
{
yield return current;
current += step;
}
}
}
public static class Demo
{
public static void Print()
{
foreach (var n in SequenceBuilder.RangeBy(10, 5, 3))
{
Console.WriteLine(n);
}
}
}
When you combine yield return with foreach, you get clean, streaming pipelines without allocating whole lists.
foreach with IEnumerable vs Concrete Types
A practical performance rule: if your method accepts IEnumerable, your foreach may trigger interface dispatch or reference-type enumerators. If you know you’ll always have a List or array, consider overloading or using generics constrained to struct enumerators.
Example: two methods doing the same work, one using IEnumerable and one using List. The behavior is identical, but the second can be faster on hot paths.
using System.Collections.Generic;
public static class ScoreStats
{
public static int CountHighScores(IEnumerable scores)
{
int count = 0;
foreach (var s in scores)
if (s >= 900) count++;
return count;
}
public static int CountHighScores(List scores)
{
int count = 0;
foreach (var s in scores)
if (s >= 900) count++;
return count;
}
}
I usually choose the IEnumerable signature for flexibility unless profiling tells me a concrete overload would help.
foreach and Early Exit Patterns
A common misconception is that foreach is “all or nothing.” In reality, it supports break, continue, and return just like for.
public static bool HasNegative(IEnumerable values)
{
foreach (var v in values)
{
if (v < 0) return true;
}
return false;
}
This is perfect for searches and validations. You keep the loop readable and still get early exit when you find what you need.
foreach and Null Safety
Two common null pitfalls:
1) foreach on a null collection throws NullReferenceException.
2) An iterator method can yield null values for reference types, which can be surprising.
I often guard a loop with a null-coalescing fallback or a simple check.
public static int CountNonEmpty(IEnumerable? items)
{
if (items == null) return 0;
int count = 0;
foreach (var item in items)
{
if (!string.IsNullOrWhiteSpace(item))
count++;
}
return count;
}
If null values inside the sequence are possible, handle them explicitly rather than assuming everything is valid.
Nested foreach and Complexity Awareness
Nested loops are where performance risks show up fast. The readability of foreach can disguise the complexity. I keep a mental note: foreach inside foreach is usually O(n*m). That’s not automatically bad, but it should be intentional.
When I suspect a hot path, I’ll consider using a dictionary for lookups or precomputing sets to reduce nested scanning.
using System.Collections.Generic;
public static class Permissions
{
public static bool HasAccess(IEnumerable userRoles, HashSet allowed)
{
foreach (var role in userRoles)
{
if (allowed.Contains(role))
return true;
}
return false;
}
}
This replaces a nested loop with O(n) lookups and keeps the code clear.
foreach and LINQ: Healthy Boundaries
LINQ is excellent for composability, but there are times where foreach is the better tool. I use a rule of thumb:
- Use LINQ for simple projections, filters, or aggregations with no side effects.
- Use
foreachfor complex branching, error handling, logging, or early exits.
Here’s a case where I intentionally choose foreach over LINQ because of side effects and early returns:
using System;
using System.Collections.Generic;
public static class FraudCheck
{
public static bool IsSuspicious(IEnumerable amounts)
{
decimal running = 0;
foreach (var amount in amounts)
{
if (amount <= 0) continue;
running += amount;
if (running > 10000m)
{
Console.WriteLine("High risk detected");
return true;
}
}
return false;
}
}
LINQ can express this, but it becomes harder to read and debug. The loop is honest about what’s happening.
Edge Cases and Subtle Behaviors
1) Multiple Enumeration
Some IEnumerable sequences are “one-shot” (like a stream). Iterating twice can re-run the operation or even fail. If you need to enumerate multiple times, materialize once.
var lines = File.ReadLines(path);
// Safe materialization if you need multiple passes
var cached = new List(lines);
foreach (var line in cached) { / pass 1 / }
foreach (var line in cached) { / pass 2 / }
2) Deferred Execution
LINQ queries are often deferred. When you use foreach on a query, the execution happens at that moment. That’s fine, but it can surprise you if the underlying data changes between query creation and enumeration.
3) Exception Handling
If an exception occurs inside the loop, the enumerator still gets disposed. This matters for resource cleanup and is one of the strengths of foreach.
4) Thread Safety
Enumerating most collections is not thread-safe. If another thread modifies the collection, you’ll usually get an exception. If you need concurrent access, consider immutable collections or snapshot copies.
Practical Patterns I Use Often
Pattern: Collect and Validate
Read, validate, and collect results with explicit intent.
public static List ParseIds(IEnumerable inputs)
{
var ids = new List();
foreach (var input in inputs)
{
if (int.TryParse(input, out var id) && id > 0)
ids.Add(id);
}
return ids;
}
Pattern: First Match with Early Exit
Fast path for a search across a list.
public static string? FindFirstError(IEnumerable lines)
{
foreach (var line in lines)
{
if (line.StartsWith("ERROR"))
return line;
}
return null;
}
Pattern: Multi-Stage Processing
Multiple loops with clear purpose instead of a single complex loop.
public static List NormalizeAndSort(IEnumerable names)
{
var cleaned = new List();
foreach (var name in names)
{
if (string.IsNullOrWhiteSpace(name)) continue;
cleaned.Add(name.Trim());
}
cleaned.Sort(StringComparer.OrdinalIgnoreCase);
return cleaned;
}
I prefer multiple simple loops over one mega-loop. It’s easier to test and easier to read.
foreach and Modern Records
Records make it easy to project and transform immutable data. Combined with foreach, you can apply transformations without mutating the original object.
public record Order(string Id, decimal Total, bool IsPriority);
public static List PromoteHighValue(IEnumerable orders)
{
var result = new List();
foreach (var o in orders)
{
if (o.Total >= 5000m)
result.Add(o with { IsPriority = true });
else
result.Add(o);
}
return result;
}
This is an expressive pattern: it reads cleanly and keeps immutability intact.
When to Prefer for or while Instead
It’s worth being explicit about cases where foreach isn’t the best tool.
1) You need the index for neighboring items
If you’re comparing items[i] and items[i+1], for is more natural and less error-prone than manual indexing in foreach.
2) You are removing items in place
foreach isn’t safe for structural modifications. A reverse for loop is the simplest safe approach.
3) You are implementing a custom iterator
Sometimes you need a while loop to pull from a queue, database cursor, or channel until it’s closed. foreach is great for enumerables but isn’t always the right abstraction.
Small Comparison Table: Loop Choices in Practice
Best Choice
—
foreach
for
for (reverse)
await foreach
ref foreach
foreach and Testing Strategy
One underappreciated benefit: loops make testing easier. When logic is in a foreach, you can test with a small list and read the logic top to bottom. I often write tests that specifically target loop edge cases:
- Empty input
- Input with invalid items
- Input with only one item
- Input where the early-exit branch is hit
If your foreach relies on external state (like a logger or service), consider injecting that dependency so the loop stays testable.
Debugging Tips for foreach
A few practical tricks I use:
- Watch the enumerator: Set a breakpoint on the loop body and inspect the
Currentitem. - Check the concrete type: If performance is off, inspect the runtime type of the collection to see if it’s a generator or a list.
- Move suspicious logic out: If a loop feels complicated, extract the body into a helper method. This makes it easier to step through.
Practical Pitfalls and How I Avoid Them
Pitfall: Mutating a captured loop variable in async code
If you start tasks inside a foreach, be careful about variable capture (especially with older language versions). In modern C#, each iteration variable is scoped, but if you’re supporting older versions or codebases, it’s worth being explicit.
foreach (var id in ids)
{
var captured = id; // Safe explicit capture
tasks.Add(Task.Run(() => Process(captured)));
}
Pitfall: Assuming enumeration order
Most collections preserve order, but not all. A Dictionary does preserve insertion order in modern .NET, but relying on that in cross-runtime code can be risky. If order matters, use a list or explicitly sort.
Pitfall: Enumerating a lazy sequence multiple times
If your sequence is built with yield return, enumerating it twice will recompute it. That may be fine, but if it’s expensive, materialize it once.
Putting It All Together: A Complete Example
Here’s a more realistic example that combines several patterns: async streaming, filtering, mapping, and a final aggregation.
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
public record Purchase(string UserId, decimal Amount, string Region);
public static class PurchaseStream
{
public static async IAsyncEnumerable StreamAsync()
{
await Task.Delay(10);
yield return new Purchase("u1", 1200m, "US");
await Task.Delay(10);
yield return new Purchase("u2", 50m, "US");
await Task.Delay(10);
yield return new Purchase("u3", 800m, "CA");
}
}
public static class PurchaseAnalytics
{
public static async Task<Dictionary> SumByRegionAsync(CancellationToken ct = default)
{
var totals = new Dictionary(StringComparer.OrdinalIgnoreCase);
await foreach (var purchase in PurchaseStream.StreamAsync().WithCancellation(ct))
{
if (purchase.Amount <= 0) continue;
if (!totals.TryGetValue(purchase.Region, out var current))
totals[purchase.Region] = purchase.Amount;
else
totals[purchase.Region] = current + purchase.Amount;
}
return totals;
}
}
This is the kind of code you’ll see in real services: it’s readable, handles streaming, and does real work without overcomplicating things.
Final Thoughts
I use foreach constantly because it’s readable, safe, and expressive. It doesn’t replace for, but it covers the most common traversal tasks with less risk. Once you understand the enumerator pattern and the few pitfalls around mutability, you can use it confidently in everything from tiny helpers to large streaming pipelines.
If you take one thing from this guide, let it be this: choose the loop that makes the intent obvious. Most of the time, that’s foreach.


