Memory & Garbage Collection · Lesson 13

Boxing and reference identity

Observe boxed value semantics before making allocation claims about generic or object-based APIs.

Explore the interactive Memory Lab

Change owners, step through generations, pinning and finalization, or compare memory under load. The model explains concepts; the SDK examples measure real runtime observations.

Open the Memory Lab ↗

The memory contract

Boxing converts a value-type value into an object representation that can be used through object or an appropriate interface. The boxed value is a copy, not a live reference to the original integer variable. Two boxing conversions can therefore create logically distinct objects with equal contained values. This matters for identity and mutation reasoning before performance is discussed. Generic APIs can often preserve value-type information without forcing every value through object, but actual allocation costs still depend on the operation, optimization, and target runtime.

Trace the actual program

The integer begins at seven and is boxed separately into first and second. ReferenceEquals prints False, because the two boxed object identities are distinct. Their value-based Equals comparison prints True, since both contain the same integer value. Updating the original variable to nine does not rewrite either box, so unboxing first prints 7. The program observes language semantics, not a byte counter. An optimizer may avoid unnecessary physical work only when it preserves those observable identities and values.

How the mechanism works

Unboxing must agree with the boxed value's actual value type. A boxed int is not a boxed long merely because both are numeric, and a cast that unboxes the wrong type can fail at runtime. Converting an int to long before boxing is a different sequence of operations from boxing an int and hoping a later unbox changes its type. Interface calls, nongeneric collections, formatting paths, and equality APIs can introduce boxing in different contexts, so inspect the specific call site rather than treating every use of a struct as allocation-free.

Boundaries and misconceptions

Avoid solving a type mismatch by converting everything to object. That can discard useful compile-time information and shift invalid assumptions into runtime casts. Conversely, do not replace an appropriate abstraction solely because a toy example suggests boxing: measure its real cost in the relevant workload. A mutable struct also has copying pitfalls independent of boxing. Distinguish the value in a local variable, the value copied into a box, and references that alias the same box. These are different parts of the ownership graph.

Practice: change one ownership path

First assign second = first instead of boxing value again. Predict ReferenceEquals true while value equality remains true. Hint: this variation copies an object reference rather than repeating the boxing conversion. Restore the original, then change value to a different integer before constructing second and predict that both identity and value equality are false. Keep the original unbox observation separate so you can explain which box contains which copied number.

Practice: test a second boundary

Next store a small struct in a generic List of that struct and compare its strongly typed use with a separate List of object experiment. Hint: preserve the required behavior before counting allocations. Use the later synchronous allocation-measurement pattern on a supported target, record the runtime, and avoid claiming the browser delta must equal a desktop result. Add a deliberate wrong-type unbox in a controlled failure exercise and explain why numeric convertibility does not erase the actual boxed type.

Review the evidence

For the unchanged source, False, True, and seven are the important facts. A correct explanation separates object identity, contained-value equality, and reassignment of the original value variable. The performance follow-up asks where boxing is required, whether it is avoidable without changing the API contract, and what the target runtime actually allocates. This prevents both common extremes: assuming all value-type code is free of managed allocations, or treating every boxed value as a reason to abandon a clear design without evidence. Include identity-preservation tests when evaluating any optimization that changes where values are stored.

  • The original boxes are distinct objects containing equal integer values.
  • Changing the source integer does not rewrite a previously boxed copy.
  • Allocation evidence must identify the target and preserve the required semantics.

A working example

using System;
using System.Linq;
using System.Collections.Generic;
using System.Text;
using System.Buffers;
using System.Runtime.InteropServices;
using System.Runtime.CompilerServices;

int value = 7;
object first = value;
object second = value;
Console.WriteLine(ReferenceEquals(first, second));
Console.WriteLine(first.Equals(second));
value = 9;
Console.WriteLine((int)first);

Expected output

False
True
7
Try this
The original boxes are distinct objects containing equal integer values.

Further reading