Skip to main content
C# · lesson 9 of 26
C# beginner Lesson 9 of 26

Arrays in C#

C# arrays by example: fixed-size declaration, indices and ranges, rectangular vs jagged arrays, Array.Sort and BinarySearch, LINQ, and Span<T> slices.

By SkillByExample · Updated

Declaring and Initializing Arrays

An array has a fixed size set at creation, and every slot starts at the element type’s default value.

int[] numbers = new int[5];
string[] names = new string[3];

int[] primes = { 2, 3, 5, 7, 11 };
var scores = new[] { 95, 87, 76, 92 };

Console.WriteLine(string.Join(", ", numbers));
Console.WriteLine(names[0] is null);
Console.WriteLine(scores.GetType());

Console.WriteLine(primes[0]);
Console.WriteLine(primes[^1]);
primes[2] = 99;
Console.WriteLine(string.Join(", ", primes));
Console.WriteLine(primes.Length);

try
{
    Console.WriteLine(primes[5]);
}
catch (IndexOutOfRangeException ex)
{
    Console.WriteLine(ex.Message);
}
$ dotnet run
0, 0, 0, 0, 0
True
System.Int32[]
2
11
2, 3, 99, 7, 11
5
Index was outside the bounds of the array.

new int[5] is five zeros and new string[3] is three nulls — the array exists, but the strings inside it do not. new[] { ... } infers Int32[] from the elements. ^1 means “one from the end”, so it is the last element without writing primes.Length - 1. Arrays are mutable (primes[2] = 99 sticks) but not resizable, and reading past the end throws rather than returning garbage.

Iterating and Slicing

foreach is cleanest when you only need values; for gives you the index. A range like [1..4] produces a new array.

int[] numbers = { 10, 20, 30, 40, 50 };

foreach (int n in numbers)
    Console.Write(n + " ");
Console.WriteLine();

for (int i = 0; i < numbers.Length; i += 2)
    Console.WriteLine($"[{i}] = {numbers[i]}");

int[] last3 = numbers[2..];
int[] middle = numbers[1..4];
Console.WriteLine(string.Join(", ", last3));
Console.WriteLine(string.Join(", ", middle));

middle[0] = -1;
Console.WriteLine(numbers[1]);
$ dotnet run
10 20 30 40 50 
[0] = 10
[2] = 30
[4] = 50
30, 40, 50
20, 30, 40
20

[1..4] includes index 1 and stops before index 4, so it holds three elements. The last line is the one people get wrong: writing -1 into middle left numbers[1] at 20, because slicing an array with a range copies. If you want a window that shares storage, use Span<T> (below).

Rectangular vs Jagged Arrays

A rectangular array int[,] is one block with every row the same length; a jagged array int[][] is an array of independent arrays.

int[,] matrix = {
    { 1, 2, 3 },
    { 4, 5, 6 },
};

Console.WriteLine(matrix[1, 2]);
Console.WriteLine($"{matrix.GetLength(0)} rows x {matrix.GetLength(1)} cols, Length = {matrix.Length}");

for (int row = 0; row < matrix.GetLength(0); row++)
{
    for (int col = 0; col < matrix.GetLength(1); col++)
        Console.Write($"{matrix[row, col]} ");
    Console.WriteLine();
}

int[][] jagged = new int[3][];
jagged[0] = new[] { 1, 2 };
jagged[1] = new[] { 3, 4, 5 };
jagged[2] = new[] { 6 };

foreach (int[] row in jagged)
    Console.WriteLine($"{row.Length}: {string.Join(" ", row)}");
Console.WriteLine(jagged.Length);
$ dotnet run
6
2 rows x 3 cols, Length = 6
1 2 3 
4 5 6 
2: 1 2
3: 3 4 5
1: 6
3

For the rectangular array, Length is the total element count (6), not the number of rows — use GetLength(0) and GetLength(1) for the dimensions. For the jagged array, Length is 3 because it is an array of three references; each row carries its own length.

Array Methods

The static Array class sorts, searches and fills arrays in place.

int[] numbers = { 5, 3, 8, 1, 9, 2, 7, 4, 6 };

Array.Sort(numbers);
Console.WriteLine(string.Join(", ", numbers));

Array.Reverse(numbers);
Console.WriteLine(string.Join(", ", numbers));
Console.WriteLine(Array.IndexOf(numbers, 7));

