AlgoMaster Logo

Arrays Basics

Last Updated: May 22, 2026

High Priority
9 min read

An array is a fixed-size, ordered collection of values that live next to each other in memory. When a cart needs to remember three product names, when a price report needs to track twelve months of revenue, or when a UI needs to render a list of star ratings, an array is the simplest container that does the job. This chapter walks through what an array is, how to declare and initialize one, how to read items by index, and the rules that catch people on day one.

What an Array Is

An array holds a fixed number of values of the same type, stored side by side in one block of memory. Every value sits at a numbered position called an index. The first slot is index 0, the second is 1, and so on, up to one less than the array's length.

A small concrete example. A cart of three product names:

Three slots, three indices, three reads. The square brackets after a name select one slot.

Four properties of C# arrays are worth pinning down up front, because everything else in this chapter follows from them:

  • Fixed size. Once an array is created, its length doesn't change. If you need to grow or shrink it, you build a new one or switch to a List<T>.
  • Indexable. You read or write a slot by its position. Lookup by index is O(1).
  • Contiguous. All slots live in one block of memory. Walking the array in order is cache-friendly and fast.
  • Reference type. The array itself lives on the heap. A variable of array type holds a reference to that heap block, not the data inline.

A diagram of the layout helps. Three string slots, one reference variable pointing at them:

