AlgoMaster Logo

Multidimensional Arrays

Last Updated: May 17, 2026

11 min read

A regular array gives you one row of values. Plenty of real data isn't shaped like that. A monthly sales report for ten products has rows for products and columns for months. A warehouse layout has rows for shelves and columns for bins. A rating breakdown has rows for categories and columns for star counts. C# has a dedicated array shape for exactly this: the multidimensional array, where you ask for an element with two (or more) coordinates at once. This chapter covers what those arrays look like, how to declare and fill them, how to iterate over them, and when to pick one over a jagged array.

What a Multidimensional Array Actually Is

A C# multidimensional array is a single rectangular block of memory addressed by more than one index. The shape is fixed: every row has the same number of columns, every column has the same number of rows. The compiler writes the type as T[,] for two dimensions, T[,,] for three, and so on. The commas inside the brackets count the dimensions, so int[,] means "an array of int with two dimensions" and int[,,] means "three dimensions."

People also call these rectangular arrays because every row is the same length, like a rectangle on paper. That word matters because C# also has jagged arrays (T[][]), which look similar but are a different type with different memory layout. Lesson 04 walks through jagged arrays in depth; for this lesson, treat T[,] as the rectangular shape and remember that the comma is the giveaway.

Three concrete cases where this shape fits the problem:

  • A [product][month] sales matrix where every product has a value for every one of the twelve months.
  • A [row][col] warehouse grid where every row of the grid has the same number of bins.
  • A [category][rating] count of how many five-star, four-star, etc. reviews each category received.

The rectangular shape is what lets the compiler lay the whole thing out as one contiguous slab of memory, which has speed implications we'll come back to.

The diagram lines up the three shapes you'll meet. Today's focus is the cyan one on the left and the teal 3D extension on the right. The orange jagged variant is shown here only so you don't confuse the syntaxes.

Declaring a 2D Array

The simplest case is the two-dimensional array. The type is T[,] and the variable declaration looks like this:

Those lines declare the variables without allocating anything. The variables themselves are reference-type slots that currently hold null, the same way a regular array reference would. None of these is usable yet; you need to actually allocate the rectangular block.

You allocate with new, giving both dimensions:

The expression new int[3, 4] creates a 3-by-4 rectangular block of int. Every cell starts at the default value for the element type, which is 0 for int, null for reference types, and false for bool. The total cell count is rows times columns, which is why grid.Length returns 12.

You read the per-dimension size with GetLength(dimensionIndex). The dimensions are numbered starting at zero, so GetLength(0) is the first dimension (rows) and GetLength(1) is the second (columns). Don't confuse GetLength with the regular Length property; on a 2D array, Length is the total number of cells across both dimensions, which is almost never what you want when you're writing loop bounds.

The number of dimensions itself is available as Rank:

Rank is mostly useful for code that has to handle arrays of unknown dimensionality (reflection, serialization), but it's worth knowing the property exists.

Initializing With Values

new int[3, 4] gives you a block full of zeros. When you already know the values, you can supply them in one expression using nested braces. Each inner brace is one row, and the rows have to all be the same length.

This matrix is a [category][rating] count: three categories, five rating buckets (one through five stars). The element at reviewCounts[0, 4] is row 0, column 4, which is the five-star count for the first category. The compiler figures out the dimensions from the layout you wrote, so new int[,] with no size numbers is fine here.

When you declare and initialize on the same line, you can shorten the right side and let the compiler infer the type and dimensions:

This is the cleanest form for hardcoded data. The compiler reads the row count from the outer braces and the column count from the first inner brace, then checks that every other inner brace matches. If you mix sizes:

The error CS0847 makes the rule explicit: a rectangular array initializer needs every row to be the same length.

A string[,] works the same way. Here's a product-by-store price matrix where the values are strings (formatted prices for display):

Same shape, different element type. The brace pattern is the same; only the values changed.

Accessing Elements

