Last Updated: May 22, 2026
Declaring arrays, filling them, and reading a single element by index is enough to store data, but most real work involves doing something with every element: adding prices, finding the cheapest item, checking if a product is in stock, marking each item on sale. This lesson walks through those traversal patterns by hand, using only a for or foreach loop and index arithmetic. Before using built-in helpers like Array.Sort and Array.IndexOf, it pays to know how they work.
A for loop is the standard way to walk an array; the counter doubles as the array index. The shape is always the same: start at 0, run while the counter is strictly less than array.Length, step by one.
The counter i plays two roles: it's the position into the array (cart[i]) and a human-friendly position when printed as i + 1. The condition uses <, not <=, because cart.Length is one past the last valid index. The off-by-one details come up shortly.
A second example, walking a price list and printing each price with currency formatting:
Same shape, different element type. The pattern is identical whether the array holds strings, decimals, ints, or anything else.
Array.Length is O(1). It's a field on the array object, not a count of elements, so reading it inside the loop's condition is cheap. You don't need to cache it in a separate variable like you would for IEnumerable<T>.Count().
When you only need each item in order and don't care about the position, foreach is shorter. It hides the counter and the bounds check, leaving the body focused on the element itself.
The variable item is fresh on each iteration and holds a copy of the element (for value types) or the reference (for reference types). The loop walks the array from index 0 to Length - 1 in order, exactly like the for loop above. The index is hidden, and you don't need to manage it.
var is a common shorthand here, since the type is obvious from the array:
Both forms compile to the same thing for a plain T[] array. Use whichever reads better in your code.
Both loops walk every element. The choice comes down to what the body needs.
| You want to... | Pick |
|---|---|
| Just read each item once, in order | foreach |
| Read the index along with the item | for |
| Modify the array element in place | for |
| Walk in reverse | for |
| Step by something other than one | for |
| Iterate two arrays in parallel by index | for |
Stop or skip cleanly using i | for |
The default for "give me each item" is foreach. It's shorter, harder to get wrong, and signals intent: "I'm reading, not mutating." Use for when the index actually does something useful in the body.
There is also a behavior difference: foreach gives you a copy of the loop variable on each iteration, and assigning to that variable doesn't change the array.
Updating an element in the array means assigning to array[i]. Since foreach gives you a copy of the loop variable, not a reference into the array, you can't use foreach to overwrite elements. A for loop fits.
Applying a 10% discount to every price:
The first loop writes back into the array by index. The second loop only reads, so foreach fits. Mixing the two is common: for when mutating, foreach when displaying.
What doesn't work, and why:
What's wrong with this code?
The compiler rejects this with CS1656: Cannot assign to 'price' because it is a 'foreach iteration variable'. The foreach variable is read-only by design. Even if it weren't, assigning to it wouldn't update the array because the variable holds a copy, not a slot into the array.
Fix:
The for version reaches into the array through prices[i], so the assignment actually changes the stored value.
The diagram captures the asymmetry. A for loop accesses the array slot directly through indexing, so assignments persist. A foreach loop's iteration variable is a separate local, populated from the array each pass, and assigning to it just rewrites the local.
Sometimes the order matters. Newest order first, last-added cart item first, a wishlist printed top-to-bottom from the end of the array. A descending for loop flips the three header slots.
The counter starts at recentOrders.Length - 1 (the last valid index), the condition is i >= 0 (the first valid index), and the iterator steps down with i--. Read aloud: "start at the end; keep going while the index is still in range; step back one each time."
The condition is a common slip. It has to be >= 0, not > 0. Using > 0 would stop after printing the second item (recentOrders[1]), missing index 0 entirely.
There's no built-in foreach form for reverse traversal of a plain array. You can call .Reverse() (LINQ) or Array.Reverse(...), both covered later in this section, but for a quick reverse walk a descending for is the most direct option.
Most array work breaks down into a handful of repeating patterns. Each one combines a for loop with a state variable declared before the loop. These cover most of the array code in real projects.
Adding up every value, then dividing by the count.
total lives in the outer scope so it survives the loop. Each iteration adds one element. After the loop, dividing by the length gives the average. Watch out for an empty array: cart.Length would be 0, and total / 0 on a decimal throws DivideByZeroException. A real program would check if (cart.Length == 0) ... first.
A foreach version reads slightly cleaner because no index is needed:
Same result. Both are O(n) in the array length, because every element is touched exactly once.
Finding the largest or smallest value means tracking the current best as you walk. Start the tracker at the first element, then compare every other element against it.
Two things matter. First, max and min start at prices[0], not at 0 or decimal.MinValue. Starting at 0 would give a wrong min if every price were positive (which they are). Starting at decimal.MinValue would work for max but reads less clearly. Seeding from the first element keeps the logic honest. Second, the loop starts at i = 1, not 0, because prices[0] is already accounted for in the seed.
If the array is empty, prices[0] throws IndexOutOfRangeException. A real program would guard against that.
Sum, average, max, and min are all single-pass O(n) operations. Computing all four in one loop is also O(n). Splitting them across four loops is technically still O(n) (constant factors don't change the order), but each pass adds memory-traffic overhead. For a small array it's irrelevant; for a million elements, prefer one combined loop.
Counting how many elements pass some test. The state variable is an int counter.
The body runs every iteration; the counter only advances when the predicate is true. 29.00 and 49.00 both pass, so the count ends at 2.
You can count more than one category in a single pass by declaring multiple counters before the loop:
Three counters, one pass through the array. This is the kind of work a single for loop is good at: walk once, update many aggregates.
Finding the index of the first element that matches a condition. The pattern is "scan until you find one, then stop." Stopping early matters because the array might be large and the match might be near the front.
Two conventions show up here. The tracker foundAt starts at -1 because that's not a valid array index, so any value >= 0 after the loop means "found." The break jumps out of the loop the moment a match is found, so the loop doesn't waste time scanning the rest. If the target isn't in the array, the loop runs to completion and foundAt stays at -1.
Array.IndexOf(cart, "Mouse") does this in one call. Writing it by hand shows what IndexOf is doing internally.
A simpler variant of find-first: you only care whether the value exists, not where. The state is a single bool.
Same shape as find-first, with a bool instead of an int. The break is still important: once you've found one, there's no reason to keep looking.
Linear search (find-first and contains) is O(n) in the worst case. If the value is near the front, it stops early. If the array is sorted and you'll search it many times, binary search via Array.BinarySearch is O(log n), which is much faster for large arrays.
Two arrays that hold related data, indexed together. The classic example is one array of names and one of stock counts, where names[i] and stocks[i] belong to the same product.
The counter i indexes both arrays in parallel. This is one of the situations where for clearly beats foreach: a foreach loop over names doesn't give you i, so you can't index into stocks. Two parallel foreach loops wouldn't work either, since each has its own variable but no shared index.
There's a risk with parallel arrays: if the two arrays have different lengths, indexing one of them by the other's length throws IndexOutOfRangeException. Bundling related fields into a Product class so you can hold a single Product[] instead avoids the mismatch entirely. Until then, parallel arrays are a workable shortcut as long as the lengths match.
The diagram shows the shared-index idea. One counter steps through both arrays at the same rate, so names[i] and stocks[i] always point at matching positions.
So far the array values have been hard-coded. Real programs pull values from somewhere: user input, another array, a config file. The pattern is the same as before, just with the source of the values changing.
Filling an array from Console.ReadLine:
Sample run:
The for loop fills the array slot by slot. Console.ReadLine() returns string? (it can be null if the input stream ends), so the ?? "" substitutes an empty string when that happens. The display loop is a plain foreach, since the index isn't needed at that point.
Building a new array from an existing one is the same idea, with the source array taking the place of the input:
The new array discounted is sized to match prices. The first loop fills it; the second loop walks both arrays in parallel to print before/after. This pattern (read array A, build array B from it) is so common that LINQ provides a one-liner for it (prices.Select(p => p * 0.85m).ToArray()). For now, an explicit loop and an explicit second array are the approach.
Building a second array allocates a new heap object of size n. For small arrays this is negligible. For very large arrays in tight loops, the allocation and the second pass over memory both add up.
C# arrays do bounds checking at runtime. Every array access is paired with an implicit check that the index is in range, and if it isn't, the runtime throws IndexOutOfRangeException. This is a safety feature; in some other languages, reading past the end of an array silently returns garbage memory and your program keeps running with wrong data. C# stops the program instead.
Three flavors of bounds error show up in beginner code.
<=A common bug: using <= when you meant <.
What's wrong with this code?
orders.Length is 3. Valid indices are 0, 1, 2. The condition i <= orders.Length lets i reach 3, and orders[3] throws System.IndexOutOfRangeException: Index was outside the bounds of the array. at runtime.
Fix:
The rule is simple: forward-walking loops use <, never <=, because Length is one past the last valid index.
The mirror of the previous bug, but in the descending direction.
What's wrong with this code?
The condition is > 0, so the loop stops once i becomes 0 without entering the body for that value. Index 0 (which holds "ORD-001") never prints. No exception, just silently wrong output.
Fix:
Reverse loops use >= 0, never > 0. The pattern: forward uses <, reverse uses >= 0.
Even a perfectly written loop can hit the bounds if some other code changes assumptions. A common case: copying from one array into another with a typo'd length.
What's wrong with this code?
source.Length is 3, but dest only has room for 2 elements. The third iteration writes dest[2], which is out of range, and the runtime throws IndexOutOfRangeException.
Fix: Make the loop bound the smaller of the two lengths:
Or, more often, size the destination to match the source:
Length - 1 is the rightmost valid index for a single array; with two arrays involved, the rightmost shared valid index is Math.Min(a.Length, b.Length) - 1.
The diagram shows the boundaries. Anywhere from 0 up to Length - 1 is safe. Anything below 0 or at Length (or above) throws. The boundary is exact, and the runtime checks it on every access.
Pulling several patterns together: a program reads a price array, finds the three most expensive items, and prints them along with the total cost of those three. No Array.Sort, no LINQ, just for loops and trackers.
Three loops, each doing one job. The outer round loop picks one winner per round. The middle loop scans every price to find the current best, skipping anything already picked. The inner k loop is the "already picked" check. After three rounds, topIndices holds the indices of the three highest prices.
This uses three nested loops, so the complexity is roughly O(n 3 3) = O(n) for picking the top three from an array of size n. For top-k with large k, the scheme becomes O(n * k), which is fine for small k. Sorting the whole array first is O(n log n), which is faster for top-k when k is close to n. Array.Sort and LINQ's OrderByDescending both cover this ground.
The example is a bit longer than the others on purpose. Real array work often layers patterns: a find-max nested inside an outer loop, a count nested inside a filter, a sum nested inside a sort. Reading and writing nested loops over arrays is half the work in any DSA interview, and most production code dealing with collections.
Built-in Array.Sort, Array.IndexOf, Array.Reverse, and friends do many of these patterns for you in one method call. Understanding how they work internally is the reason the manual versions came first.