AlgoMaster Logo

Jagged Arrays

Last Updated: May 17, 2026

12 min read

A jagged array is an array whose elements are themselves arrays, where each inner array can have a different length. C# writes the type as T[][], said out loud as "array of arrays of T." That shape fits the messy real data an online store actually has: a customer's order history where each order contains a different number of line items, a weekly delivery schedule where some days have three drops and others have none, or a category that owns one product list and another that owns fifty. This lesson covers how to declare jagged arrays, how to fill them, how to walk them, how their memory differs from T[,], and when to pick one over the other.

Why Jagged Instead of Rectangular

A multidimensional array (T[,]) is a rectangle. Every row has exactly the same number of columns, and the storage is a single contiguous block sized rows * cols. That fits a chessboard or a fixed-size price grid, but it doesn't fit data where rows vary in length.

Consider three customer orders. The first has two line items, the second has five, the third has one. With a rectangular array you'd need a 3-by-5 grid and you'd waste the empty slots in rows one and three, plus you'd need a sentinel value (like null or -1) to mark "this slot has no item."

A jagged array drops that constraint. Each row is its own array, sized to fit its actual contents. Row zero holds two items, row one holds five, row two holds one, and no slots are wasted.

The outer array is a small header object that holds three references, one per inner array. Each inner array lives in its own block of memory and carries its own length. This is the structural difference that makes ragged rows possible.

Declaring and Initializing

The declaration syntax mirrors the shape: one set of brackets for the outer array and another set for the inner. There's an important rule: you only size the outer array at declaration time. The inner arrays are created separately.

A few things worth pinning down. new int[3][] allocates the outer array with three slots, but those slots start as null because the inner arrays haven't been created yet. Reading orders[0][0] before assigning orders[0] = new int[2] throws a NullReferenceException, not an IndexOutOfRangeException. Each row's Length is independent because each row is a separate array object with its own length field.

You can also use a collection initializer to build the whole structure in one expression. This is the form you'll see most often in real code:

new[] { 10, 20 } is the array-creation shorthand. The compiler infers int[] from the element type. You could spell it out as new int[] { 10, 20 } and the meaning is identical. The outer array's length comes from the number of inner arrays you listed.

You can also use the full element form when you want to be explicit about the inner type:

Notice the third row, new int[] { }, is a perfectly valid zero-length array. A jagged array can hold empty rows. You can't do that with T[,] because every row in a rectangle must have the same number of columns.

Indexing With Two Bracket Pairs

The single biggest syntactic difference between jagged and rectangular arrays is how you index into them. A jagged array uses two separate index operations: one to get the row, one to get the column. A rectangular array uses a single index expression with a comma inside.

Read jagged[0][1] as "first get the array at row zero, then get its element at index one." The first set of brackets returns an int[], the second set returns an int. That's two separate operations the runtime performs, in order. With rect[0, 1], the comma is part of a single indexer call; the runtime computes the flat offset from the row and column in one step.

The two-step nature of jagged indexing has a visible consequence: you can hold onto a row as its own variable, because each row is a real array.

firstProductReviews holds the same reference as reviewsByProduct[0]. They're two names pointing to the same array on the heap, so modifying one is visible through the other. You can't do that with T[,]. A rectangular array doesn't have "real rows" you can hand out as separate objects, only positions you can read with rect[row, col].

Iterating With Nested Loops

Walking a jagged array with nested for loops is the bread-and-butter pattern, but the inner loop uses each row's own Length rather than a shared column count. That's the iteration consequence of ragged rows.

The outer loop walks the rows using orders.Length. The inner loop walks the items in each specific row using orders[i].Length. If you used a shared constant for the inner bound, you'd either skip data in long rows or overrun short rows.

foreach works just as well and is often cleaner when you don't need the indices:

The outer foreach produces each inner array as string[]; the inner foreach produces each string. The type of the outer loop variable, string[], makes the structure obvious to anyone reading the code.

A common aggregation pattern: total the items across all rows. The outer loop walks the customers, the inner loop walks the per-customer reviews, and a single running counter accumulates.

The averaging logic is exactly what it would be for a flat array, just with one extra loop wrapped around it. Eleven reviews, forty-four stars total, average four.

Mixed-Length Real-World Examples

A few patterns from a typical e-commerce app fit the jagged shape naturally. They all share the same feature: each row's length depends on data, not on a fixed schema.

Category to Product List

