C#

Value types, reference types, and null

What actually gets copied when you pass an int against a List, and how a value type opts into null.

After this lesson you can

  • Say what gets copied when a value type is passed to a method
  • Explain why two reference-type variables can point at the same object
  • Use a nullable value type instead of a sentinel value

structs and the built-in numeric types are value types — a variable holds the data itself. Classes are reference types — a variable holds a pointer to data that lives elsewhere.

int a = 5;
int b = a;      // b is a genuine copy
b = 10;
// a is still 5 — the two ints never shared anything

var list1 = new List<int> { 1, 2, 3 };
var list2 = list1;    // list2 points at the same List object
list2.Add(4);
// list1 now has 4 elements too — there was only ever one list

Passing either kind to a method follows the same rule: a value type is copied in, so changes inside the method do not escape; a reference type passes the reference, so mutating the object it points to is visible to the caller. Reassigning the parameter itself to a new object, though, never escapes either way — only mutating what it already points to does.

Try it

A copied int, a shared listcsharp-9
public static class Solution{    public static string Describe()    {        int a = 5;        int b = a;        b = 10;         var list1 = new List<int> { 1, 2, 3 };        var list2 = list1;        list2.Add(4);         return $"a={a} list1.Count={list1.Count}";    }}
What to look for

Structs are value types too

public struct Point
{
    public int X, Y;
}

var p1 = new Point { X = 1, Y = 2 };
var p2 = p1;          // a full copy, not a shared reference
p2.X = 99;
// p1.X is still 1

This is the actual reason to reach for a struct instead of a class: small, immutable-in-spirit data where copy semantics are what you want, and where the type is small enough that copying it is cheap. Reach for class by default; struct is the deliberate exception.

Nullable value types

A plain value type can never be null — int x = null; does not compile. int? (shorthand for Nullable<int>) opts a value type into carrying "no value" explicitly, rather than a caller inventing a sentinel like -1 to mean the same thing.

int? maybeAge = null;
if (maybeAge.HasValue)
    Console.WriteLine(maybeAge.Value);

int age = maybeAge ?? 0;   // ?? supplies a default when it is null

Reference types are nullable by default — string, List<int>, any class — which is exactly what nullable reference types (string? under #nullable enable) exist to push back on, by making the compiler warn when a reference that was not declared ? might be null.

Try it yourself

2 visible tests · 2 hidden tests

Implement FindMax(List<int> values), returning int?. Return the largest value in the list, or null if the list is empty — never use a sentinel like -1 or int.MinValue, since either could be a genuine value in the list.

  • FindMax([3,7,2,9,4])
  • FindMax([])
Loading editor…

Sign up to check the hidden tests and save your progress. Sign up