Array.Sort(numbers);
Console.WriteLine(Array.BinarySearch(numbers, 5));
int missing = Array.BinarySearch(numbers, 42);
Console.WriteLine($"{missing} -> insert at {~missing}");

int[] filled = new int[4];
Array.Fill(filled, 42);
Console.WriteLine(string.Join(", ", filled));

Array.Clear(numbers, 0, 3);
Console.WriteLine(string.Join(", ", numbers));
$ dotnet run
1, 2, 3, 4, 5, 6, 7, 8, 9
9, 8, 7, 6, 5, 4, 3, 2, 1
2
4
-10 -> insert at 9
42, 42, 42, 42
0, 0, 0, 4, 5, 6, 7, 8, 9

Every call here mutates the array it is given; none of them returns a new one. BinarySearch for a missing value does not return -1 — it returns the bitwise complement of the insertion point, here -10, and ~ turns that back into 9 (the end of the array). Array.Clear resets elements to the default value, so the first three slots become 0.

LINQ with Arrays

Every array is an IEnumerable<T>, so LINQ works on it — and unlike Array.Sort, LINQ never touches the original.

int[] numbers = { 5, 3, 8, 1, 9, 2, 7, 4, 6 };

int[] evens = numbers.Where(n => n % 2 == 0).ToArray();
Console.WriteLine(string.Join(", ", evens));

int[] top3 = numbers
    .Where(n => n > 3)
    .OrderByDescending(n => n)
    .Take(3)
    .ToArray();
Console.WriteLine(string.Join(", ", top3));

Console.WriteLine($"sum={numbers.Sum()} avg={numbers.Average()} max={numbers.Max()}");
Console.WriteLine(numbers.Any(n => n > 8));
Console.WriteLine(numbers.First(n => n % 2 == 0));
Console.WriteLine(numbers.FirstOrDefault(n => n > 100));

foreach (var group in numbers.GroupBy(n => n % 2 == 0 ? "even" : "odd"))
    Console.WriteLine($"{group.Key}: {string.Join(", ", group)}");

Console.WriteLine(string.Join(", ", numbers));
$ dotnet run
8, 2, 4, 6
9, 8, 7
sum=45 avg=5 max=9
True
8
0
odd: 5, 3, 1, 9, 7
even: 8, 2, 4, 6
5, 3, 8, 1, 9, 2, 7, 4, 6

FirstOrDefault on an int[] with no match returns 0, not null — default(int) is 0, which is ambiguous if 0 is a real value in your data. GroupBy emits groups in the order their keys first appear (5 is odd, so odd comes first). The last line proves the source array is still in its original order.

Span<T> for Slicing Without Copying

AsSpan gives a view onto the array’s own memory, so writes through the span land in the array.

int[] data = { 1, 2, 3, 4, 5, 6, 7, 8 };

Span<int> slice = data.AsSpan(2, 4);
Console.WriteLine($"{slice.Length} items, first = {slice[0]}");

slice[0] = 99;
Console.WriteLine(data[2]);

Console.WriteLine(Sum(data.AsSpan(0, 4)));

Span<int> buffer = stackalloc int[5];
for (int i = 0; i < buffer.Length; i++)
    buffer[i] = i * i;
Console.WriteLine(string.Join(", ", buffer.ToArray()));

static int Sum(ReadOnlySpan<int> segment)
{
    int total = 0;
    foreach (int n in segment) total += n;
    return total;
}
$ dotnet run
4 items, first = 3
99
106
0, 1, 4, 9, 16

Compare this with the range example earlier: there, writing to the slice left the original alone; here data[2] became 99. Sum receives the first four elements without a new array being allocated, and it reports 106 (1 + 2 + 99 + 4) because it reads the same memory the span just wrote to. stackalloc puts the buffer on the stack, so there is nothing for the garbage collector to clean up — ToArray() is only there to print it.

Common Array Patterns

Building, flattening, converting and comparing arrays each have a one-line idiom.

int[] squares = Enumerable.Range(1, 6).Select(n => n * n).ToArray();
Console.WriteLine(string.Join(", ", squares));

int[][] jagged = { new[] { 1, 2 }, new[] { 3, 4, 5 }, new[] { 6 } };
int[] flat = jagged.SelectMany(row => row).ToArray();
Console.WriteLine(string.Join(", ", flat));

List<int> list = flat.ToList();
list.Add(7);
Console.WriteLine($"{flat.Length} vs {list.Count}");