A catalog has categories, and each category owns a list of product names. Some categories have hundreds of products, others have a handful.

If you'd modeled this with string[,], you'd have had to pick a column count equal to the largest category (five) and pad the shorter categories with null or "". The jagged version stores exactly the data that exists and nothing extra.

Weekly Delivery Schedule

Pick-up windows vary by day. Some days have two windows, some have four, weekends might have none.

The two new string[0] entries are empty arrays. They're real array objects, just with zero elements, and they fit naturally into the string[][] shape. The check schedule[d].Length == 0 decides which days are closed.

Customer Orders With Line Items

The earlier int[][] order example becomes more realistic when each line item is itself a richer record. Even just storing an item count per line, plus a customer-by-customer breakdown, fits a jagged shape:

One pass per customer, aggregating along the row. This is what jagged arrays are for: the shape of the data and the shape of the storage match.

Memory Layout: Jagged Versus Multidim

The structural difference between T[][] and T[,] shows up clearly when you draw the heap. A multidimensional array is one contiguous block. A jagged array is a small outer array of references plus one separately allocated inner array per row.

Four practical consequences fall out of this picture.

One allocation versus many. The rectangular form is a single object on the heap. The jagged form is one outer object plus one inner object per row, so a 1000-row jagged array is 1001 heap allocations. For very small rows, the per-object overhead (the array header) becomes a noticeable fraction of total memory.

Locality. Reading neighboring cells of a rectangular array touches adjacent memory, which is friendly to the CPU's cache. Reading neighboring rows of a jagged array can jump to wherever each inner array happens to live in memory. For tight numerical loops over large grids, the rectangular form is usually faster because of this cache behavior.

Ragged rows. Rectangular arrays cannot have different row lengths. Jagged arrays can. If your data is genuinely ragged, picking rectangular forces you to pad or use a sentinel.

Row-as-object. Each row of a jagged array is a real T[] you can pass to methods, assign to a variable, replace wholesale, or share with other code. A row of a rectangular array doesn't exist as a separate object.

Comparison Table: Jagged Versus Multidim

When the trade-offs are this varied, a table makes the picture clearer than prose.

AspectJagged T[][]Multidim T[,]
ShapeArray of arrays; rows can vary in lengthFixed rectangle; all rows have the same length
MemoryOne outer array plus one inner array per rowSingle contiguous block
Heap allocations1 + rows1
Indexing syntaxarr[i][j] (two operations)arr[i, j] (one operation)
Row as a standalone objectYes, each row is a real T[]No, row is not a separate object
Length per rowarr[i].Length (per-row)arr.GetLength(1) (same for all rows)
Total lengtharr.Length is row count; sum the inner lengths for totalarr.Length is total cell count
Cache localityWorse; rows scattered on the heapBetter; cells are contiguous
Iteration speedSlower for tight numeric loopsFaster for tight numeric loops
Compile-time bounds checksNone (each inner length is a runtime value)Same (still runtime), but fixed at construction
Initialization syntaxnew[] { new[] { 1, 2 }, new[] { 3 } }new int[2, 2] { { 1, 2 }, { 3, 4 } }
Resizing a rowReplace the entire row referenceNot possible; rectangle is fixed
BCL / framework supportStrong; widely used in LINQ, JSON, genericsLimited; many APIs prefer T[] or T[][]
foreach over rowsYields each T[] directlyYields each cell directly, no row separation
Default value of an unfilled rownull (the row reference itself)All cells default to default(T)

The two rows worth lingering on are BCL support and foreach behavior. Most of the .NET ecosystem assumes either flat arrays or jagged arrays, not multidimensional ones. LINQ, for instance, treats T[,] as an opaque sequence of cells and loses the row structure entirely. JSON serialization through System.Text.Json works smoothly with T[][] (it round-trips as a nested JSON array) but doesn't handle T[,] cleanly. If you ever expect your data to flow through these APIs, jagged is almost always the better default.

Replacing and Adding Rows

Because each row is its own array, you can swap an entire row by assigning a new array to that slot. This is something a multidimensional array can't do.

The outer array's reference at index 0 now points to a four-element array instead of a two-element one. The old two-element array becomes eligible for garbage collection because nothing references it anymore.

You cannot grow the outer array itself, though. Like every array in C#, the outer array has a fixed length once it's created. To add a category, you'd allocate a new outer array of length oldLength + 1, copy the existing references over, and place the new row at the end. That's exactly what List<T[]> does for you behind the scenes, and for code that grows dynamically, List<T[]> or List<List<T>> is usually the better choice.

