.net made easy[]

C# Jagged Arrays: Arrays of Arrays Explained

BeginnerLesson 27 of 315·C# Foundations·17 min read·Updated

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.

Jagged Array = An Array Whose Elements Are Arrays

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 Support

That 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

A JAGGED ARRAY — string[][]
teams[0]
Amy
Ben
Cara
length 3
teams[1]
Gia
Hugo
Ivy
Jax
Kim
length 5 (…and more)
teams[2]
Uma
Vik
length 2 (…and more)
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

BUILDING A JAGGED ARRAY, STEP BY STEP
1. CREATE THE OUTER ARRAY
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.
▼
2. CREATE EACH INNER ARRAY SEPARATELY
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.
▼
3. OR INITIALIZE IT ALL AT ONCE
int[][] triangle2 =
[
    [1],
    [1, 2],
    [1, 2, 3],
    [1, 2, 3, 4],
];
▼
4. READ, WRITE, AND ITERATE
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 4

Code → Meaning → Result:

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 // - Wes

Notice 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

A Bookshelf With Uneven Shelves

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

HOW A JAGGED ARRAY IS STORED
1. THE OUTER ARRAY HOLDS REFERENCES, NOT VALUES
  • The outer array is a normal array whose element type is int[] (or whatever T[] 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.
▼
2. MULTIPLE SEPARATE ALLOCATIONS, NOT ONE BLOCK
  • 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.
▼
3. AN OUTER SLOT CAN BE NULL
  • Because outer slots just hold references, new int[4][] creates 4 slots that are all null until you assign an actual inner array to each one.
  • Trying to access triangle[0][0] before assigning triangle[0] throws a NullReferenceException — 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

FeatureRectangular T[,]Jagged T[][]
Row lengthsAll equal, alwaysIndependent — each row its own length
Memory layoutOne contiguous blockMultiple separate arrays (heap allocations)
Access syntaxgrid[r, c] — one indexer callgrid[r][c] — two separate indexer calls
Size checkGetLength(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 existYes — 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 array

Correct — create the inner array first:

int[][] rows = new int[3][]; rows[0] = new int[5]; rows[0][0] = 5; // works now

Mistake 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 shape

Correct — 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

Jagged array = an array of arrays, each row independent
T[][] = two separate bracket pairs, no comma
grid[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


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>.