What Type of Type Is That?

The .NET runtime has two broad categories that types fall into. There are value types and there are reference types. There are a lot of minor differences and implementation details that distinguish these two categories. Only a couple of differences are relevant to the daily experience of most developers.

Reference Types

A reference type is a type whose instances are copied by reference. This means that when you have an instance of one in a variable, and then you assign that to another variable, both variables point to the same object. Apply changes via the first variable, and you'll see the effects in the second.

public class Point {
    public double X { get; set; }
    public double Y { get; set; }
}

// Elsewhere...
Point p1 = new Point { X = 5.5, Y = 4.5 };
Point p2 = p1;
p1.X = 6.5;
Console.WriteLine(p2.X); // Prints "6.5"

This reference copy happens any time you bind the value to a new variable. Whether that's a private field on an object, a local variable, a function parameter, or a static field on a class. The runtime keeps track of these variables as they float around and doesn't allow the memory holding the actual object to be freed until it is sure that none of the references are in scope of any active objects.

Value Types

A value type is a type whose instances are copied by value. This means that when you have an instance of one in a variable, and then you assign that to another variable, the second variable has a new object, with the value of each property, and which you can change independently of the original value. 

public struct Point {
    public double X { get; set; }
    public double Y { get; set; }
}

// Elsewhere...
Point p1 = new Point { X = 5.5, Y = 4.5 };
Point p2 = p1;
p1.X = 6.5;
Console.WriteLine(p2.X); // Prints "5.5"
Console.WriteLine(p1.X); // Prints "6.5"

Value types can get tricky. The thing to remember is that this policy goes only one level deep. The properties of a value type have their own copy type, and that will be how they get copied to the new containing object.

public class Point {
    public double X { get; set; }
    public double Y { get; set; }
}

public struct Line {
    public Point Start { get; set; }
    public Point End { get; set; }
}

// Elsewhere...
Point p1 = new Point { X = 0, Y = 0 };
Point p2 = new Point { X = 3, Y = 3 };
Line l1 = { Start = P1, End = P2 };
Line l2 = l1;
p1.X = 1;

Console.WriteLine(p1.Start.X); // Prints "1"
Console.WriteLine(l1.Start.X); // Prints "1"
Console.WriteLine(l2.Start.X); // Prints "1"

Here we see that the changes we make to the reference type instances are retained across the value types, because it's only the bit of information that points at the reference type that is duplicated, not the object that's pointed to.

Memory

The last thing we should talk about is memory. Unfortunately, this bit is complicated despite most often being inconsequential. But it's a question that a prickly interviewer might decide to quiz you on if you make the mistake of claiming to be an expert.

You might guess that based on this difference in copying behaviors that passing around complex value types would be computationally expensive. It is. And potentially memory-consuming as well. Every new reference is a new variable with new copies of its value-typed properties. Instances also tend to be short-lived, though, so you have to work to actually keep the memory filled with value types. Unless you box them.

"Boxing" is what happens when you assign a value type to an object variable. The value type gets wrapped into an object, which does not get copied when you assign it to other variables. This means that you can end up with very long lived value types, with lots of references to them, if you keep them in an object variable. Fortunately, you're not allowed to modify these values without assigning them back to a value typed variable first.

public struct Id {
    public int Value { get; set; }
}

Id id1 = new Id { Value = 5 };
Object id2 = id1;
Object id3 = id2;

Console.WriteLine(id1.Equals(id2)); // Prints "false"
Console.WriteLine(id2.Equals(id3)); // Prints "true"
((Id)id2).Value = 6; // Compiler error

Folks will often talk about stack and heap when asked about the differences between value types and reference types, because stack allocation is way faster. But value types are only guaranteed to be stored on the stack when they are unboxed local variables that aren't used in certain ways. The decision whether to do so in other cases is often not dictated by the CLR spec, so depending on the platform it might or might not do so in a given situation. In short, it's not worth thinking about unless you are bumping into out of memory errors. And even then, there are almost certainly more permanent wins to be had than by worrying about the whereabouts of your local variables.