Overview of Types
This section explores the differences between reference types and value types in programming, including practical examples using method calls.
Value Types
Value types store the actual data. When you pass a value type to a method, the method receives a copy of the data.
class Person
{
public string Name { get; set; }
public int Age { get; set; }
}
void ChangeAge(int age)
{
age = 10;
}
Person person = new Person { Name = "Old Name", Age = 20 };
ChangeAge(person.Age);
// person.Age is still 20
Reference Types
Reference types store references to the actual data in memory. When you pass a reference type to a method, the method receives a reference to the same object.
class Person
{
public string Name { get; set; }
}
void ChangeName(Person person)
{
person.Name = "New Name";
}
Person person = new Person { Name = "Old Name" };
ChangeName(person);
// person.Name is now "New Name"
Special note: Strings are a special type of reference type in C# that are treated a bit more like value types when passed to methods in that their value is copied.
Stack vs. Heap
You don't need to know the difference between the stack and heap deeply for this course, but you should be aware that value types are stored on the stack and reference types are stored on the heap. What are the most important implications of this?
Value Types:
- Stored on the stack.
- Faster access - direct memory allocation
- Automatic memory management - collected by the runtime when out of scope
Reference Types:
- Stored on the heap.
- Slower access - indirect memory allocation. Reference typed variables, technically speaking, are pointers to the object's location in memory.
- Cleaned up via garbage collection, which is managed by the .NET runtime. Not automatically removed from memory when out of scope
Null and Nullability
Represents the absence of a value. Commonly used in reference types.
string str = null; // Valid for reference types
int number = null; // This will cause a compilation error - value types cannot be null
Nullable value types:
Allows value types to be assigned null. Add a ?
to the type to make it nullable!
string str = null; // Valid for reference types
int? nullableInt = null; // Valid for nullable value types
Word to the Wise: NullReferenceException
This is perhaps the most common exception in C#. It's thrown when you try to access a member of a null object.
Person person = null;
Console.WriteLine(person.Name); // NullReferenceException
"Object reference not set to an instance of an object." These are famous (and famously frustrating) words in C#.
Most important thing to remember: Check for null before accessing members of a reference type!
🌶️🌶️🌶️ Spencer's opinion: if there's one of a few things that Spencer would credit a lot of the success in his career with, it's checking for null
. Google defensive programming and embrace it.
Modern Nullability
Modern .NET versions have added explicit nullability for reference types to the language. This feature helps you to be more explicit about what is nullable and what isn't.
public class Address
{
public string Street { get; set; }
public string City { get; set; }
public string State { get; set; }
public string ZipCode { get; set; }
}
public class Person
{
public string Name { get; set; } // Non-nullable reference type
public string? MiddleName { get; set; } // Nullable reference type
public Address? Address { get; set; } // Nullable reference type
}
🌶️🌶️🌶️ C# devs tend to not fully embrace the benefits of nullable reference types. In brownfield (e.g. existing) projects, it's almost impossible to reverse-add nullability in a meaningful way. In greenfield (e.g. new) projects, I'd recommend turning on nullability + treating warnings as errors - this gives you the strictest null handling rules, and will result in less bugs.
Boxing and Unboxing
This section covers boxing and unboxing in programming, explaining how value types are converted to reference types and vice versa.
Boxing is the process of converting a value type to a reference type.
Unboxing is the process of converting a reference type back to a value type.
Example:
int number = 42;
object boxed = number; // Boxing
object boxed = 42;
int number = (int)boxed; // Unboxing
Boxing and unboxing can impact performance due to the additional overhead of creating objects on the heap. It's important to understand the implication of this, but I wouldn't bother getting too hung up on it either as in practice, it usually doesn't matter that much.