Value Types and Reference Types
A value type variable holds the data; a reference type variable holds its address. That difference decides what assignment copies.
struct Pixel { } // value type class Lamp { } // reference typeThis one distinction explains most of the surprising behaviour in C#. A class is a blueprint for a kind of thing, and an object is one thing built from that blueprint. C# sorts every type into two groups, by how a variable holds it.
The two groups
A value type variable holds the data itself. All the numeric types, bool, char and anything declared with the struct keyword are value types. A reference type variable holds the address where the data lives. Anything declared with class is a reference type, and so are string, arrays, interfaces and delegates. An address is not the thing, in the same way that a street address written on paper is not the house.
What that changes
Assignment copies whatever the variable holds. For a value type that is the data, so afterwards the two variables are independent. For a reference type that is the address, so both variables now reach the same single object, and a change made through one is visible through the other. A method is a named block of code you call by name, and passing a variable to one works the same way: the method receives a copy of the variable, so the same two rules apply.
Only a reference type variable can hold null, meaning this address points at nothing. The two forms Lamp one = new Lamp(); and Lamp one = new(); are the same thing; C# 9 added the short one for when the type is already written on the left.
What trips people up
Writing Lamp two = one; and expecting two lamps. You have one lamp and two names for it.
Example
// Pixel is a struct, which makes it a value type: the variable holds the data.
Pixel first = new Pixel();
first.Brightness = 10;
Pixel second = first; // copies the data into a separate Pixel
second.Brightness = 99;
Console.WriteLine($"struct first={first.Brightness} second={second.Brightness}");
// Lamp is a class, which makes it a reference type: the variable holds an address.
Lamp one = new Lamp();
one.Brightness = 10;
Lamp two = one; // copies the address, so both names reach one Lamp
two.Brightness = 99;
Console.WriteLine($"class one={one.Brightness} two={two.Brightness}");
struct Pixel
{
public int Brightness;
}
class Lamp
{
public int Brightness;
}
// Output:
// struct first=10 second=99
// class one=99 two=99When to use it
- Work out why a method you called changed the object you passed it.
- Decide whether to declare a small, simple group of fields as a struct or a class.
- Explain why a test comparing two freshly built objects fails although every field matches.
- Copy an object on purpose, rather than handing out a second name for the same one.
More examples
Passing a variable to a method
Both methods do exactly the same thing to their parameter, and the results differ because the parameters hold different things. This is the shape of a whole family of real bugs: a method that quietly changes an object it was handed, and a method whose change to a struct vanishes the moment it returns. When you write a method that takes a class, assume the caller will see whatever you change.
Pixel pixel = new Pixel();
pixel.Brightness = 10;
Lamp lamp = new Lamp();
lamp.Brightness = 10;
// The method receives a copy of the variable in both cases.
DimPixel(pixel); // a copy of the data, so the original is untouched
DimLamp(lamp); // a copy of the address, so the original object changes
Console.WriteLine($"pixel={pixel.Brightness} lamp={lamp.Brightness}");
static void DimPixel(Pixel p)
{
p.Brightness = 0;
}
static void DimLamp(Lamp l)
{
l.Brightness = 0;
}
struct Pixel
{
public int Brightness;
}
class Lamp
{
public int Brightness;
}
// Output:
// pixel=10 lamp=0Two names for one object, and equality
Two lamps set to the same brightness are not equal, because == on a class asks whether the two variables hold the same address. This is why a test that compares two freshly built objects fails even though every field matches, and why you compare the fields or override equality instead. Note that writing kitchen == spare compiles happily, so nothing warns you.
Lamp kitchen = new Lamp();
kitchen.Brightness = 50;
Lamp alsoKitchen = kitchen; // the same address, so the same object
Lamp spare = new Lamp(); // a different object
spare.Brightness = 50;
// == on a class compares addresses, not contents.
Console.WriteLine(kitchen == alsoKitchen);
Console.WriteLine(kitchen == spare);
Console.WriteLine(kitchen.Brightness == spare.Brightness);
// A struct gets no == for free, but Equals compares its fields.
Pixel one = new Pixel();
Pixel two = new Pixel();
Console.WriteLine(one.Equals(two));
struct Pixel
{
public int Brightness;
}
class Lamp
{
public int Brightness;
}
// Output:
// True
// False
// True
// Truenull, and what a fresh variable holds
The question mark on Lamp? is how you tell the compiler that this variable is allowed to be null, which lets it warn you when you forget to check. A struct variable needs no such marker because there is no address to be missing; it always holds a complete value, with every field at its default. This is the reason a struct can never throw a NullReferenceException.
// Only a reference type variable can hold null: only an address can point at
// nothing. A struct variable always contains a value.
Lamp? missing = null;
Console.WriteLine(missing == null);
Pixel blank = new Pixel(); // already usable, Brightness is 0
Console.WriteLine(blank.Brightness);
Lamp fresh = new Lamp(); // a real object, Brightness is also 0
Console.WriteLine(fresh.Brightness);
struct Pixel
{
public int Brightness;
}
class Lamp
{
public int Brightness;
}
// Output:
// True
// 0
// 0
Discussion