Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

C# & .NET (C# 14, .NET 10 LTS)  Module 5 – Arrays and collections

Arrays, indices and ranges

Create, sort and search C# arrays, slice them with ^ and .., choose int[,] or int[][] for a grid, and see which slices copy and which share memory.

  • Beginner
  • 15 minutes
  • Examples run with .NET 10.0.401
  • By MySmartCoPilot
Free lesson, Pro answers: 15 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

What you will learn

  • Create, fill, sort and search arrays with Array.Sort and Array.BinarySearch
  • Choose between a rectangular int[,] and a jagged int[][] for a grid
  • Slice an array with ^ and .. and predict whether the slice is a copy or a view
  • Explain why storing an int through an object[] can throw ArrayTypeMismatchException

Before you start

What you’ll learn · Before you start

What you will learn

  • Create, fill, sort and search arrays with Array.Sort and Array.BinarySearch
  • Choose between a rectangular int[,] and a jagged int[][] for a grid
  • Slice an array with ^ and .. and predict whether the slice is a copy or a view
  • Explain why storing an int through an object[] can throw ArrayTypeMismatchException

Before you start

On this page

A sleeper coach has 72 berths, and a booking program keeps the 12 booked so far in an int[12]. Sort it, and Array.BinarySearch answers “is berth 40 taken?” in 4 comparisons; ask for booked[12] instead and the program throws, because the last index is 11.

Arrays are where every C# collection starts. You will create, fill, sort and search them, lay out a grid as int[,] or int[][], cut parts out of an array with ^ and .. and see which cuts copy, and meet the one array rule that can fail at run time in code that compiles.

An array has a fixed length and starts with default values

