📁 Sangam Dhakal - Portfolio v2.0 - Netscape Navigator 4.0
_
□
×

📚 Interview question

How do single-dimensional arrays work in C#? How do you declare, initialise, copy, resize, slice and search them, and when would you use an array instead of List<T>?

Single-dimensional arrays in C# 14: declaration and collection expressions, fixed length and zero-initialisation, the Array helper methods, copying versus slicing, ranges that copy, and choosing between arrays and List<T>.

Junior Theory Beginner arrays basics collections csharp single-dimensional-arrays span

🎯 Key points

  • An array is a fixed-length, zero-based, contiguous block of elements of one type; T[] is a reference type derived from System.Array
  • new T[n] zero-initialises every element (0, false, null); collection expressions int[] a = [1, 2, 3] work since C# 12
  • Length is fixed: Array.Resize allocates a new array and copies, it does not grow in place
  • Clone() and CopyTo make shallow copies; reference-type elements are shared
  • Range indexing on arrays (arr[1..3]) allocates a new array; AsSpan(1..3) is a zero-copy view
  • Array.Sort is an unstable introsort; Array.BinarySearch requires sorted data; Array.IndexOf is linear
  • Return Array.Empty<T>() or [] instead of null for 'no items'
  • Use arrays for fixed-size data, interop, buffers and hot loops; use List<T> when the size changes

⏱ 30-second answer

An array is a fixed-length, zero-based, contiguous block of elements of one type. T[] is a reference type, and new int[5] gives five zeros. I create them with new T[n], initialisers or collection expressions like int[] ids = [1, 2, 3]. The length never changes: Array.Resize actually allocates a new array and copies. Clone is a shallow copy, arr[1..3] copies a range while arr.AsSpan(1..3) is a free view, and Array.Sort/BinarySearch are the built-in algorithms. I use arrays for fixed-size data, buffers and hot loops, and List<T> when the number of items changes.

🗣 Full answer (senior)

How I would answer this in an interview

A single-dimensional array (T[]) is a fixed-length, contiguous block of elements in one heap object, indexed from zero, with bounds checking on every access. It is the most fundamental collection in .NET — List<T>, Dictionary, string and Span<T> are all built on arrays. I cover the everyday operations and then when an array is a better choice than List<T>.

1. Declaring and initialising

int[] a = new int[5];                 // five zeros (default values)
int[] b = new int[] { 1, 2, 3 };
int[] c = { 1, 2, 3 };                 // array initialiser
int[] d = [1, 2, 3];                   // C# 12 collection expression
var e = new[] { 1.5, 2.5 };            // inferred double[]
string[] names = new string[3];        // three nulls
int[] empty = [];                      // uses a cached empty array
  • The length is fixed at creation: a.Length never changes.
  • Elements start at default(T).
  • Array.Empty<T>() (or []) returns a cached zero-length array, so returning empty arrays allocates nothing.
  • new T[n] with a huge n allocates on the Large Object Heap once it exceeds ~85,000 bytes.

2. Accessing and slicing

int first = d[0];
int last  = d[^1];          // index from end (C# 8)
int[] mid = d[1..];         // RANGE ON AN ARRAY ALLOCATES A COPY
Span<int> view = d.AsSpan(1); // a view - no copy

Ranges on arrays call RuntimeHelpers.GetSubArray, which copies. Ranges on spans are free views. This distinction matters in hot paths.

3. Copying

Operation Behaviour
int[] copy = a; Copies the reference — same array
a.Clone() Shallow copy, returns object (needs a cast)
Array.Copy(src, srcIndex, dest, destIndex, length) Copies a range; handles overlapping ranges correctly
a.CopyTo(dest, index) Copies all elements
a.AsSpan().CopyTo(dest) Fastest, vectorised (memmove)
a.ToArray() (LINQ) / [.. a] New array

All copies are shallow: for reference-type elements, both arrays point to the same objects.

4. "Resizing"

Arrays can't grow. Array.Resize(ref a, newSize) allocates a new array, copies the elements and reassigns your variable — other references still point to the old array. If you're resizing repeatedly, you want a List<T> (which does exactly this with geometric growth).

5. Searching and sorting

Need API Cost
Linear search Array.IndexOf(a, x), a.AsSpan().IndexOf(x), Array.Exists, Array.Find O(n), vectorised for primitives
Sort Array.Sort(a), Array.Sort(a, comparer), a.AsSpan().Sort() O(n log n) introsort, unstable
Binary search Array.BinarySearch(a, x) O(log n), array must be sorted with the same comparer
Reverse Array.Reverse(a) / span.Reverse() O(n)
Fill Array.Fill(a, value) / span.Fill(value) O(n), vectorised
Clear Array.Clear(a) / span.Clear() O(n)

Array.BinarySearch returns the bitwise complement of the insertion point when not found (~index), which is useful for maintaining a sorted array.

