Skip to main content
CodeOath
← All posts

C#64 min total · 17 parts

C# Fundamentals: Value Types, Reference Types, Boxing, and the Type System

Part 2 of 17 · ~3 min

Value Types Copy, Reference Types Share

Diagram comparing value types copying data on the stack versus reference types sharing one object on the heap

Start with the smallest possible piece of warehouse data: one SKU's quantity on hand, at one moment in time.

struct StockCount
{
    public string Sku;
    public int OnHand;
}

That's a struct, so it's a value type — like int, bool, or DateTime. Assign one to another variable and you get an independent copy of the data, not a second name for the same thing:

var counted = new StockCount { Sku = "WDG-100", OnHand = 42 };
var forReport = counted;
forReport.OnHand = 0;              // a report line that zeroes out a count for display purposes

Console.WriteLine(counted.OnHand);    // 42 — untouched
Console.WriteLine(forReport.OnHand);  // 0

Nothing about counted changed. forReport got its own separate copy of the two fields the moment the assignment ran, and editing that copy has no way back to the original.

Now put the same idea next to a class, because the difference between "copies" and "shares" is the whole chapter:

class Shipment
{
    public string OrderId;
    public List<string> PickedSkus = new();
}

var shipment = new Shipment { OrderId = "ORD-9001" };
var alsoShipment = shipment;
alsoShipment.PickedSkus.Add("WDG-100");

Console.WriteLine(shipment.PickedSkus.Count); // 1 — same list, same object

shipment and alsoShipment were never two shipments. The assignment copied a reference — directions to one Shipment object sitting on the heap — so both variables point at the identical object, and a change made through either name is visible through the other. This is exactly the behavior the warehouse system actually wants here: two different parts of the code (the picker's scanner and the packer's screen) need to see the same in-progress shipment update in real time, not two independent snapshots of it.

A parameter list follows the exact same rule as any other assignment. Hand a StockCount to a method and a brand-new copy is what that method actually works with for its entire body — whatever happens to it there stays there, unless the signature was written to ask for something different (a later chapter covers the keyword that changes this):

void ZeroOut(StockCount count) { count.OnHand = 0; }  // mutates a local copy only

var live = new StockCount { Sku = "WDG-100", OnHand = 42 };
ZeroOut(live);
Console.WriteLine(live.OnHand); // 42 — ZeroOut never touched the caller's copy

Hand a Shipment to a method, though, and the parameter is carrying a copy of the reference, not the object. Whatever that method does to the object on the other end of the reference is visible to everyone else holding it too — but pointing the parameter's own copy of the reference somewhere new is a purely local move the caller never finds out about:

void MarkPacked(Shipment s) { s.PickedSkus.Add("PACKED-FLAG"); }  // mutates the shared object
void Replace(Shipment s) { s = new Shipment { OrderId = "DECOY" }; } // rebinds only its own local variable

MarkPacked(shipment);
Console.WriteLine(shipment.PickedSkus.Count); // 2 — the caller's shipment WAS changed

Replace(shipment);
Console.WriteLine(shipment.OrderId); // still "ORD-9001" — Replace only changed its own local reference

That last pair is the one people trip over: MarkPacked and Replace look symmetrical on the page — both take a Shipment and do something to the parameter named s — but only one of them is visible outside the method. The rule that resolves it: mutating the object a reference points to is shared; reassigning the reference variable itself is local. Keep those two verbs — mutate and reassign — separate in your head and this stops being surprising.