List<string[]> and string[][] look similar from the outside but solve different problems. The jagged array is right when the row count is known up front; the list is right when rows come and go.

When to Pick Which

A small set of questions answers "jagged or multidim?" reliably:

  • Do rows really have different lengths? If yes, use jagged. Rectangular wastes space and forces sentinel values.
  • Will the data flow through LINQ, JSON, or other BCL APIs? If yes, use jagged. Multidimensional arrays don't compose well with most of the ecosystem.
  • Will tight numeric loops over a large fixed grid be a bottleneck? If yes, multidim wins on cache locality. This includes things like image pixel buffers and game boards.
  • Do you ever need to hand out a row as its own object? If yes, only jagged supports that.
  • Is the shape fixed forever? If yes, multidim is fine. If the row count might grow, prefer List<T[]>.

In practice, most C# code that needs "two-dimensional" data ends up reaching for jagged arrays or List<T> of something. The T[,] form has narrow but real uses (image processing, certain scientific computations, fixed game grids), and outside of those it's rare in modern C# codebases.

A Worked Example: Order History Summary

Pulling several ideas together: a program that prints, for each of a few customers, how many orders they've placed, the largest single order, and the total units shipped.

A few details worth noticing. history is a copy of the reference to that customer's row; it doesn't allocate, it just gives us a clearer name inside the loop body. Dan's row is new int[0], so the inner loop runs zero times and largest and totalUnits stay at their initial values of 0. The single-pass aggregation per row mirrors what you'd do over a flat array, just wrapped in an outer loop that walks customers.

Initialization Without a Collection Initializer

You won't always know the inner sizes at compile time. When the row count and per-row sizes come from data (a database query, user input, an HTTP response), you allocate the outer array first and then fill each row in a loop.

Three things to note. The outer array is sized once from ordersPerCustomer.Length. Each inner array is allocated with its own per-customer size inside the loop. The data-filling step is just nested indexing. If you forgot the orderQuantities[c] = new int[...] step, the next line would throw NullReferenceException, because the inner slot would still be null.

Common Mistakes

A handful of mistakes show up over and over with jagged arrays.

Forgetting to allocate the inner arrays. new int[3][] creates the outer array with three null slots. You must assign an inner array to each slot before reading or writing.

The fix is to allocate each row first:

Mixing jagged and multidim indexing. Trying to use jagged[i, j] or multi[i][j] is a compile error. The syntax is part of the type. T[,] needs the comma form; T[][] needs two bracket pairs.

Using a single row length for the inner bound. Hardcoding the inner loop's bound (or reusing the first row's length) breaks any row that's longer or shorter.

Use orders[i].Length so each row's bound matches its own size.

Assuming `arr.Length` is the total cell count. For T[][], arr.Length is the number of rows, not the total number of cells. To count cells, sum the inner lengths.

For T[,], by contrast, arr.Length is the product of dimensions. Mixing these up is one of the most common bugs when porting code from one form to the other.

Summary

  • A jagged array is T[][], an array of arrays. Each inner array is a separate object on the heap and can have its own length. The outer array stores references to the inner arrays.
  • Declare with new int[rows][], then assign each inner array with arr[i] = new int[size]. Or use a collection initializer: new[] { new[] { 1, 2 }, new[] { 3, 4, 5 } }.
  • Index with two bracket pairs: arr[i][j]. The first returns the inner array; the second returns the cell. This is different from T[,], which uses arr[i, j] as one indexer call.
  • Iterate with nested loops where the inner bound is the current row's own arr[i].Length, not a shared constant. foreach over T[][] yields each inner array, which is often cleaner than nested indexing.
  • Memory layout: T[,] is one contiguous block; T[][] is one outer array plus one inner array per row. The rectangular form has better cache locality for tight loops, but the jagged form supports ragged rows and lets you hand out individual rows as standalone objects.
  • Pick jagged when rows have different sizes, when the data flows through LINQ or JSON, or when you need to share rows as separate objects. Pick multidim when you have a fixed rectangle and tight numerical loops over large grids.
  • Common mistakes: forgetting to allocate inner arrays (each starts as null), using a single row's length for every row's iteration bound, mixing arr[i, j] with T[][] syntax, and assuming arr.Length is the total cell count when it's only the row count.