6. When I choose an array instead of List<T>

  • The size is known and fixed: lookup tables, buffers, matrix data, results of a known count (new Result[items.Count] filled by index).
  • Performance-critical code: arrays have no extra indirection, no version checks, best JIT optimisation (bounds-check elimination, vectorisation), and are directly usable as Span<T>.
  • Interop and I/O: byte[] buffers for streams, sockets, cryptography, ArrayPool<T> rentals.
  • Immutable snapshots: return a fresh array (or ImmutableArray<T>, which wraps one) when callers should not change your state.
  • Small memory footprint: no spare capacity, one object instead of two.

I choose List<T> when items are added or removed, or the count is unknown until the end. For public APIs I usually return IReadOnlyList<T> or ImmutableArray<T> and accept IEnumerable<T>/IReadOnlyList<T>/ReadOnlySpan<T>, rather than exposing mutable arrays.

7. A production example

A pricing engine looked up tax rates and discounts per product category using a Dictionary<int, decimal> built per request. The categories were a dense range 0..255. Replacing it with a static readonly decimal[] RatesByCategory indexed by category ID (loaded once at start-up, exposed as ReadOnlySpan<decimal>) made the lookup a single bounds-checked array access, removed per-request dictionary allocation, and was easy to reason about. The trade-off — a fixed, dense key range — was acceptable because categories were controlled internally.

🔍 What the interviewer is checking

A fundamentals check that also tests awareness of copying, mutability and API choice.

Green flags

  • Knows arrays are fixed-size reference types with zeroed elements.
  • Explains that Resize reallocates and Clone is shallow.
  • Knows ranges copy but spans do not.
  • Gives sensible array versus List<T> guidance and avoids exposing mutable arrays.

Red flags

  • Thinks arrays can grow.
  • Returns null instead of empty arrays.
  • Exposes public static int[] lookup tables.

📚 Learn in depth

## 1. Array object layout

```text
[ header 8B ][ MethodTable* 8B ][ Length 8B (4B length + padding) ][ element 0 ][ element 1 ] ...
```

- An `int[1000]` is one object of ~4,024 bytes: elements are stored **inline and contiguously**.
- A `string[1000]` stores 1,000 **references** (8 bytes each); the strings themselves are separate objects.
- The `MethodTable` describes the element type, which the runtime uses for type checks (array covariance) and the GC uses to know whether elements contain references.
- Arrays of value types without references (`int[]`, `double[]`) are not scanned by the GC for pointers — cheaper to collect.

## 2. Indexing and bounds checks

`a[i]` compiles to the IL `ldelem` / `stelem`, which the JIT turns into:

```text
cmp i, [array + 8] ; compare with Length
jae ThrowIndexOutOfRange
mov eax, [array + 16 + i*4]
```

The unsigned comparison (`jae`) catches both negative and too-large indexes in one test. The JIT **removes** this check when it can prove `i` is in range — most notably in `for (int i = 0; i &lt; a.Length; i++)` loops, and in `foreach` over arrays.

## 3. Zero-length arrays and `Array.Empty

💻 Code

using System.Collections.Immutable;

namespace Arrays;

public sealed record Person(string Name, int Age);

public static class SingleDimensional
{
    // Do not expose mutable arrays publicly
    public static ImmutableArray<decimal> GstBrackets { get; } = [0m, 0.10m];

    public static void Main()
    {
        int[] scores = [72, 95, 58, 88];
        Console.WriteLine($"Length {scores.Length}, last {scores[^1]}");

        // Sort and search
        Array.Sort(scores);                              // [58, 72, 88, 95]
        int found = Array.BinarySearch(scores, 88);      // 2
        int missing = Array.BinarySearch(scores, 80);    // negative: ~insertion point
        Console.WriteLine($"{found} {~missing}");        // 2 2

        // Copy vs view
        int[] copy = scores[1..3];                       // new array [72, 88]
        ReadOnlySpan<int> view = scores.AsSpan(1..3);    // no allocation
        Console.WriteLine($"{copy.Length} {view[0]}");

        // Resize allocates a new array
        Array.Resize(ref scores, 6);                     // [58, 72, 88, 95, 0, 0]

        // Shallow copy shares reference-type elements
        Person[] people = [new("Ada", 36)];
        var clone = (Person[])people.Clone();
        Console.WriteLine(ReferenceEquals(people[0], clone[0]));  // True

        // Stable sort when order of equal keys matters
        Person[] staff = [new("Bo", 30), new("Al", 30), new("Cy", 25)];
        var byAge = staff.OrderBy(p => p.Age).ToArray(); // Cy, Bo, Al (stable)
        Console.WriteLine(string.Join(",", byAge.Select(p => p.Name)));

        Console.WriteLine(Find([], 5).Length);           // 0: return empty, not null
    }

    private static int[] Find(int[] source, int min) =>
        source.Length == 0 ? [] : Array.FindAll(source, x => x >= min);
}

⚠ Common mistakes

1. Assuming assignment copies the array

What happens: int[] backup = data; then modifying data. Why it is wrong: both variables reference the same array. Do instead: copy explicitly (data.ToArray(), [.. data], Array.Copy).

2. Forgetting copies are shallow

What happens: cloning an array of objects and modifying an element's properties in the copy. Why it is wrong: both arrays share the same element objects. Do instead: deep-copy elements explicitly, or use immutable element types (records).

