Value Types, Reference Types and Boxing in C#
Codevity Admin · 10/25/2026
Step 3 of 31 · C# Interview Preparation for Beginners · Stage 2: Language fundamentals
Codevity Admin · 10/25/2026
How data is stored and copied is the root of many C# interview questions. This article covers the whole family: value versus reference types, boxing, equality, the type keywords, and parameter passing.
| Value types | Reference types | |
|---|---|---|
| Examples | int, double, bool, char, decimal, enum, struct, DateTime | class, string, arrays, interface, delegates, object, record |
| A variable holds | The data itself | A reference to an object on the managed heap |
| Assignment | Copies the data | Copies the reference: both variables point to the same object |
| Default value | 0, false and so on | null |
Can be null | Only as Nullable<T> (int?) | Yes |
Point p1 = new() { X = 1 };
Point p2 = p1; // copies the data
p2.X = 99;
Console.WriteLine(p1.X); // 1
Box b1 = new() { X = 1 };
Box b2 = b1; // copies the reference
b2.X = 99;
Console.WriteLine(b1.X); // 99
public struct Point { public int X; } // value type
public class Box { public int X; } // reference typeThat answer is common but incomplete. A value-type variable stores its data where the variable lives:
int lives on the stack (or in a CPU register),int field of a class lives inside that object, on the heap,int[] live inside the array object, on the heap.Objects of reference types always live on the heap. The reference itself is just a value, and it lives wherever the variable lives. Saying this precisely is a reliable way to stand out.
Boxing converts a value type to object (or to an interface it implements). The runtime allocates an object on the heap and copies the value into it. Unboxing casts it back, and it must name the exact original type.
int number = 42;
object boxed = number; // boxing: allocates a heap object
int copy = (int)boxed; // unboxing: copies the value back
boxed = 100; // changes only the boxed object
Console.WriteLine(number); // 42
Console.WriteLine((int)boxed); // 100
try
{
long wrong = (long)boxed; // the box holds an int, not a long
}
catch (InvalidCastException)
{
Console.WriteLine("cannot unbox an int as a long");
}Boxing costs an allocation, and allocations create work for the garbage collector. It happens quietly when you put value types in non-generic collections such as ArrayList, pass them as object, or call interface methods on a struct. Generic collections like List<int> avoid it.
== is an operator. For value types it compares values. For reference types it compares references unless the type overloads it (string does, so == compares text).Equals() is a virtual method. The default compares references for classes and fields for structs. Types such as string and record override it to compare content.string a = "hello";
string b = new string("hello".ToCharArray()); // a different object with the same text
object oa = a;
object ob = b;
Console.WriteLine(a == b); // True (string overloads ==)
Console.WriteLine(ReferenceEquals(a, b)); // False (two different objects)
Console.WriteLine(oa == ob); // False (static type object: reference comparison)
Console.WriteLine(oa.Equals(ob)); // True (the virtual Equals is string's)The third line is the one that catches people: the operator is chosen by the compile-time type, while is chosen by the run-time type.
| Keyword | Type is decided | Notes |
|---|---|---|
| At compile time, from the right-hand side | Still strongly typed. is an | |
| It is always | Any value fits, but you need a cast (and boxing for value types) to use it | |
| At run time | Member access is not checked by the compiler. A mistake becomes a |
| Value fixed | At compile time | At run time, once: in the declaration or a constructor |
| Allowed types | Numbers, , , , | Any type |
| Static | Implicitly | Per instance, or |
| Caveat | The value is copied into callers when they are compiled | Changing it does not require recompiling callers |
Use for true constants like , and for values computed at start-up.
All three pass an argument by reference instead of copying it.
| Modifier | Caller must initialize? | Method must assign? | Method may change it? |
|---|---|---|---|
| Yes | No | Yes | |
| No | Yes, before returning | Yes | |
| Yes | No | No: read-only, which avoids copying large structs |
A value type cannot be . is shorthand for , a struct with a flag and a :
Reading while is false throws , so use or . Nullable reference types () are a different feature: annotations that make the compiler warn about possible dereferences. Treat those warnings as bugs.
Equals()varvar x = 5;intobjectobjectdynamicRuntimeBinderExceptionconstreadonlyboolcharstringnullstatic readonlyconstPistatic readonlyrefoutinnullint?Nullable<int>HasValueValue.ValueHasValueInvalidOperationException??GetValueOrDefault()string?nullPointBoxstructclassintArrayListList<int>int counter = 0;
AddOne(ref counter);
Console.WriteLine(counter); // 1
if (int.TryParse("42", out int parsed)) // out: the method assigns parsed
Console.WriteLine(parsed); // 42
static void AddOne(ref int value) => value++;int? score = null;
Console.WriteLine(score.HasValue); // False
Console.WriteLine(score ?? -1); // -1
score = 80;
Console.WriteLine(score.Value); // 80