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.
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.
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.
T[][], not T[,]. The syntax difference matters: no comma.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.
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
];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.
int[][] triangle = new int[4][]; // 4 rows — but each row is still null right now
null.triangle[0] = [1];
triangle[1] = [1, 2];
triangle[2] = [1, 2, 3];
triangle[3] = [1, 2, 3, 4];
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();
}
triangle[2][1] means "row 2, then position 1 in that row's array."triangle[r].Length, not a shared column count — every row can be a different length.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:
for walks each row; the inner foreach walks whatever that specific row contains, however long it is.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.
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.
int[] (or whatever T[] is) — and arrays are reference types.NULLnew int[4][] creates 4 slots that are all null until you assign an actual inner array to each one.triangle[0][0] before assigning triangle[0] throws a NullReferenceException — a very common jagged-array pitfall (see Common Mistakes below).| 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 |
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 nowWrong — 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]);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.
List<List<T>>, which you'll be ready for after the next lesson.Dictionary<TKey, TValue>.T[][] = two separate bracket pairs, no commagrid[r][c] = "go to row r's array, then index c inside it"null — you must assign each one before using it.T[][]) is an array of arrays — each row is its own independent array with its own length.grid[row][col] — two separate index operations, not one.null — always create an inner array before indexing into it, or you'll hit a NullReferenceException.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?
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?
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?
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?
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>.
dotnetmadeeasy.com — Learn C# and .NET, the right way.