The variable cart lives on the stack (when it's a local) and stores the address of the array on the heap. The three slots are laid out one after another. Reading cart[1] jumps directly to the second slot, no scanning required.

Why does this exist when C# already has List<T>? Arrays are the foundation. List<T> is built on top of an array internally. Arrays trade flexibility (no add or remove) for speed and a tighter memory layout. When the size is known up front and won't change, use an array. When the size grows or shrinks, use List<T>.

Declaring an Array

A declaration tells the compiler "this name will hold an array of T." It doesn't create the array yet, just the variable that can point to one.

The type goes first, followed by [], then the variable name. Read int[] as "array of int." Read string[] as "array of string." The brackets are part of the type, not the name.

A declaration on its own leaves the variable holding null, since arrays are reference types and an uninitialized reference is null. Trying to read or write through a null reference throws NullReferenceException:

For a local variable, the compiler stops you before the program runs. The error is CS0165: Use of unassigned local variable 'scores'. The fix is to assign an array to scores before reading from it. That's what the next section is about.

Declaring int[] scores; doesn't allocate anything. No memory is reserved until an actual array is assigned. The allocation happens with new int[...] or one of the initializer forms below.

Creating and Initializing an Array

There are several ways to create an array in C#. Each one ends up with the same kind of object on the heap; they differ in how much typing you have to do and where the values come from.

Empty Array of a Given Size

The most explicit form uses new with a size. Every slot gets the type's default value (more on defaults in a moment):

new int[5] allocates an array of five int slots on the heap. Every slot starts at 0 because 0 is the default for int. You can write to the slots one at a time:

Array with Values, Long Form

If you already know the values up front, you can supply them inline. The long form spells everything out:

new int[] says "make an int array," and the braced list provides the values. The compiler counts them and sizes the array to match. Five values, length five.

Array with Values, Short Form

When the variable's declared type already says int[], you can drop new int[] and just write the braced list:

This form is the most common in everyday code. It reads concisely and the compiler handles the type and size for you. The catch: it only works in a declaration. You can't write ratings = { 5, 4, 3 }; later as a reassignment, because the short form is parsed as a declaration initializer, not an expression. Use one of the other forms for that.

Target-Typed new[]

When you want var but still need an array, the target-typed new[] form (from C# 3) lets the compiler infer the element type from the values:

The braces hold decimal literals, so the compiler picks decimal[]. All elements must agree on a common type, or the compiler errors out. This form is useful when the right-hand side is the source of truth for the type.

Collection Expressions (C# 12+)

C# 12 added collection expressions, which let you use square brackets in place of braces:

These work in C# 12 (.NET 8) and later. Collection expressions also work for List<T>, Span<T>, and other collection types, but for arrays specifically, this is just a shorter syntax for the same result. If your project targets an older language version, stick with { ... }.

A summary table of the five forms:

FormExampleWhen to use
Sized, default-fillednew int[5]You know the size but not the values yet
Long, with valuesnew int[] { 1, 2, 3 }Reassignment or method arguments
Short, with valuesint[] x = { 1, 2, 3 }Declaration with known values
Target-typed new[]var x = new[] { 1, 2, 3 }Using var with an array literal
Collection expressionint[] x = [1, 2, 3]C# 12+, shortest form

Indexing and Length

The way you reach into an array is with square brackets and an integer index. Indices start at 0.

cart[0] is the first element, not cart[1]. The last valid index is cart.Length - 1, not cart.Length. And Length is a property, not a method, so there are no parentheses.

You can both read and write through an index:

Arrays support assignment in place. The slot at index 2 used to hold 3 and now holds 20. The array's length doesn't change.

Indexing an array is O(1). The runtime computes base_address + index * element_size and reads from that address. The size of the array doesn't matter; reading slot 999_999 of a million-element array is the same speed as reading slot 0.

The Length Property

Every C# array exposes a Length property that returns the number of slots, as an int. It's a field read, not a count, so it's O(1) regardless of the array's size:

The compiler also tracks the length and uses it for bounds checks. Every indexed read or write checks 0 <= index < Length before it runs.

Index from End: ^1

C# 8 added the ^ (hat) operator for indexing from the end. ^1 is the last element, ^2 is the second-to-last, and so on:

The math is: cart[^n] means cart[cart.Length - n]. So ^1 translates to Length - 1, the last valid index. ^0 would be Length, which is one past the end and would throw at runtime, so the from-end indices start at ^1, not ^0.

The from-end syntax is mostly a readability tool. cart[^1] reads more clearly than cart[cart.Length - 1] when you just want the last item.

Range Slicing: ..

C# 8 also added range syntax with ... Applied to an array, it returns a new array containing a copy of the chosen slice:

The bounds work the way the rest of C# treats half-open ranges: the start is inclusive, the end is exclusive. cart[..3] includes indices 0, 1, and 2. cart[1..4] includes 1, 2, and 3.

Range slicing an array (cart[1..4]) allocates a new array and copies the chosen elements into it. The slice is independent of the original; modifying it doesn't change cart. For a small slice this is fine. In a hot loop or for a large array, it's wasted memory and CPU. Span<T> provides a view into the same memory without copying.

Default Values

When you create an array with new T[size] and don't supply values, every slot is filled with the default value for T. The defaults are not arbitrary; they follow the same rules as default values for fields and value types in general.

TypeDefault value
int, long, short, byte0
double, float, decimal0 (or 0m for decimal)
boolfalse
char'\0' (the null character)
Any reference type (string, classes, arrays)null
Any custom structAll fields set to their defaults

A demonstration:

For a reference-typed array like string[], every slot starts as null. That's important to remember: declaring string[] names = new string[3] does not give you three empty strings; it gives you three null references. Reading any of those slots and calling a method on it (names[0].Length) would throw NullReferenceException. You have to assign actual strings to each slot, or use the initializer form new string[] { "", "", "" } if you want empty strings.

new int[1_000_000] is fast even for a million slots. The runtime asks the GC for a single contiguous block and zeroes the memory in one operation. There's no per-slot loop; the OS-level zeroing is highly optimized.

Arrays Are Reference Types

This is the most important property to understand, because it changes how arrays behave when you pass them around. The array variable holds a reference to the heap block, not the data itself. Assignment copies the reference. Passing an array to a method passes a reference. Two variables can point to the same array.

A quick demonstration:

Both names point to the same array on the heap, so a write through one name is visible through the other. There is exactly one array; there are two references to it.

A diagram makes this clearer:

Two arrows, one heap object. This is what "reference type" means in practice.

The same thing happens when you pass an array to a method. The method receives a copy of the reference, but both the caller and the method see the same array, so writes inside the method are visible outside:

The method modified cart directly, and the caller sees the changes. This is pass-by-reference semantics for the array's contents. It's why methods that take arrays and "fill them in" (like sorting helpers) don't need to return anything.

If you want a method that doesn't risk modifying the caller's array, copy it first. The simplest copy is a range slice (cart[..]) or Array.Clone().

A Worked Example

A small program that pulls several ideas together. A cart with five product names and matching prices, a quick look at the first and last items, a slice for a promo banner, and a discount applied through a helper.

Look at what the example exercises. Two arrays declared with the short form. Length for the count. Index 0 and ^1 for first and last. A range ..3 to get the first three names (note that this allocated a new array). And a helper method that mutates the array in place because arrays are reference types.

What this lesson doesn't show: iterating through the array with a for or foreach loop, sorting it, searching it, or growing it. Those are covered in later lessons of this section.