new int[8] makes an array of 8 ints, and every one of them is 0 before you store anything. Each element starts with the default value of its type: 0 for numbers, false for bool, null for a string or any other reference. Indexes run from 0 to Length - 1, so a 4-element array has no [4]. A collection expression such as [12, 19, 23, 31] (C# 12) creates an array that already holds its values. Run the basics:

basics.cs: lengths, default values, an index too far, and Resize C# · basics/basics.cs
// A coach's fares and bookings in arrays: fixed lengths, default values, and what Resize does.
int[] fares = new int[8];
Console.WriteLine($"new int[8]       Length {fares.Length}, fares[0] = {fares[0]}, fares[7] = {fares[7]}");

string[] names = new string[3];
Console.WriteLine($"new string[3]    names[0] is null: {names[0] is null}");

int[] booked = [12, 19, 23, 31];   // a collection expression (C# 12) makes an int[4]
Console.WriteLine($"booked           [{string.Join(", ", booked)}], indexes 0 to {booked.Length - 1}");

try
{
    Console.WriteLine(booked[4]);
}
catch (IndexOutOfRangeException e)
{
    Console.WriteLine($"booked[4]        {e.GetType().Name}: the last index is 3");
}

// Resize cannot grow an array: it makes a new one, copies, and moves the variable to it.
int[] alias = booked;
Array.Resize(ref booked, 6);
Console.WriteLine($"after Resize     booked [{string.Join(", ", booked)}], Length {booked.Length}");
Console.WriteLine($"                 alias  [{string.Join(", ", alias)}], Length {alias.Length}");
Console.WriteLine($"                 the same array: {ReferenceEquals(alias, booked)}");

Array.Fill(fares, 450);
fares[^1] = 610;
Console.WriteLine($"Fill, then [^1]  [{string.Join(", ", fares)}]");

Output

new int[8]       Length 8, fares[0] = 0, fares[7] = 0
new string[3]    names[0] is null: True
booked           [12, 19, 23, 31], indexes 0 to 3
booked[4]        IndexOutOfRangeException: the last index is 3
after Resize     booked [12, 19, 23, 31, 0, 0], Length 6
                 alias  [12, 19, 23, 31], Length 4
                 the same array: False
Fill, then [^1]  [450, 450, 450, 450, 450, 450, 450, 610]

Recorded with .NET SDK 10.0.401 (runtime 10.0.12) on macOS 26 arm64. To run it yourself: dotnet run basics.cs

An array is a reference type: A type whose variables hold a reference to an object, such as a class, string or array. Assigning or passing such a variable copies the reference, so both variables lead to the same object. More in “Value types and reference types”, and its length is fixed when it is made: no method makes an existing array longer. Array.Resize looks as if it does, but the reference documentation says it allocates a new array, copies the elements and replaces the array the variable refers to. Step through the run and watch the two variables:

Interactive · step by step What Array.Resize does to two variables Step through the Resize lines of basics.cs. Variables stand on the left, the arrays on the right.
What Array.Resize does to two variables, step 6 of 6. Showing booked, int[4], alias, int[6] (12, 19, 23, 31, 0, 0). In focus: alias, int[4].bookedint[4]12, 19, 23, 31aliasint[6]12, 19, 23, 31, 0, 0

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. One array. booked refers to an array of 4 ints. The variable holds a reference; the 4 values live in the array object, whose length was fixed when it was made.
  2. int[] alias = booked;. Assigning an array copies the reference, not the values: alias and booked now refer to the same array.
  3. A new array of 6. Array.Resize(ref booked, 6) cannot make the old array longer. It allocates a new array of 6 ints, each set to its default value, 0.
  4. The values are copied. Resize copies the 4 values into the new array. Its last 2 elements keep the default 0, which is why the run printed 12, 19, 23, 31, 0, 0.
  5. booked moves. booked was passed with ref, so Resize can change the variable itself: it now refers to the new array of 6.
  6. alias stays. Nothing changed alias. It still refers to the old array of 4, so the run printed Length 4 for it and False for ReferenceEquals(alias, booked).

That is why the run printed Length 6 for booked and Length 4 for alias, and False when it asked whether they were the same array. Array.Fill(fares, 450) set all 8 fares at once, and fares[^1] = 610 changed the last one: ^1 counts one back from the end, as a later section shows.

Growing an array one element at a time

Array.Resize(ref data, data.Length + 1) before every new reading copies the whole array each time, so 10,000 readings copy about 50 million elements. Add the readings to a List<int>, which grows by doubling, as the next lesson measures, and call ToArray() once at the end.

Checkpoint

Check: Fixed length and default values

  1. Question 1 of 2 What does new bool[3] hold before anything is stored in it?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: false, false, false

    An array is created with every element set to its default value: 0 for numbers, false for bool, null for references. basics.cs showed it for new int[8] and new string[3].

  2. Question 2 of 2 int[] a = [1, 2, 3]; int[] b = a; Array.Resize(ref a, 5); What is b.Length now?

    Type a number.

    A hint

    Resize cannot make an array longer. What does it do instead, and which variable does it change?

    Show the answer: Question 2 of 2

    Answer: 3 elements

    Resize allocates a new array of 5, copies 1, 2 and 3 into it and moves a, the variable passed with ref, to the new array. b still refers to the old array of 3, exactly as alias kept Length 4 in basics.cs.

Sort the array, then BinarySearch halves it at every step

Array.Sort(booked) puts the 12 booked berths in order inside the same array; it returns nothing because it makes no new array. On sorted data, Array.BinarySearch compares the value it looks for with the middle element and drops the half that cannot hold it, which is a binary search: A search of sorted values that compares the value it looks for with the middle element and drops the half that cannot hold it, so n values take at most ⌊log₂ n⌋ + 1 comparisons: 4 for 12 values, 20 for a million.. The comparer in this program writes down every element it is asked to compare:

search.cs: Sort, BinarySearch, the complement and an unsorted trap C# · search/search.cs
// Berths already booked in a 72-berth coach, in the order people booked them.
int[] booked = [45, 12, 67, 3, 31, 52, 8, 23, 60, 19, 38, 71];

Array.Sort(booked);   // sorts this array in place: no new array
Console.WriteLine($"sorted        [{string.Join(", ", booked)}]");

Console.WriteLine($"search 45  -> {Array.BinarySearch(booked, 45)}");

// A comparer that writes down every value BinarySearch compares with 40.
var probes = new Probes();
int result = Array.BinarySearch(booked, 40, probes);
Console.WriteLine($"search 40  -> {result}, after comparing with {string.Join(", ", probes.Seen)}");
Console.WriteLine($"~{result} = {~result}: 40 would go at index {~result}, in front of {booked[~result]}");

// Insert 40 where the complement says, and the array stays sorted.
int at = ~result;
int[] withForty = [.. booked[..at], 40, .. booked[at..]];
Console.WriteLine($"inserted      [{string.Join(", ", withForty)}]");

// On data that is not sorted, BinarySearch can miss a value that is there.
int[] unsorted = [45, 12, 67, 3, 31];
Console.WriteLine($"unsorted: BinarySearch(3) -> {Array.BinarySearch(unsorted, 3)}, IndexOf(3) -> {Array.IndexOf(unsorted, 3)}");

sealed class Probes : IComparer<int>
{
    public List<int> Seen { get; } = [];

    // BinarySearch calls Compare(element, value) once for each element it looks at.
    public int Compare(int element, int value)
    {
        Seen.Add(element);
        return element.CompareTo(value);
    }
}

Output

sorted        [3, 8, 12, 19, 23, 31, 38, 45, 52, 60, 67, 71]
search 45  -> 7
search 40  -> -8, after comparing with 31, 52, 38, 45
~-8 = 7: 40 would go at index 7, in front of 45
inserted      [3, 8, 12, 19, 23, 31, 38, 40, 45, 52, 60, 67, 71]
unsorted: BinarySearch(3) -> -1, IndexOf(3) -> 3

Recorded with .NET SDK 10.0.401 (runtime 10.0.12) on macOS 26 arm64. To run it yourself: dotnet run search.cs

Searching for 45 returned its index, 7. Searching for 40 returned −8, after comparing with 31, 52, 38 and 45. A negative result means “not there”, and its bitwise complement, ~(-8), is 7: the index of the first element larger than 40, where 40 must go to keep the array sorted. The program inserted it there. Step through the same search on the same 12 values and watch the range shrink:

Interactive · step through it Binary search for berth 40 in the 12 booked berths Each probe halves the range where the target can still be; the dimmed values are ruled out.
Array of 12: 3 8 12 19 23 31 38 45 52 60 67 71. lo at 0, hi at 11.308112219323431538645752860967107111lohi
  • being compared or looked at
  • visited or ruled out

Pseudo-code

  1. lo ← 0; hi ← n − 1
  2. while lo ≤ hi:
  3. mid ← lo + ⌊(hi − lo) / 2⌋
  4. if a[mid] = target: return mid
  5. if a[mid] < target: lo ← mid + 1
  6. else: hi ← mid − 1
  7. return "not found"

Look for 40. The invariant: if 40 is in the array, it is somewhere in a[lo..hi]. At the start that range is the whole array, positions 0 to 11.

When it finishes: 40 not found.

Probes 0
Positions left 12

When the range is empty, lo stops at 7, the same 7 that ~(-8) gave. Each probe halves what is left, so 12 values take at most 4 probes, and a million take at most 20. The last line of the run is the trap: on the unsorted [45, 12, 67, 3, 31], BinarySearch(3) returned −1 although 3 is at index 3, because after 67 and 45 it had dropped the positions where 3 sat. Array.IndexOf checks every element in turn, so it works on any order, at the cost of a full scan.

Key idea

Array.BinarySearch is right only on sorted data. A result of 0 or more is the index of the value, and a negative result is the complement of the index where the value belongs.

Checkpoint

Check: Sort and BinarySearch

  1. Question 1 of 2 Array.BinarySearch on a sorted int[] returns -5. At which index must the value be inserted to keep the array sorted?

    Type a number.

    A hint

    Apply the bitwise complement, ~, to the result.

    Show the answer: Question 1 of 2

    Answer: 4 index

    A negative result is the complement of the index of the first element larger than the value: ~(-5) is 4. In search.cs, -8 gave 7, and 40 went in front of 45 at index 7.

  2. Question 2 of 2 Why did BinarySearch(3) return -1 on [45, 12, 67, 3, 31], although 3 is at index 3?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: It assumes sorted data, and after comparing with 67 and 45 it had dropped the positions where 3 sat

    BinarySearch never checks that the array is sorted. On unsorted data its halving steps discard the wrong halves, and the documentation warns that the result can be negative even when the value is present. Sort first, or use Array.IndexOf.

A grid is one block of values or an array of rows

A chair-car coach is a grid of rows and seats, and C# gives it two shapes. A multidimensional array: An array with more than one index, such as int[3, 4], read as seats[row, seat]. It is one object with every row the same length; Length counts all its elements and GetLength(d) gives the length of dimension d., int[3, 4], is one object with the same 4 seats in every row, read as seats[row, seat]. A jagged array: An array whose elements are arrays, such as int[][], read as bays[row][berth]. Each row is an array of its own, may have its own length, and is null until it is created., int[3][], is an array whose elements are arrays, read as bays[row][berth], and each row may have its own length, as a sleeper coach’s last bay of 2 side berths does:

grid.cs: a seat grid as int[3, 4] and a sleeper coach as int[][] C# · grid/grid.cs
using System.Globalization;

// A chair-car coach as a grid of 3 rows and 4 seats: 0 is free, 1 is booked.
int[,] seats = new int[3, 4];
seats[0, 1] = 1;
seats[2, 3] = 1;
Console.WriteLine($"int[3, 4]   Rank {seats.Rank}, Length {seats.Length}, GetLength(0) {seats.GetLength(0)}, GetLength(1) {seats.GetLength(1)}");
Console.Write("foreach     ");
foreach (int seat in seats)
{
    Console.Write(seat);   // row 0 first, then row 1, then row 2
}
Console.WriteLine();

// A sleeper coach as a jagged array: each bay is its own array, of any length.
int[][] bays = new int[3][];
Console.WriteLine($"int[3][]    bays[0] is null until it is created: {bays[0] is null}");
bays[0] = new int[8];   // a bay with 8 berths
bays[1] = new int[8];
bays[2] = new int[2];   // the 2 side berths at the end
bays[2][1] = 1;
Console.WriteLine($"bays        Length {bays.Length}; rows of {bays[0].Length}, {bays[1].Length} and {bays[2].Length}; bays[2][1] = {bays[2][1]}");

// What each layout allocates for a million ints.
long before = GC.GetAllocatedBytesForCurrentThread();
int[,] block = new int[1000, 1000];
long blockBytes = GC.GetAllocatedBytesForCurrentThread() - before;

before = GC.GetAllocatedBytesForCurrentThread();
int[][] rows = new int[1000][];
for (int i = 0; i < rows.Length; i++)
{
    rows[i] = new int[1000];
}
long rowBytes = GC.GetAllocatedBytesForCurrentThread() - before;

CultureInfo inv = CultureInfo.InvariantCulture;
Console.WriteLine($"int[1000, 1000]          1 object      {blockBytes.ToString("N0", inv),10} bytes");
Console.WriteLine($"int[1000][] with rows    1,001 objects {rowBytes.ToString("N0", inv),10} bytes");
GC.KeepAlive(block);
GC.KeepAlive(rows);

Output

int[3, 4]   Rank 2, Length 12, GetLength(0) 3, GetLength(1) 4
foreach     010000000001
int[3][]    bays[0] is null until it is created: True
bays        Length 3; rows of 8, 8 and 2; bays[2][1] = 1
int[1000, 1000]          1 object       4,000,040 bytes
int[1000][] with rows    1,001 objects  4,032,024 bytes

Recorded with .NET SDK 10.0.401 (runtime 10.0.12) on macOS 26 arm64. To run it yourself: dotnet run grid.cs

Step through how each one sits in memory:

Interactive · step by step One block, or an array of rows Step through grid.cs. The variables are on the left; the arrays they refer to are on the right.
One block, or an array of rows, step 6 of 6. Showing seats, 1 object of 12 ints, bays, int[3][] (3 references to rows), int[8], int[8], int[2]. In focus: 1 object of 12 ints, int[3][].seatsint[3, 4]1 object of 12 intsrow 0, row 1, then row 2baysint[3][]int[3][]3 references to rowsint[8]bays[0]: 8 berthsint[8]bays[1]: 8 berthsint[2]bays[2]: 2 side berths

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. new int[3, 4]. A rectangular array is one object. All 12 ints sit in one block, row 0 first, and every row has the same 4 seats. seats[0, 1] is row 0, seat 1.
  2. foreach reads the block. foreach visits the 12 values in the order they sit: the last index changes fastest, so row 0, then row 1, then row 2. The run printed 010000000001.
  3. new int[3][]. A jagged array starts as an array of 3 references to rows. No row exists yet, so each reference is null, and bays[0].Length would throw.
  4. bays[0] = new int[8]. Each row is an array of its own, created by its own new. The outer array now refers to an int[8].
  5. Rows of any length. The other two rows hold 8 berths and 2 side berths. bays.Length is 3, the number of rows; each row has its own Length.
  6. Objects and bytes. Here that is 1 object against 4. For a million ints the run counted 4,000,040 bytes in 1 object, and 4,032,024 bytes in 1,001 objects for the rows of rows.

Length means different things in the two: all 12 seats of the int[3, 4], but only the 3 rows of the jagged array. The table sums up the differences the run showed:

What differs int[,] rectangular int[][] jagged
Element seats[1, 2] bays[1][2]
Row lengths all the same each row its own
Length every element: 12 the rows: 3
Dimensions GetLength(0), GetLength(1) bays.Length, bays[r].Length
A new grid ready to use, all 0 every row null until created
Objects for a million ints 1, 4,000,040 bytes 1,001, 4,032,024 bytes
Ranges and one row on its own neither bays[1..], bays[2]

Choose by the data: a board or an image whose rows always match fits int[,], while rows that differ, or rows you pass to a method one at a time, fit int[][]. The extra memory of the jagged form was under 1 % here.

Checkpoint

Check: Rectangular and jagged grids

  1. Question 1 of 2 What is the Length of new int[3, 4]?

    Type a number.

    Show the answer: Question 1 of 2

    Answer: 12 elements

    On a multidimensional array, Length counts every element: 3 rows × 4 seats = 12, as grid.cs printed. GetLength(0) gives the 3 rows and GetLength(1) the 4 seats.

  2. Question 2 of 2 int[][] bays = new int[3][]; What happens on the next line, Console.WriteLine(bays[0].Length);?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: It throws NullReferenceException

    A jagged array is an array of references to rows. grid.cs printed True for bays[0] is null before the rows were created with new int[8], new int[8] and new int[2].

A range copies an array, and a span slice shares it

An index from end: An index written with ^, such as ^1, that counts back from the end of a string, array or span: ^1 is the last element, ^2 the one before it, and ^0 the position just past the end. More in “Strings: slicing, immutability and comparison” such as fares[^1] is the last element, and a range: Two indexes written a..b, such as s[4..8]: the part of a string, array or span from index a up to, but not including, index b. Either end may be left out, as in s[..3] or s[^2..]. More in “Strings: slicing, immutability and comparison” such as fares[1..4] is the elements from index 1 up to, but not including, index 4. On an array, the language specification says a range is a new array that holds a copy of those elements. A span: A Span<T> or ReadOnlySpan<T>: a view of a run of values that sit next to each other in memory, such as part of an array, which reads and writes them where they are. array.AsSpan(1, 3) refers to three elements of array and copies nothing. is different: fares.AsSpan()[1..4] is a view of the same three elements, with no array of its own. This program changes fares after taking both slices:

slices.cs: a copy and a view of the same three fares C# · slices/slices.cs
// Fares of six trains on one route, from the cheapest to the dearest.
int[] fares = [450, 520, 610, 700, 880, 990];
Console.WriteLine($"fares[^1] = {fares[^1]}, fares[^2] = {fares[^2]}, fares[1..4] = [{string.Join(", ", fares[1..4])}]");

int[] copy = fares[1..4];               // a range on an array: a new array of 3
Span<int> view = fares.AsSpan()[1..4];  // a range on a span: the same 3 elements of fares

fares[1] = 555;
Console.WriteLine($"after fares[1] = 555    copy[0] = {copy[0]}, view[0] = {view[0]}");
view[2] = 777;
Console.WriteLine($"after view[2] = 777     fares[3] = {fares[3]}, copy[2] = {copy[2]}");

// What 1,000 slices of 100 values each cost.
int[] prices = new int[1_100];
long sum = 0;
long before = GC.GetAllocatedBytesForCurrentThread();
for (int i = 0; i < 1_000; i++)
{
    int[] window = prices[i..(i + 100)];
    sum += window[0];
}
long rangeBytes = GC.GetAllocatedBytesForCurrentThread() - before;

before = GC.GetAllocatedBytesForCurrentThread();
for (int i = 0; i < 1_000; i++)
{
    ReadOnlySpan<int> window = prices.AsSpan(i, 100);
    sum += window[0];
}
long spanBytes = GC.GetAllocatedBytesForCurrentThread() - before;
Console.WriteLine($"1,000 array ranges: {rangeBytes} bytes; 1,000 span slices: {spanBytes} bytes (sum {sum})");

// An Index and a Range are values you can store and pass around.
Index last = ^1;
Range inner = 1..^1;
Console.WriteLine($"fares[last] = {fares[last]}, fares[inner].Length = {fares[inner].Length}");
try
{
    Console.WriteLine(fares[^0]);
}
catch (IndexOutOfRangeException e)
{
    Console.WriteLine($"fares[^0]: {e.GetType().Name}, because ^0 is one past the last element");
}

Output

fares[^1] = 990, fares[^2] = 880, fares[1..4] = [520, 610, 700]
after fares[1] = 555    copy[0] = 520, view[0] = 555
after view[2] = 777     fares[3] = 777, copy[2] = 700
1,000 array ranges: 424000 bytes; 1,000 span slices: 0 bytes (sum 0)
fares[last] = 990, fares[inner].Length = 4
fares[^0]: IndexOutOfRangeException, because ^0 is one past the last element

Recorded with .NET SDK 10.0.401 (runtime 10.0.12) on macOS 26 arm64. To run it yourself: dotnet run slices.cs

Step through it and watch which slice follows the change:

Interactive · step by step A range makes a copy; a span slice is a view Step through slices.cs. Watch which arrays change when fares changes, and which do not.
A range makes a copy; a span slice is a view, step 6 of 6. Showing fares, int[6] (450 555 610 777 880 990), copy, int[3], a new array, view. In focus: int[3], a new array, view.elements 1 to 3faresint[6]450 555 610 777 880 990copyint[]int[3], a new array520 610 700viewSpan<int>

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. Six fares. fares refers to an array of 6 ints. fares[^1] is the last one, 990, and fares[^2] the one before it, 880.
  2. copy = fares[1..4]. A range on an array allocates a new array and copies the elements from index 1 up to, not including, index 4: 520, 610 and 700.
  3. view = fares.AsSpan()[1..4]. The same range on a span allocates nothing. view is a window onto elements 1 to 3 of fares itself, and reads them where they are.
  4. fares[1] = 555. Changing fares changes what view reads, because they share the elements: view[0] is now 555. copy[0] is still 520, because copy is a separate array.
  5. view[2] = 777. Writing through the span writes into fares: fares[3] is now 777, while copy[2] is still 700.
  6. What each slice costs. Every range on an array is a new array. The run took 1,000 slices of 100 ints: 424,000 bytes as ranges, 0 bytes as span slices.

copy[0] stayed 520 when fares[1] became 555, while view[0] read 555, and writing view[2] = 777 changed fares[3]. The cost follows from the same rule: 1,000 ranges of 100 ints allocated 424,000 bytes, each a new array of 24 + 400 bytes, and 1,000 span slices allocated nothing. Index and Range are types, so ^1 and 1..^1 can be stored in variables, and ^0 is one past the last element, which is why fares[^0] threw.

Taking ranges of an array inside a hot loop

prices[i..(i + 20)].Sum() for each of a million windows allocates a million small arrays. Slice a span instead, prices.AsSpan(i, 20), and loop over it: the values are read where they are, and nothing is copied.

Checkpoint

Check: Ranges copy, spans share

  1. Question 1 of 2 What does this print?

    Read the code, then choose one answer.

    int[] a = [1, 2, 3, 4];
    int[] part = a[1..3];
    Span<int> view = a.AsSpan(1, 2);
    a[1] = 9;
    Console.WriteLine($"{part[0]} {view[0]}");
    Show the answer: Question 1 of 2

    Answer: 2 9

    a[1..3] allocates a new int[2] holding 2 and 3. a.AsSpan(1, 2) allocates nothing and refers to a[1] and a[2], so it sees the store of 9.

  2. Question 2 of 2 a.Length is 10. How many elements does a[2..^1] hold?

    Type a number.

    A hint

    ^1 is the same index as a.Length - 1, and a range stops before its end.

    Show the answer: Question 2 of 2

    Answer: 7 elements

    ^1 is index 9, so the range runs from 2 up to, but not including, 9: 9 − 2 = 7 elements.

Arrays are covariant, so a store can fail at run time

A string[] of station codes can be assigned to a variable of type object[], because a string is an object. This rule is array covariance: The rule that lets an array of one reference type be used as an array of a base type, such as a string[] as an object[]. The array keeps its real element type, so storing a value of another type through it throws ArrayTypeMismatchException., and Microsoft’s guide to variance warns that it is not type safe: the array is still a string[], whatever the variable says. Watch what happens when the program stores a number through the object[]:

covariance.cs: a string[] seen as an object[] C# · covariance/covariance.cs
// Station codes in a string[], seen through a variable of type object[].
string[] stations = ["NDLS", "MAS", "HWH"];
object[] items = stations;   // compiles: array covariance
Console.WriteLine($"items refers to a {items.GetType().Name}");

items[0] = "SBC";            // a string: the store succeeds
Console.WriteLine($"stations[0] is now {stations[0]}");

try
{
    items[1] = 42;           // compiles, but the array can only hold strings
}
catch (ArrayTypeMismatchException e)
{
    Console.WriteLine($"items[1] = 42 -> {e.GetType().Name}; stations[1] is still {stations[1]}");
}

// A read-only interface is covariant as well, and it has no way to store anything.
IReadOnlyList<object> readOnly = stations;
Console.WriteLine($"IReadOnlyList<object>: Count {readOnly.Count}, [2] = {readOnly[2]}");

// object[] boxes = new int[3]; does not compile: arrays of value types are not covariant.
object[] boxes = [1, 2, 3];  // an object[] that holds three boxed ints
Console.WriteLine($"an object[] of {boxes[0].GetType().Name} values accepts a string: {Store(boxes, "four")}");

static bool Store(object[] array, object value)
{
    array[0] = value;
    return true;
}

Output

items refers to a String[]
stations[0] is now SBC
items[1] = 42 -> ArrayTypeMismatchException; stations[1] is still MAS
IReadOnlyList<object>: Count 3, [2] = HWH
an object[] of Int32 values accepts a string: True

Recorded with .NET SDK 10.0.401 (runtime 10.0.12) on macOS 26 arm64. To run it yourself: dotnet run covariance.cs

Storing "SBC" worked and changed stations[0], because it is the same array. Storing 42 compiled, then threw ArrayTypeMismatchException, and stations[1] stayed MAS. The language specification explains why: because of covariance, every store into an array of a reference type is checked at run time against the array’s real element type. The rule does not reach value types, so object[] boxes = new int[3]; does not compile, and the object[] of boxed ints at the end is an array of objects that accepts any value.

Code Compiles When it runs
object[] items = stations; yes items refers to the string[]
items[0] = "SBC"; yes stores the string
items[1] = 42; yes throws ArrayTypeMismatchException
IReadOnlyList<object> r = stations; yes reads only, nothing can fail
object[] boxes = new int[3]; no value-type arrays are not covariant

Ask for what the method needs

A method that only reads should take IReadOnlyList<T> or ReadOnlySpan<T> instead of object[]. Callers can still pass their arrays, and no store can fail inside it, because the type has no way to store.

Checkpoint

Check: Array covariance

  1. Question 1 of 2 object[] items = new string[2]; items[0] = 7; What happens?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: It compiles, and the store throws ArrayTypeMismatchException when it runs

    Because of array covariance, every store into an array of a reference type is checked at run time, and a boxed int is not a string.

  2. Question 2 of 2 Which line compiles?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: object[] a = new string[3];

    Array covariance exists only between arrays of reference types whose elements convert: string[] to object[], never int[] to object[].

Practise: rotate a seat map

rotate.cs checks a method that you write. Rotate turns a seat map 90 degrees clockwise and returns it as a jagged array: row r of the result is column r of the grid, read from its bottom row up. The checks include a square grid, a 2 × 3 grid, a single row and an empty grid. The method you start from only throws, so all 6 checks fail:

rotate.cs: six checks of the Rotate method C# · rotate/rotate.cs
// Practice: write Rotate so that every check below passes, then run this file again.
// Rotate turns a seat map 90 degrees clockwise and returns it as a jagged array:
// row r of the result is column r of the grid, read from its bottom row up.
(string Name, int[,] Grid, int[][] Expected)[] checks =
[
    ("1 x 1", new int[,] { { 7 } }, [[7]]),
    ("2 x 2", new int[,] { { 1, 2 }, { 3, 4 } }, [[3, 1], [4, 2]]),
    ("3 x 3", new int[,] { { 1, 2, 3 }, { 4, 5, 6 }, { 7, 8, 9 } }, [[7, 4, 1], [8, 5, 2], [9, 6, 3]]),
    ("2 x 3", new int[,] { { 1, 2, 3 }, { 4, 5, 6 } }, [[4, 1], [5, 2], [6, 3]]),
    ("1 x 4", new int[,] { { 1, 2, 3, 4 } }, [[1], [2], [3], [4]]),
    ("0 x 0", new int[0, 0], []),
];

int passed = 0;
foreach (var (name, grid, expected) in checks)
{
    string verdict;
    try
    {
        int[][] got = Rotate(grid);
        verdict = Same(got, expected) ? "ok" : $"FAIL, returned {Show(got)}";
    }
    catch (Exception e)
    {
        verdict = $"FAIL, {e.GetType().Name}";
    }
    if (verdict == "ok")
    {
        passed++;
    }
    Console.WriteLine($"Rotate({name}): {verdict}");
}
Console.WriteLine($"{passed} of {checks.Length} checks pass");
return passed == checks.Length ? 0 : 1;

static int[][] Rotate(int[,] grid)
{
    throw new NotImplementedException("Make GetLength(1) rows; fill row r from column r, bottom row first.");
}

static bool Same(int[][] a, int[][] b)
{
    if (a.Length != b.Length)
    {
        return false;
    }
    for (int r = 0; r < a.Length; r++)
    {
        if (!a[r].AsSpan().SequenceEqual(b[r]))
        {
            return false;
        }
    }
    return true;
}

static string Show(int[][] g) => "[" + string.Join(", ", g.Select(row => "[" + string.Join(", ", row) + "]")) + "]";

Output (exit status 1)

Rotate(1 x 1): FAIL, NotImplementedException
Rotate(2 x 2): FAIL, NotImplementedException
Rotate(3 x 3): FAIL, NotImplementedException
Rotate(2 x 3): FAIL, NotImplementedException
Rotate(1 x 4): FAIL, NotImplementedException
Rotate(0 x 0): FAIL, NotImplementedException
0 of 6 checks pass

Recorded with .NET SDK 10.0.401 (runtime 10.0.12) on macOS 26 arm64. To run it yourself: dotnet run rotate.cs

Write Rotate

Copy rotate.cs to your computer and replace the body of Rotate. Make grid.GetLength(1) rows of grid.GetLength(0) ints each, and fill row r from column r of the grid, starting at its bottom row. Run the file with dotnet run rotate.cs until it prints 6 of 6 checks pass.

Then check yourself on default values, the complement of a missed search, the two grids, the copy against the view and the covariant store. The session opens with the idea you have got wrong most often and explains every choice:

Practice that adapts to you

Practise this lesson

10 questions about 5 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 10 An array has Length 72. What is the index of its last element?

    Choose one answer.

    Show the answer: Question 1 of 10

    Answer: 71

    Valid indexes run from 0 to Length - 1. ^1 names the same last element without arithmetic.

  2. Question 2 of 10 A method adds 10,000 readings to an int[] by calling Array.Resize(ref data, data.Length + 1) before storing each one. What is wrong with it?

    Choose one answer.

    Show the answer: Question 2 of 10

    Answer: Each Resize copies every element so far, about 50 million copies in all; add to a List<int> and call ToArray() once

    The code is correct but quadratic: the copying grows with the square of the number of readings. A List<T> keeps spare room and doubles it when full, so its copying grows only in step with the count.

  3. Question 3 of 10 At most how many comparisons does BinarySearch make on a sorted array of 12 values?

    Type a number.

    A hint

    Each comparison that misses halves the positions still left.

    Show the answer: Question 3 of 10

    Answer: 4 comparisons

    12 positions, then at most 6, 3 and 1: ⌊log₂ 12⌋ + 1 = 4 comparisons. The search for 40 in the figure used all 4, comparing with 31, 52, 38 and 45.

  4. Question 4 of 10 You keep a sorted int[] and insert new values so that it stays sorted. Which index is right for a new value v?

    Choose one answer.

    Show the answer: Question 4 of 10

    Answer: r = Array.BinarySearch(a, v); then r if r >= 0, otherwise ~r

    The complement of a negative result is the insertion point. search.cs inserted 40 at ~(-8) = 7 and the array stayed sorted.

  5. Question 5 of 10 int[,] g = { { 1, 2 }, { 3, 4 }, { 5, 6 } }; In what order does foreach (int x in g) visit the values?

    Choose one answer.

    Show the answer: Question 5 of 10

    Answer: 1 2 3 4 5 6

    A rectangular array stores row 0 first, then row 1, and foreach visits the elements in that order. Use nested for loops with GetLength when you need another order.

  6. Question 6 of 10 A sleeper coach has bays of 8 berths and a last bay of 2 side berths, and the code often hands one bay to a method. Which layout fits?

    Choose one answer.

    Show the answer: Question 6 of 10

    Answer: int[][]: each bay is its own array of its own length, and bays[i] can be passed as an int[]

    Choose by the data: rows that always match fit int[,], rows that differ or travel on their own fit int[][].

  7. Question 7 of 10 What does fares[^0] do?

    Choose one answer.

    Show the answer: Question 7 of 10

    Answer: It throws IndexOutOfRangeException, because ^0 is one past the last element

    An index from the end counts back from Length. ^0 is useful as the end of a range, fares[2..^0] means up to the end, but not as an element.

  8. Question 8 of 10 A loop sums each 20-value window of a 1,000,000-value int[] with prices[i..(i + 20)].Sum(). What does the range cost, and what is the fix?

    Choose one answer.

    Show the answer: Question 8 of 10

    Answer: Every pass allocates a new 20-int array, about 100 MB in all; slice prices.AsSpan(i, 20) instead

    Ranges on arrays allocate copies; spans read the same memory. For a hot loop the span version does the same arithmetic without allocating.

  9. Question 9 of 10 What does this print?

    Read the code, then choose one answer.

    string[] codes = ["NDLS", "MAS"];
    object[] items = codes;
    items[1] = "HWH";
    Console.WriteLine(codes[1]);
    Show the answer: Question 9 of 10

    Answer: HWH

    Covariance gives a second, less specific view of the same array. Stores of the right type go through and are seen through both variables.

  10. Question 10 of 10 A method only reads the items of the array it is given, and callers pass a string[] or a Uri[]. Which parameter type is the safest choice?

    Choose one answer.

    Show the answer: Question 10 of 10

    Answer: IReadOnlyList<object>

    Covariant read-only interfaces such as IReadOnlyList<out T> and IEnumerable<out T> give the flexibility of array covariance without its run-time failures.

Interview questions

Fresher · What is the difference between int[,] and int[][] in C#?

int[,] is a rectangular, multidimensional array: one object that holds every element in one block, with the same length in every row, read as grid[row, column]. int[][] is a jagged array: an array of references to row arrays, read as grid[row][column], where each row is its own object, may have its own length and is null until you create it. Length counts every element of an int[,] (12 for int[3, 4]) but only the rows of an int[][], so use GetLength(0) and GetLength(1) for the dimensions of the rectangular one. A jagged array can hand one row to a method as an int[] and supports ranges; a multidimensional array supports neither. For a million ints, the block was one object of 4,000,040 bytes and the rows of rows 1,001 objects of 4,032,024 bytes.

Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.

  1. Fresher to mid

    A method computes moving averages of 20 prices over an int[] of 1,000,000 prices, using prices[i..(i + 20)] in a loop. A memory profile shows about 100 MB allocated per call. Why, and how do you remove the allocations without changing any result?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Fresher to mid

    A helper Fill(object[] slots, object value) replaces the null slots of an array with a placeholder. Its tests pass object[] arrays. In production it throws ArrayTypeMismatchException when called with a string[] of station codes and a placeholder of 0. Explain the failure and redesign the API.

    Model answer, Pro. First step and follow-ups

Key takeaways

  • An array’s length is fixed when it is made and every element starts at its default value; Array.Resize makes a new array and moves only the variable passed with ref.
  • Array.Sort sorts in place, and Array.BinarySearch is right only on sorted data, returning the complement of the insertion index when the value is missing.
  • int[,] is one block with equal rows and Length counting every element; int[][] is an array of row arrays of any length, each null until created.
  • ^1 is the last element and a..b stops before b; a range on an array is a copy, while a span slice is a view of the same elements that allocates nothing.
  • Array covariance lets a string[] pass as an object[], and a store of the wrong type through it throws ArrayTypeMismatchException at run time.

Terms in this lesson

array covariance
The rule that lets an array of one reference type be used as an array of a base type, such as a string[] as an object[]. The array keeps its real element type, so storing a value of another type through it throws ArrayTypeMismatchException.
binary search
A search of sorted values that compares the value it looks for with the middle element and drops the half that cannot hold it, so n values take at most ⌊log₂ n⌋ + 1 comparisons: 4 for 12 values, 20 for a million.
jagged array
An array whose elements are arrays, such as int[][], read as bays[row][berth]. Each row is an array of its own, may have its own length, and is null until it is created.
multidimensional array
An array with more than one index, such as int[3, 4], read as seats[row, seat]. It is one object with every row the same length; Length counts all its elements and GetLength(d) gives the length of dimension d.
span
A Span<T> or ReadOnlySpan<T>: a view of a run of values that sit next to each other in memory, such as part of an array, which reads and writes them where they are. array.AsSpan(1, 3) refers to three elements of array and copies nothing.

References

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.