C# Jagged Arrays: Arrays of Arrays Explained
A jagged array is an array of arrays — each row is its own independent array, free to be a completely different length.
In the last lesson, a rectangular array modeled a theater with 8 seats in every single row — a perfect grid. But real data is often messier than that. A company's engineering team might have 6 people, its sales team 14, and its support team 3. There's no rectangle that fits that shape without wasting space or lying about how many "seats" each team has.
In this lesson, you'll learn what a jagged array is, how it differs from the rectangular array you just learned, when its flexibility is exactly what you need, and the trade-offs it brings.
What Is It?
The Simple Explanation
A jagged array is an array where each element is itself a separate array — and those inner arrays don't have to be the same length. Picture a bookshelf where each shelf holds a different number of books. The shelf itself is one row; how many books sit on it is entirely up to that shelf.
The Technical Definition
In C#, a jagged array is written with separate pairs of square brackets: T[][]. The outer array is a normal 1D array whose element type happens to be another array (T[]). Each of those inner arrays is independently created, independently sized, and can even be null until you assign it.
- Two levels — an outer array of rows, and each row is its own independent inner array.
- Independent lengths — row 0 might have 3 elements, row 1 might have 10.
- Separate brackets —
T[][], notT[,]. The syntax difference matters: no comma.
Why Does It Exist?
The Problem
Suppose you want to store the team members in every department of a company: Engineering has 6 people, Sales has 14, Support has 3. A rectangular array would force every row to be the length of the longest one:
string[,] teams = new string[3, 14]; // wastes 8 empty slots in Engineering, 11 in SupportThat wastes memory and, worse, makes empty slots ambiguous — is an empty slot "no employee here" or a genuine data error? The rectangle lies about the true shape of the data.
The Solution
A jagged array lets each row be exactly the size it needs to be — no wasted space, no fake "empty" entries:
string[][] teams =
[
["Amy", "Ben", "Cara", "Dev", "Elle", "Finn"], // Engineering: 6
["Gia", "Hugo", "Ivy", "Jax", "Kim", "Leo", "Moe", "Nia",
"Omar", "Pia", "Quin", "Rex", "Sam", "Tia"], // Sales: 14
["Uma", "Vik", "Wes"], // Support: 3
];Big Picture
teams is one outer array of 3 rows. Each row is its own independent array, with its own length. No wasted slots, no forced uniformity.
How It Works
int[][] triangle = new int[4][]; // 4 rows — but each row is still null right now
- This only creates the outer array — 4 slots that will each hold an inner array. Each slot starts out
null.
triangle[0] = [1];
triangle[1] = [1, 2];
triangle[2] = [1, 2, 3];
triangle[3] = [1, 2, 3, 4];
- Each row is its own array, built and sized independently — this is impossible with a rectangular array.
int[][] triangle2 =
[
[1],
[1, 2],
[1, 2, 3],
[1, 2, 3, 4],
];
int value = triangle[2][1]; // row 2, then index 1 within that row → 2
for (int r = 0; r < triangle.Length; r++)
{
for (int c = 0; c < triangle[r].Length; c++) // each row asks ITS OWN Length
Console.Write($"{triangle[r][c]} ");
Console.WriteLine();
}
- Two separate index operations, one per bracket pair —
triangle[2][1]means "row 2, then position 1 in that row's array." - Notice the inner loop checks
triangle[r].Length, not a shared column count — every row can be a different length.
Simple Example
int[][] triangle =
[
[1],
[1, 2],
[1, 2, 3],
[1, 2, 3, 4],
];
for (int r = 0; r < triangle.Length; r++)
{
foreach (int n in triangle[r])
Console.Write($"{n} ");
Console.WriteLine();
}
// 1
// 1 2
// 1 2 3
// 1 2 3 4Code → Meaning → Result:
- Each row genuinely has a different length — a triangle simply can't be represented as a rectangle without wasted, meaningless cells.
- The outer
forwalks each row; the innerforeachwalks whatever that specific row contains, however long it is.
Real-World Example
A company's org chart tool needs to print every department along with its members. Departments genuinely have different headcounts, so a jagged array models this honestly.
string[] departmentNames = ["Engineering", "Sales", "Support"];
string[][] departmentMembers =
[
["Amy", "Ben", "Cara", "Dev", "Elle", "Finn"],
["Gia", "Hugo", "Ivy", "Jax", "Kim", "Leo", "Moe",
"Nia", "Omar", "Pia", "Quin", "Rex", "Sam", "Tia"],
["Uma", "Vik", "Wes"],
];
for (int d = 0; d < departmentNames.Length; d++)
{
string[] members = departmentMembers[d];
Console.WriteLine($"{departmentNames[d]} ({members.Length} people):");
foreach (string name in members)
Console.WriteLine($" - {name}");
}
// Engineering (6 people):
// - Amy
// - Ben
// - Cara
// - Dev
// - Elle
// - Finn
// Sales (14 people):
// - Gia
// ...
// Support (3 people):
// - Uma
// - Vik
// - WesNotice how naturally members.Length reports the true size of each department — 6, 14, and 3 — with no wasted memory and no ambiguous "empty seat" values to filter out.
Analogy
Picture a bookshelf with 4 shelves. Shelf 1 holds a single thick encyclopedia. Shelf 2 holds 6 paperbacks. Shelf 3 holds 20 thin comic books. Shelf 4 is completely empty. The shelf unit is the outer array — it has 4 fixed slots, one per shelf. But what's on each shelf is its own independent collection, unrelated in size to any other shelf.
That's a jagged array: a fixed number of "shelves" (the outer array), each holding its own independently-sized inner array. A rectangular array, by contrast, would be a shelf unit where every shelf is legally required to hold the exact same number of books, whether or not you actually have that many.
Under the Hood
- The outer array is a normal array whose element type is
int[](or whateverT[]is) — and arrays are reference types. - So the outer array actually stores references to separate inner arrays, which can live anywhere on the heap — not necessarily next to each other.
- A rectangular array is one contiguous memory block. A jagged array is several separate blocks — one for the outer array, and one more for each inner array.
- This is more flexible (rows can grow, shrink, or be reassigned independently) but slightly less cache-friendly, since the inner arrays may be scattered in memory rather than sitting side by side.
NULL- Because outer slots just hold references,
new int[4][]creates 4 slots that are allnulluntil you assign an actual inner array to each one. - Trying to access
triangle[0][0]before assigningtriangle[0]throws aNullReferenceException— a very common jagged-array pitfall (see Common Mistakes below).
Common Confusion
Jagged array vs rectangular array — the syntax difference is tiny, the meaning is huge
| Feature | Rectangular T[,] | Jagged T[][] |
|---|---|---|
| Row lengths | All equal, always | Independent — each row its own length |
| Memory layout | One contiguous block | Multiple separate arrays (heap allocations) |
| Access syntax | grid[r, c] — one indexer call | grid[r][c] — two separate indexer calls |
| Size check | GetLength(0)/GetLength(1) | grid.Length for rows, grid[r].Length per row |
Can a row be null? | No — cells default to 0/null but rows always exist | Yes — an unassigned row is null until set |
Common Mistakes
Mistake 1 — Forgetting to initialize each inner row
Wrong — this throws NullReferenceException:
int[][] rows = new int[3][];
rows[0][0] = 5; // rows[0] is still null — you never gave it an inner arrayCorrect — create the inner array first:
int[][] rows = new int[3][];
rows[0] = new int[5];
rows[0][0] = 5; // works nowMistake 2 — Using a shared column count in the inner loop
Wrong — assumes every row has the same length, defeating the point of a jagged array:
for (int c = 0; c < triangle[0].Length; c++) // uses row 0's length for every row
Console.Write(triangle[3][c]); // will miss elements or throw, depending on shapeCorrect — always ask each row for its own length:
for (int c = 0; c < triangle[3].Length; c++)
Console.Write(triangle[3][c]);Mistake 3 — Reaching for a jagged array when the data is actually uniform
If every row genuinely is the same length, a jagged array adds unnecessary complexity and extra heap allocations compared to a simple rectangular array. Only use a jagged array when row lengths genuinely differ.
When Should I Use It?
Use a jagged array when
- Rows genuinely have different, meaningful lengths (org charts, triangular data, per-item variable-length records).
- You want to avoid wasting memory on cells that don't correspond to real data.
- You need each row to be independently reassignable to a whole new array.
Reach for something else when
- Every row genuinely has the same length → use a rectangular array (previous lesson) — simpler and more compact.
- The number of rows itself needs to grow or shrink at runtime → consider
List<List<T>>, which you'll be ready for after the next lesson. - You're modeling key-based lookups rather than positional rows → use
Dictionary<TKey, TValue>.
Mental Model
T[][] = two separate bracket pairs, no commagrid[r][c] = "go to row r's array, then index c inside it"Remember:
· Each row is its own array — different lengths are not just allowed, they're the whole point.
· Rows start as
null — you must assign each one before using it.· Use rectangular arrays when rows are uniform; use jagged arrays when they aren't.
Key Takeaway
- A jagged array (
T[][]) is an array of arrays — each row is its own independent array with its own length. - It exists for data where rows genuinely aren't uniform — a rectangular array would waste space or misrepresent the shape.
- Access with
grid[row][col]— two separate index operations, not one. - Under the hood, each row is a separate heap allocation, unlike the single contiguous block behind a rectangular array.
- Outer slots start
null— always create an inner array before indexing into it, or you'll hit aNullReferenceException.
Check Your Understanding
You've seen how jagged arrays let each row have its own independent length. Time to check your understanding.
1. What is the defining feature of a jagged array compared to a rectangular array?
Show answer
Correct: B
Why B is correct: A jagged array (T[][]) is an array whose elements are themselves arrays. Each of those inner arrays is created and sized independently, so different rows can have different lengths — unlike a rectangular array, where every row is forced to be the same length.
Why A is incorrect: Jagged arrays work with any type — int[][], Product[][], and so on, not just strings.
Why C is incorrect: Rectangular arrays are typically faster for uniform data, since they're one contiguous block rather than several scattered allocations.
Why D is incorrect: Jagged arrays can be modified after creation — you can reassign an entire row to a new array, or change individual elements.
Reinforcement: The whole point of a jagged array is independent row lengths — that's what "jagged" (uneven) refers to.
2. After running int[][] rows = new int[3][];, what is rows[0] before you assign anything to it?
Show answer
Correct: B
Why B is correct: new int[3][] only allocates the outer array — 3 slots that will each eventually hold a reference to an inner int[]. Until you explicitly assign one, each slot's default value (like any uninitialized reference-type slot) is null.
Why A is incorrect: No inner array has been created yet at all — there's no empty array sitting there, just a null reference.
Why C is incorrect: Nothing was sized to 3 elements — the outer array has 3 slots, but each slot's own inner array hasn't been created.
Why D is incorrect: This code compiles and runs fine; the null reference only becomes a problem if you try to index into rows[0] before assigning it.
Reinforcement: Creating the outer jagged array does not create any inner arrays — that's always a separate step.
3. An org-chart tool needs to store each department's list of employees, where department sizes range from 2 to 40 people. Which structure best fits, and why?
Show answer
Correct: B
Why B is correct: Department sizes vary wildly (2 to 40) — a jagged array lets each department's inner array be exactly the right size, with no wasted or ambiguous slots.
Why A is incorrect: A rectangular array sized to the biggest department (40) would waste up to 38 unused slots per smaller department, and you'd need a separate way to know how many entries in each row are "real."
Why C is incorrect: Mixing everyone into one flat array loses the grouping by department entirely — you'd have no way to tell where one department ends and the next begins.
Why D is incorrect: The two structures behave very differently here — one wastes memory and needs extra bookkeeping, the other represents the true shape of the data directly.
Reinforcement: Jagged arrays exist precisely for data where "row length" is genuinely variable and meaningful, like department headcounts.
4. Why is a jagged array typically less cache-friendly than a rectangular array of the same total size?
Show answer
Correct: B
Why B is correct: The outer array of a jagged array holds references to independently allocated inner arrays. Those inner arrays can end up anywhere on the heap, unlike a rectangular array's single unbroken memory block — so a rectangular array is generally more compact and faster to scan sequentially.
Why A is incorrect: Arrays store whatever type you declare them with (e.g. int), not text — this has nothing to do with memory layout.
Why C is incorrect: Neither array type sorts its contents automatically.
Why D is incorrect: There is a real, well-known performance difference for uniform data — it's one of the trade-offs to weigh when choosing between the two.
Reinforcement: Flexibility (independent row lengths) comes at the cost of memory locality — that's the core trade-off between jagged and rectangular arrays.
You now know both flavors of multidimensional data in C# — rectangular for uniform grids, jagged for irregular ones. Next, you'll meet a collection that can grow and shrink on its own: List<T>.