You read or write a cell with two indices separated by a comma, both inside one set of square brackets:

This is the place where T[,] and T[][] differ in syntax in a way that matters. The rectangular array uses one set of brackets with a comma. The jagged array (lesson 04) uses two sets of brackets. Mixing them up is a compile error, not a runtime error, so the compiler will catch you the first time.

A small example, filling a 2-by-3 warehouse grid with bin labels:

Reading an out-of-range index throws IndexOutOfRangeException at runtime, the same way a one-dimensional array would:

Valid indices go from 0 to GetLength(dim) - 1 for each dimension, just like a flat array. The runtime checks both indices on every access, so you can't accidentally read into a neighboring row by going past the column count.

Iterating With Nested For Loops

To touch every cell you need two indices, so two nested loops. The outer loop walks the first dimension, the inner loop walks the second:

Two things to call out. First, GetLength(0) and GetLength(1) are the right way to get the per-dimension bounds; don't divide Length by anything. Second, the outer loop fixes a row index r and the inner loop sums all the columns for that row, which gives a per-category total. Flip the loops around and you'd sum each column instead (the total number of reviews at each rating tier across categories).

When you nest the loops, the order you pick has a memory-layout consequence:

The diagram shows the order in which cells sit in memory for a 3-by-3 int[,]. C# stores multidimensional arrays in row-major order, meaning all of row 0 comes first, then all of row 1, then all of row 2. Iterating row by row (outer loop on the row, inner loop on the column) walks the cells in the order they're laid out. Iterating column by column jumps around in memory, which can be slower for very large arrays.

A second pattern: filling a grid based on its coordinates. Here's a [product][month] sales matrix where each cell starts at a base price and gets a small bump per month:

Two indices, two nested loops, write back to the cell. This is the bread-and-butter shape for any 2D computation. The base prices array is a flat 1D array on purpose: it has one value per product, not one per (product, month) cell, so it fits the 1D shape.

Foreach Flattens the Array

foreach works on a multidimensional array, but it has one important behavior: it gives you the elements in row-major order without telling you the coordinates. There's no (row, col) pair, just one element at a time.

That's the whole grid, flattened into a single sequence. The order is row-major, matching the memory layout. If you only need to operate on each value (sum, count, find max, filter), foreach is the cleanest option:

When you need the coordinates of each cell (printing a labeled table, writing back to the cell by index, doing something different at the edges), you need the nested for loops because foreach doesn't expose the indices. Use for when you need where, use foreach when you just need what.

Three-Dimensional Arrays

The pattern extends to more dimensions. A 3D array adds a third coordinate. The type is T[,,] (two commas), and you allocate with three sizes:

You can also initialize a 3D array inline by triple-nesting braces, with one outer brace per slab, one inner brace per row inside the slab, and the actual values inside that:

Iterating a 3D array needs three nested loops, one per dimension:

C# allows up to 32 dimensions on paper, but in practice anything beyond three becomes hard to reason about. If you find yourself reaching for a 4D array, consider whether a different data structure (a class with named fields, a dictionary keyed by tuple) would be clearer.

Use Cases That Fit the Rectangular Shape

A rectangular array fits when:

  • The shape is genuinely rectangular: every row has the same number of columns, and that number doesn't change.
  • You frequently access cells by both coordinates and the access pattern is roughly uniform across the grid.
  • The total cell count is bounded and known up front.
  • You need the contiguous memory layout for cache performance (the row-major sweep advantage above).

Here's a typical fit: a [product][store] price comparison table. Every product has a price at every store, and you want to compute the average price per product across stores.

Each row is one product; each column is one store. The outer loop fixes the product, the inner loop walks the stores for that product. The total cell count (3 * 4 = 12) is small and fixed, and every row genuinely has the same width because every product has a listed price at every store.

If your data doesn't have that uniform shape (some products are missing from some stores, or rows have different lengths for a real reason), the rectangular array forces you to invent placeholder values to fill the empty cells. That's where the jagged shape starts to look attractive.

