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

📚 Interview question

How do structs work in C#? Explain value semantics and copying, constructors and default values, readonly structs and readonly members, ref structs and Span, record structs, boxing, and the bugs that mutable structs cause.

C# structs in depth: copying, default(T), parameterless constructors, readonly structs and defensive copies, ref structs, C# 13 interface support, boxing and mutable-struct bugs.

Senior Theory Advanced csharp oop performance readonly-struct ref-struct span struct value-types

🎯 Key points

  • Structs are value types: assignment and parameter passing copy the data
  • A struct lives inside its container (local, array, class field), not always on the stack
  • default(T) and array elements are all-zero and skip any parameterless constructor (allowed since C# 10)
  • readonly struct and readonly members prevent mutation and avoid defensive copies on readonly fields and in parameters
  • ref structs such as `Span<T>` are stack-only; C# 13 lets them implement interfaces and adds `allows ref struct`
  • Converting a struct to object or an interface boxes it; generic constraints avoid boxing
  • Default ValueType.Equals can use reflection; implement `IEquatable<T>` or use a record struct
  • Keep structs small and immutable; mutable structs cause lost updates on copies

⏱ 30-second answer

A struct is a value type: a variable holds the data itself, assignment and parameter passing copy it, and it is stored inline (in a local, a field or an array element) rather than as a separate heap object. Structs cannot inherit (they are sealed) but can implement interfaces. default(T) always gives all-zero fields, even when you write a parameterless constructor (allowed since C# 10). readonly struct guarantees immutability and avoids defensive copies. ref struct (like Span<T>) can live only on the stack; since C# 13 it can implement interfaces and be used with allows ref struct generics. Converting a struct to object or an interface boxes it. Mutable structs cause lost updates because you often change a copy.

🗣 Full answer (senior)

How I would answer this in an interview

A struct is a value type: a variable contains the data itself, and assignment, argument passing and returning copy it. Structs are for small, immutable values where avoiding heap allocation matters. I'd cover copying, constructors and defaults, readonly structs and members, ref struct and Span<T>, record structs, boxing, and the bugs mutable structs cause.

1. Value semantics and copying

public struct Point { public int X; public int Y; }

var a = new Point { X = 1, Y = 2 };
var b = a;      // full copy
b.X = 99;       // a.X still 1
  • Structs live inline wherever their variable lives: on the stack for locals, inside the object for fields of a class, contiguously inside arrays.
  • Copy cost equals the struct's size; small structs (≤ 16 bytes) are cheap — often passed in registers; large structs cost a memcpy on every assignment/call.
  • No heap allocation and no GC tracking for the struct itself — the main benefit.

2. Constructors and default values

  • Every struct has a default value with all fields zeroed (default(Point)), and the runtime creates it without running any constructor — for array elements, uninitialised fields and default.
  • C# 10 allows explicit parameterless constructors and field initialisers, but they only run for new Point(), not for default or array elements.
  • C# 11 auto-defaults fields not assigned in a constructor (previously CS0171).
  • Design rule: the all-zero value must be valid or detectable (IsDefault), e.g. Money with Currency == null.

3. readonly structs and members

public readonly struct Money(decimal amount, string currency)
{
    public decimal Amount { get; } = amount;
    public string Currency { get; } = currency;
    public Money Add(Money other) => new(Amount + other.Amount, Currency);   // returns a new value
}
  • readonly struct makes all fields readonly, guarantees immutability, and lets the compiler avoid defensive copies when the struct is accessed via in parameters, readonly fields or ref readonly returns.
  • readonly members (public readonly double Length() => ...) mark individual methods as non-mutating in a mutable struct.

4. ref struct and Span<T>

ref struct types (e.g. Span<T>, ReadOnlySpan<T>, Utf8JsonReader) must stay on the stack:

  • Can't be boxed, stored in fields of classes or regular structs, captured by lambdas, or (before C# 13) used in async methods/iterators.
  • They can contain ref fields (C# 11), which is how Span<T> holds a managed pointer to arrays, strings, stack memory or native memory.
  • C# 13 lets ref structs implement interfaces and be used as generic arguments with allows ref struct, still without boxing.

This enables zero-allocation, memory-safe slicing of buffers.

5. Record structs

readonly record struct OrderId(Guid Value) generates fast value equality (IEquatable<T>, GetHashCode), ==, ToString, deconstruction and with. Ideal for strongly typed IDs and small value objects; much better than ValueType's default reflection-based Equals.

6. Boxing

Converting a struct to object, ValueType, or an interface allocates a box on the heap and copies the value. Hidden boxing appears with non-generic collections, object parameters (string.Format), interface parameters, Equals(object) on structs without IEquatable<T>, and calling non-overridden ToString/GetHashCode. Generics with constraints avoid boxing.

7. Bugs mutable structs cause

var points = new List<Point> { new() { X = 1 } };
points[0].X = 5;                 // CS1612: can't modify the return value (it's a copy)
foreach (var p in points) p.X = 5;   // CS1654 / or silently modifies a copy in other patterns

readonly Point _origin;
_origin.Move(1, 1);              // mutating method runs on a defensive copy - no effect!

IMovable m = point; m.Move(1, 1);   // boxes - mutates the box, not 'point'

Lost updates are silent and confusing. Microsoft's guidance: structs should be immutable ("mutable structs are evil" — Eric Lippert). Make them readonly and return new values.

8. A production example

A telemetry ingestion pipeline modelled readings as a 24-byte readonly record struct Reading(DateTime At, float Value, int SensorId) stored in arrays and spans; switching from a class saved millions of allocations per minute and roughly halved GC time. In the same codebase, a mutable struct Counter stored in a Dictionary<string, Counter> never incremented because dict[key].Increment() modified a copy — we changed it to CollectionsMarshal.GetValueRefOrAddDefault to mutate in place, and later to an immutable struct with replace-on-update.

🔍 What the interviewer is checking

Probes deep understanding of value types and modern struct features.

Green flags: copy semantics; struct location depends on container; default versus constructor; readonly structs and defensive copies; ref struct rules and C# 13 changes; boxing and ValueType.Equals; mutable struct bugs; size guideline; measurement.

Red flags: "structs are on the stack, classes on the heap" as the whole story; mutable structs; "structs are always faster"; unaware of defensive copies.

📚 Learn in depth

1. Memory layout

  • Struct fields are laid out sequentially by default ([StructLayout(LayoutKind.Sequential)]), with padding for alignment. A struct with byte, long, byte occupies 24 bytes; reordering to long, byte, byte gives 16. (For structs containing references, the runtime may use auto layout.)
  • [StructLayout(LayoutKind.Explicit)] with [FieldOffset] enables unions for interop.
  • Unsafe.SizeOf<T>() reports the size.
  • A struct containing references (e.g. a string field) must be scanned by the GC when it's in a heap object or array.

2. Passing structs: by value, in, ref

Mechanism Copy? Callee can mutate caller's copy? Notes
By value Yes No Best for small structs
in / ref readonly No No Use with readonly struct to avoid defensive copies
ref No Yes For intentional in-place updates
out No Must assign Try-pattern

The JIT can promote small struct locals into registers (struct promotion) when they aren't address-exposed, making them effectively free.

3. Defensive copies explained

When calling a member that might mutate on a struct stored in a read-only location (readonly field, in parameter, ref readonly), the compiler copies the struct first, so the original can't change. For non-readonly structs, every such call copies — silently costly for large structs and confusing when the method mutates. readonly struct and readonly members remove the copies.

4. Ref safety

ref structs are tracked by the compiler's ref safety rules: each has a safe-to-escape scope (where it can be returned), and the compiler rejects code that would let a span over stack memory escape its frame. scoped (C# 11) restricts a parameter or local to the current method, enabling more flexible APIs.

5. Equality and hashing

ValueType.Equals uses a bitwise compare when the struct has no references and no padding issues, otherwise reflection over fields (slow, boxing). ValueType.GetHashCode may use only the first field. Always implement IEquatable<T> + GetHashCode (or use record structs) for structs used in collections or comparisons.

6. When to choose a struct (guidelines)

Framework Design Guidelines: consider a struct if the type

  • logically represents a single value (like a primitive),
  • has an instance size under ~16 bytes,
  • is immutable,
  • won't be boxed frequently.

Performance-oriented code bends the size rule (e.g. 24–32-byte structs in arrays) after benchmarking.

Try it yourself

  1. Store a mutable struct in a List<T> and a Dictionary<,> and try to mutate it in each.
  2. Call a mutating method on a readonly field of a non-readonly struct and observe no change.
  3. Measure Unsafe.SizeOf<T>() for different field orders.
  4. Benchmark a Dictionary<OrderId, T> with a plain struct key vs a readonly record struct key.

💻 Code

// Good: small, immutable value type with fast equality
public readonly record struct Coordinate(double Lat, double Lon)
{
    public double DistanceKm(Coordinate other)
    {
        const double R = 6371;
        double dLat = double.DegreesToRadians(other.Lat - Lat);
        double dLon = double.DegreesToRadians(other.Lon - Lon);
        double a = Math.Pow(Math.Sin(dLat / 2), 2) +
                   Math.Cos(double.DegreesToRadians(Lat)) * Math.Cos(double.DegreesToRadians(other.Lat)) *
                   Math.Pow(Math.Sin(dLon / 2), 2);
        return 2 * R * Math.Asin(Math.Sqrt(a));
    }
}

// C# 10 parameterless constructor: runs for new(), NOT for default
public struct RetryPolicy
{
    public int MaxAttempts { get; init; } = 3;
    public RetryPolicy() { }
}
Console.WriteLine(new RetryPolicy().MaxAttempts);   // 3
Console.WriteLine(default(RetryPolicy).MaxAttempts); // 0

// ref struct: stack-only parser over a span, no allocations
public ref struct CsvLineReader(ReadOnlySpan<char> line)
{
    private ReadOnlySpan<char> _rest = line;
    public bool TryRead(out ReadOnlySpan<char> field)
    {
        if (_rest.IsEmpty) { field = default; return false; }
        int i = _rest.IndexOf(',');
        field = i < 0 ? _rest : _rest[..i];
        _rest = i < 0 ? [] : _rest[(i + 1)..];
        return true;
    }
}

// Mutable struct bug
var points = new List<MutablePoint> { new() };
// points[0].X = 5;                 // CS1612: cannot modify the return value (it is a copy)
var p = points[0]; p.X = 5; points[0] = p;   // the only way: copy, change, write back
public struct MutablePoint { public int X; }

⚠ Common mistakes

1. Mutable structs

What happens: structs with settable fields modified through collections, properties or interfaces. Why it is wrong: updates apply to copies and are silently lost. Do instead: make structs readonly and return new values.

2. Large structs

What happens: structs with many fields used as DTOs "to avoid GC". Why it is wrong: copy costs on every assignment and call can exceed allocation costs. Do instead: keep structs small; use classes for larger data or pass by in after benchmarking.

3. Relying on constructors running

What happens: struct constructors validate or initialise fields. Why it is wrong: default and arrays bypass constructors. Do instead: make the zero state valid or check for it.

4. Structs without equality members

What happens: structs used as dictionary keys or compared often rely on ValueType.Equals. Why it is wrong: reflection-based, boxing, poor hashing. Do instead: implement IEquatable<T> or use record structs.

5. Hidden boxing

What happens: structs passed as object or interfaces, stored in non-generic collections. Why it is wrong: allocations and copies negate the benefit of structs. Do instead: use generics with constraints.

6. in with non-readonly structs

What happens: passing large structs by in for performance. Why it is wrong: defensive copies on every member call. Do instead: mark the struct or members readonly.

7. Trying to store spans in classes or across await

What happens: compile errors lead to unsafe workarounds. Why it is wrong: spans may point to stack memory. Do instead: use Memory<T> for storage and async; spans in synchronous code.

8. Choosing structs without measurement

What happens: classes converted to structs for assumed performance. Why it is wrong: boxing and copying can make it slower. Do instead: benchmark allocation and throughput with BenchmarkDotNet before and after.

🏛 Senior insight: trade-offs

Seniors pick structs for measured reasons and design them to be safe.

Situation Choice
Millions of small values in arrays readonly struct or readonly record struct
Strongly typed ID readonly record struct
Stack-only view over memory ref struct (with Span<T>)
Entity, or type with identity Class
Large data copied often Class, or struct passed by in / ref readonly
Generic API that should accept spans where T : allows ref struct (C# 13)

They verify with BenchmarkDotNet and allocation profiles, because converting to structs can make code slower when values are large, boxed or copied a lot.

🔗 Follow-ups the interviewer may ask

  1. When: Candidate says structs live on the stack Why: Checks the precise value versus reference type model
    How to answer

    A value type holds its data directly, so assigning or passing it copies the data; a reference type variable holds a reference to an object on the managed heap, so copying the variable makes two references to the same object. Value types are stored inline where they are declared: a local usually sits on the stack or in a register, but a struct field of a class lives inside that object on the heap, and captured or boxed values also end up on the heap. By default classes compare by identity and structs by their fields. I use structs for small, immutable values like Money or coordinates, and classes for everything with identity or behaviour.

  2. When: Candidate mentions ref structs and Span Why: Probes `ReadOnlySpan<char>` usage in practice
    How to answer

    ReadOnlySpan<char> is a view over contiguous characters, which can be a string, an array or a stack buffer, made of a reference plus a length. Slicing it is free (no new string), and almost every parsing API now takes spans, so I can split a CSV line, trim fields and parse numbers and dates with zero allocations until I really need a string. It's a ref struct, so it can't be stored in a class field, boxed, captured by a lambda or held across an await. For those cases I use ReadOnlyMemory<char> and call .Span when processing. I reach for it in measured hot paths such as parsers, protocol code and high-volume ingestion.

  3. When: Candidate recommends readonly record struct Why: Checks generated record members and with-expressions
    How to answer

    A record is a type whose identity is its data. public record Money(decimal Amount, string Currency); makes the compiler generate init-only properties, a constructor, Deconstruct, value-based Equals/GetHashCode/==, a readable ToString, and a hidden clone method used by with expressions for non-destructive mutation. record (or record class, C# 9) is a reference type; record struct (C# 10) is a value type whose positional properties are mutable unless you write readonly record struct. Equality includes an EqualityContract so a base and derived record are never equal. Watch out: with is a shallow copy, collection members compare by reference, and records are a poor fit for EF Core entities, which need identity-based equality.

🛠 Real-world problems: how you handle them

Cutting GC pressure in telemetry ingestion with readonly structs
Situation
A telemetry ingestion service processed about [50 million] measurements per hour, each a small class with timestamp, sensor ID and value.
Signals
dotnet-counters showed an allocation rate of [1.2 GB/s] and frequent gen-0 GCs; p99 ingest latency spiked during bursts.
Investigation
An allocation profile (dotnet-trace with the GC verbose provider) showed `Measurement` objects as the top allocation by count.
Resolution
Changed `Measurement` to a `readonly record struct` of [24] bytes, stored batches in arrays rented from `ArrayPool<Measurement>.Shared`, and processed them with `Span<Measurement>`.
Prevention
Added a BenchmarkDotNet test with `[MemoryDiagnoser]` to CI for the ingest path, failing if allocated bytes per batch grows.

How to tell it (STAR)

**Situation:** our telemetry ingestion allocated about [1.2 GB/s] because each measurement was a small class. **Task:** reduce GC pressure without a rewrite. **Action:** I converted `Measurement` to a small `readonly record struct`, batched values in pooled arrays and processed them with spans. I kept the struct immutable to avoid copy bugs and added an allocation benchmark to CI. **Result:** allocations dropped by about [90%], gen-0 GCs fell sharply and p99 latency during bursts improved by [roughly 40%].

↩ Usually asked after

More in C# OOP Features

Last updated 30 Oct 2026.

🌐 Sangam's Webring

⏪ Previous 🏠 Main Next ⏩
💬 Chat with me