int[] a = { 1, 2, 3 };
int[] b = { 1, 2, 3 };
Console.WriteLine(a == b);
Console.WriteLine(a.Equals(b));
Console.WriteLine(a.SequenceEqual(b));
$ dotnet run
1, 4, 9, 16, 25, 36
1, 2, 3, 4, 5, 6
6 vs 7
False
False
True

ToList() copies, so adding to list leaves flat at six elements. The comparison is the trap: == and Equals on arrays compare references, and a and b are two different objects, so both print False even though the contents match. SequenceEqual compares element by element.

Practice

  1. Descending sort, two ways. Sort { 5, 3, 8, 1 } into descending order once in place with Array methods and once with LINQ. Which one changes the original?
Solution
int[] a = { 5, 3, 8, 1 };
Array.Sort(a);
Array.Reverse(a);
Console.WriteLine(string.Join(", ", a));

int[] b = { 5, 3, 8, 1 };
int[] sorted = b.OrderByDescending(x => x).ToArray();
Console.WriteLine(string.Join(", ", sorted));
Console.WriteLine(string.Join(", ", b));
$ dotnet run
8, 5, 3, 1
8, 5, 3, 1
5, 3, 8, 1

Array.Sort + Array.Reverse mutate a. OrderByDescending returns a new array and b keeps its order.

  1. Slice vs span. Change the “Iterating and Slicing” example so that writing -1 into the middle segment does change numbers[1].
Solution
int[] numbers = { 10, 20, 30, 40, 50 };
Span<int> middle = numbers.AsSpan()[1..4];
middle[0] = -1;
Console.WriteLine(numbers[1]);
$ dotnet run
-1

A range on a Span<int> slices the view instead of copying the array.

  1. Row sums. For the jagged array { {1, 2}, {3, 4, 5}, {6} }, print each row’s sum and the index of the longest row.
Solution
int[][] jagged = { new[] { 1, 2 }, new[] { 3, 4, 5 }, new[] { 6 } };

for (int i = 0; i < jagged.Length; i++)
    Console.WriteLine($"row {i}: {jagged[i].Sum()}");

int longest = Array.IndexOf(jagged, jagged.MaxBy(r => r.Length));
Console.WriteLine($"longest row: {longest}");
$ dotnet run
row 0: 3
row 1: 12
row 2: 6
longest row: 1

MaxBy returns the row itself, and Array.IndexOf finds that same reference in the outer array.

  1. Insert keeping order. Given a sorted int[] { 1, 4, 9, 16 }, use Array.BinarySearch to find where 10 belongs and build a new array with it inserted.
Solution
int[] sorted = { 1, 4, 9, 16 };
int idx = Array.BinarySearch(sorted, 10);
int at = idx >= 0 ? idx : ~idx;

int[] result = [.. sorted[..at], 10, .. sorted[at..]];
Console.WriteLine($"{idx} -> {at}");
Console.WriteLine(string.Join(", ", result));
$ dotnet run
-4 -> 3
1, 4, 9, 10, 16

The collection expression [.. a, x, .. b] (C# 12) splices the two halves around the new value.

Frequently Asked Questions

What is the difference between an array and a List<T>?
Arrays have a fixed size set at creation. List<T> can grow dynamically. Use arrays when the size is known and fixed; use List<T> when you need to add or remove elements. Arrays are slightly faster for indexed access; List<T> is more flexible.
How do I sort an array in descending order?
Call Array.Sort(arr) then Array.Reverse(arr), or use LINQ: arr.OrderByDescending(x => x).ToArray(). Array.Sort and Array.Reverse change the array in place; the LINQ version leaves the original alone and returns a new array.
What is Span<T> and when should I use it?
Span<T> is a stack-only view into a contiguous block of memory — an array, a stack buffer, or unmanaged memory. Use it in performance-critical code to slice and pass array segments without copying. It cannot be stored in a field of a class or captured by a lambda.
Does slicing a C# array with a range copy it?
Yes. numbers[1..4] on an array allocates a new array holding those elements, so writing to the slice does not change the original. If you want a view that shares storage instead, call numbers.AsSpan(1, 3) or numbers.AsSpan()[1..4].
What does Array.BinarySearch return when the value is missing?
A negative number: the bitwise complement of the index where the value would be inserted to keep the array sorted. Apply ~ to the result to get that insertion point. The array must already be sorted, or the result is meaningless.