Rectangular vs Jagged: A Quick Preview

C# has a second multidimensional shape, the jagged array, written T[][]. Here's a one-paragraph version so you don't pick the wrong type by accident.

A T[,] is one rectangular block of memory with a fixed width per row. A T[][] is an array of references, where each reference points to a separate one-dimensional array. The rows in a jagged array can have different lengths, and each row is allocated independently on the heap.

PropertyT[,] RectangularT[][] Jagged
Syntaxint[,] g = new int[3, 4]int[][] g = new int[3][]
Indexingg[row, col]g[row][col]
Row widthAll rows same widthRows can vary
MemoryOne contiguous blockArray of references to row arrays
InitializationOne new callOne new per row

The short version: rectangular for uniform grids, jagged for ragged data.

Common Mistakes

Three patterns trip up almost everyone the first time they reach for T[,].

Confusing Length and GetLength

grid.Length is the total cell count, not the row count or the column count. Using Length as a loop bound on a 2D array is a quick way to get an exception or a wrong answer.

What's wrong with this code?

grid.Length is 12, but the row count is 3. When i reaches 3, grid[3, 0] throws IndexOutOfRangeException.

Fix:

Use GetLength(0) for the row count and GetLength(1) for the column count.

Using Jagged Syntax on a Rectangular Array

The two array shapes have different indexing syntax, and the compiler enforces the distinction.

What's wrong with this code?

grid is a rectangular int[,], so it needs one set of brackets with a comma inside. grid[0][2] is jagged syntax.

Fix:

If you actually want jagged behavior (independent rows), declare a jagged array instead. Lesson 04 covers the syntax in detail.

Trying to Resize a Multidimensional Array

A multidimensional array's shape is fixed at allocation. You can't add a row or a column later, and Array.Resize only works on one-dimensional arrays. If you need to grow the grid, allocate a new bigger array and copy values across. For data that genuinely changes shape over time, you probably want List<T> of something, not a multidimensional array.

A Worked Example: Sales Report

Pulling everything together: a [product][month] sales matrix, plus per-product totals, per-month totals, and the overall grand total.

One 2D array holds the data. The outer loop walks the products; the inner loop walks the months. Two parallel 1D arrays (products and months) hold the labels. A small flat array (monthTotals) accumulates the per-month totals while the per-product totals are computed inline. The grand total falls out by summing the product totals. This is the kind of code where the rectangular shape pays off: uniform data, regular access pattern, predictable size.

Summary

  • A multidimensional array (T[,], T[,,], and so on) is one rectangular block of memory addressed by multiple indices. The shape is fixed at allocation and every row has the same width.
  • Declare with commas inside the brackets: int[,] g. Allocate with new int[rows, cols]. Initialize inline with nested braces, one inner brace per row.
  • Access with grid[row, col] (one bracket, indices separated by a comma). Out-of-range indices throw IndexOutOfRangeException.
  • GetLength(dim) returns the size of one dimension; Length is the total cell count across all dimensions; Rank is the number of dimensions. Don't use Length as a loop bound.
  • Iterate with nested for loops when you need the coordinates. Use foreach when you only need the values; it flattens the array in row-major order.
  • C# stores multidimensional arrays in row-major layout. Outer loop on the row, inner loop on the column matches the memory order and gives better cache behavior on large grids.
  • 3D arrays (T[,,]) extend the pattern naturally. C# allows up to 32 dimensions, but anything past three is usually a sign you should restructure the data.
  • Pick the rectangular shape when the data is genuinely uniform: [product][month] sales, [row][col] warehouse grids, [category][rating] review tables, [store][product] price comparisons.

The _Jagged Arrays_ lesson covers jagged arrays (T[][]), which are arrays of arrays where each row can have its own length. It compares them with T[,] head-to-head and walks through when each shape is the right call.