3. Repeated Array.Resize in loops

What happens: appending items by resizing by one each time. Why it is wrong: O(n²) copying and many garbage arrays. Do instead: use List<T> (geometric growth) or pre-size when the count is known.

4. Using ranges on arrays in hot paths

What happens: var tail = buffer[1..]; inside loops. Why it is wrong: array ranges allocate a copy each time. Do instead: use buffer.AsSpan(1..) for views.

5. Binary searching an unsorted array (or with a different comparer)

What happens: Array.BinarySearch on data sorted with a different comparer or not sorted at all. Why it is wrong: results are undefined — often wrong without any exception. Do instead: sort with the same comparer used for searching, or use HashSet<T> for membership.

6. Exposing internal arrays from public APIs

What happens: public string[] AllowedRoles => _roles;. Why it is wrong: callers can modify elements and change your internal state (even with readonly on the field). Do instead: return IReadOnlyList<T>, ImmutableArray<T>, ReadOnlySpan<T>, or a copy.

7. Allocating new T[0]

What happens: empty results are returned as new Order[0]. Why it is wrong: allocates a new object every time. Do instead: return Array.Empty<T>() or [].

8. Off-by-one and ^0

What happens: a[a.Length] or a[^0] to get the last element. Why it is wrong: both are out of range; the last element is a[^1]. Do instead: use a[^1] and loop with < Length.

9. Expecting Array.Sort to be stable

What happens: sorting already-ordered data by a secondary key and expecting the previous order to be preserved for ties. Why it is wrong: introsort is unstable. Do instead: use LINQ OrderBy(...).ThenBy(...) or sort by a composite key.

10. Using arrays where the collection changes size

What happens: fixed arrays with a separate count variable managed by hand. Why it is wrong: reinvents List<T> with more bugs. Do instead: use List<T>, or ArrayPool<T> with clear ownership when you truly need manual buffers.

🏛 Senior insight: trade-offs

Arrays are the building block under most collections; seniors use them deliberately and keep them encapsulated.

Use arrays when Prefer something else when
size is known and fixed items are added and removed (List<T>)
you need the fastest indexing and iteration you need set or key lookups (HashSet, Dictionary)
interop and buffers (byte[], char[]) data is shared and must not change (ImmutableArray<T>, FrozenSet<T>)
sorting large data in place you need stable ordering (OrderBy)
  • ImmutableArray<T> is a thin wrapper over an array with no extra indirection, ideal for exposing fixed data safely.
  • Collection expressions let the compiler choose efficient construction (for example avoiding intermediate lists), so prefer them in new code.
  • Watch for arrays of 85,000 bytes or more (for example double[10_625]) landing on the LOH in frequently allocated code; pool or reuse them.

🔗 Follow-ups the interviewer may ask

  1. When: Candidate mentions arrays are contiguous and fixed size Why: Checks they can compare arrays with List<T> growth and cost
    How to answer

    An array is a fixed-size block of contiguous elements of one type. Element i lives at start + i × elementSize, so indexing is O(1) and iteration is very cache-friendly. Its size cannot change.

    List<T> wraps an internal array plus a Count. When the array is full, Add allocates a new array about twice as large and copies the elements, so Add is amortised O(1) but occasionally O(n). Inserting or removing in the middle is O(n) because elements shift.

    Use an array for fixed-size data and hot paths, List<T> when the size changes, set Capacity when you know the size, and Span<T>/Memory<T> to work on slices without copying.

  2. When: Candidate mentions int[,] or grids Why: Checks knowledge of rectangular versus jagged arrays
    How to answer

    A rectangular array int[,] is one object with every element stored contiguously in row-major order, and every row is the same length. A jagged array int[][] is an array of separate row arrays, so rows can have different lengths but each access goes through an extra reference. Rectangular arrays use helper-style access in IL and were historically slower, while each jagged row is a fast single-dimensional array that works with spans. The modern JIT has narrowed the gap, so I measure. For performance-critical grids I often flatten into one T[] with index = row * width + col, which gives contiguous memory, span and SIMD support. I always iterate row by row for cache locality.

  3. When: Candidate mentions allocation cost of new arrays Why: Probes ArrayPool, LOH and performance judgement
    How to answer

    First I measure: dotnet-counters for allocation rate and LOH size, then an allocation profiler to see which arrays dominate. Short-lived large arrays are the worst case, because anything of 85,000 bytes or more lands on the Large Object Heap, which is only collected in gen 2. The fixes are to reuse buffers with ArrayPool<T>.Shared (always return them, use only the requested length, clear sensitive data), use spans instead of copying slices, and stream data instead of building giant arrays. For speed I keep data in contiguous arrays of structs for cache locality and use the vectorised CopyTo/Fill/TensorPrimitives APIs instead of hand-written loops. Then I prove it with BenchmarkDotNet and compare production counters.

↩ Usually asked after

More in Arrays

Last updated 30 Oct 2026.

🌐 Sangam's Webring

⏪ Previous 🏠 Main Next ⏩
💬 Chat with me