Skip to main content

.NET Reference Types

Introduction

In .NET, types are categorized into two main groups: value types and reference types. In this article, we'll focus on reference types, which are fundamental to object-oriented programming in C#. Understanding how reference types work is crucial for writing efficient and bug-free code.

Reference types store references to their data (objects) rather than containing the data directly. This behavior affects how they are allocated in memory, how they're passed to methods, and how variable assignment works with them.

What Are Reference Types?

Reference types are types that, when declared, store a reference (memory address) to the actual data rather than the data itself. The data is stored on the heap, which is a region of memory managed by the .NET garbage collector.

Common examples of reference types in .NET include:

  • Classes
  • Interfaces
  • Delegates
  • Arrays (even arrays of value types)
  • Strings (although they have special immutable behavior)

How Reference Types Work

When you create a reference type in .NET, two things happen:

  1. The actual object is allocated on the heap
  2. A reference (like a pointer) to that object is created

Let's see this with a simple example:

csharp
// Creating a reference type (string)
string message = "Hello, World!";

// Creating a reference type (custom class)
Person person = new Person("John", 30);

In both cases above, the actual data is stored in the heap, while message and person variables contain references to that data.

Memory Allocation

To understand reference types better, let's look at how memory is allocated:

csharp
public class Person
{
public string Name { get; set; }
public int Age { get; set; }

public Person(string name, int age)
{
Name = name;
Age = age;
}
}

// Usage
Person person1 = new Person("Alice", 25); // Allocates memory on the heap

When this code runs:

  1. The new keyword allocates memory on the heap for the Person object
  2. The constructor initializes the object's properties
  3. The person1 variable stores a reference to this memory location

Reference Types vs Value Types

To understand reference types better, it helps to compare them with value types:

CharacteristicReference TypesValue Types
Memory locationHeapStack
ContainsReference to dataData itself
Default valuenullZero/default
Assignment behaviorCopies the referenceCopies the value
Memory managementManaged by Garbage CollectorAutomatically removed when out of scope
ExamplesClasses, interfaces, delegatesint, double, struct, enum

Understanding Reference Type Assignment

One of the key behaviors to understand with reference types is assignment. When you assign one reference variable to another, both variables refer to the same object:

csharp
Person person1 = new Person("Alice", 25);
Person person2 = person1; // Both variables now refer to the same object

// Changing a property through one reference affects the object for both references
person2.Name = "Alicia";
Console.WriteLine(person1.Name); // Outputs: "Alicia"

Output:

Alicia

This is different from value types where assignment creates a copy of the value.

Null References

Unlike value types, reference types can be null, which means they don't reference any object:

csharp
Person person = null; // Valid for reference types

// This will throw a NullReferenceException at runtime
// Console.WriteLine(person.Name);

// Safe way to handle potential null references
if (person != null)
{
Console.WriteLine(person.Name);
}

// C# 6.0+ null conditional operator
Console.WriteLine(person?.Name); // Outputs nothing, but doesn't throw exception

// C# 8.0+ null coalescing operator with null conditional
string name = person?.Name ?? "Unknown"; // name will be "Unknown" if person is null or person.Name is null

Reference Types as Method Parameters

Understanding how reference types are passed to methods is crucial:

csharp
public static void ModifyPerson(Person p)
{
// This modifies the actual object, not a copy
p.Name = "Modified";

// This doesn't affect the original reference outside this method
p = new Person("New Person", 50);
}

public static void Main()
{
Person person = new Person("Original", 30);

ModifyPerson(person);

// Will output "Modified" because the object was modified through the reference
Console.WriteLine(person.Name);

// Will output 30 because reassigning 'p' in the method doesn't affect 'person'
Console.WriteLine(person.Age);
}

Output:

Modified
30

Common Reference Types in .NET

String

string is a reference type, but it behaves differently from other reference types because it's immutable:

csharp
string text1 = "Hello";
string text2 = text1;
text1 = "Hello, World"; // Creates a new string rather than modifying the original

Console.WriteLine(text1); // Outputs: "Hello, World"
Console.WriteLine(text2); // Outputs: "Hello"

Output:

Hello, World
Hello

Arrays

Arrays are reference types, even when they contain value types:

csharp
int[] numbers1 = { 1, 2, 3 };
int[] numbers2 = numbers1;

numbers2[0] = 99;

Console.WriteLine(numbers1[0]); // Outputs: 99

Output:

99

Custom Classes

Classes are the most common reference types you'll create:

csharp
public class Product
{
public string Name { get; set; }
public decimal Price { get; set; }

public Product(string name, decimal price)
{
Name = name;
Price = price;
}

public void ApplyDiscount(decimal percentage)
{
Price -= Price * percentage / 100;
}
}

// Usage
Product laptop = new Product("Laptop", 1000m);
laptop.ApplyDiscount(10);
Console.WriteLine($"{laptop.Name}: ${laptop.Price}");

Output:

Laptop: $900

Reference Types and Garbage Collection

One important aspect of reference types is how they're cleaned up from memory. The .NET garbage collector automatically manages memory for reference types:

csharp
void CreateObjects()
{
// This object becomes eligible for garbage collection when CreateObjects ends
Person temporary = new Person("Temporary", 20);

// This also becomes eligible when reassigned to null
Person person = new Person("Example", 25);
person = null; // The original "Example" Person is now unreachable
}

Real-World Application: Building a Simple Task Management System

Let's see how reference types work in a practical scenario - a simple task management system:

csharp
public class Task
{
public string Title { get; set; }
public string Description { get; set; }
public bool IsCompleted { get; set; }

public Task(string title, string description)
{
Title = title;
Description = description;
IsCompleted = false;
}

public void Complete()
{
IsCompleted = true;
}
}

public class TaskList
{
private List<Task> tasks;

public TaskList()
{
tasks = new List<Task>();
}

public void AddTask(Task task)
{
tasks.Add(task);
}

public void CompleteTask(int index)
{
if (index >= 0 && index < tasks.Count)
{
tasks[index].Complete();
}
}

public void DisplayTasks()
{
for (int i = 0; i < tasks.Count; i++)
{
var task = tasks[i];
var status = task.IsCompleted ? "[X]" : "[ ]";
Console.WriteLine($"{i+1}. {status} {task.Title} - {task.Description}");
}
}
}

// Usage
public static void RunTaskManager()
{
TaskList myTasks = new TaskList();

// Creating task objects
Task buyGroceries = new Task("Buy Groceries", "Milk, eggs, bread");
Task studyDotNet = new Task("Study .NET", "Learn about reference types");

// Adding tasks to the list
myTasks.AddTask(buyGroceries);
myTasks.AddTask(studyDotNet);

Console.WriteLine("Initial Tasks:");
myTasks.DisplayTasks();

// Complete a task
myTasks.CompleteTask(0);

Console.WriteLine("\nAfter completing first task:");
myTasks.DisplayTasks();
}

Output:

Initial Tasks:
1. [ ] Buy Groceries - Milk, eggs, bread
2. [ ] Study .NET - Learn about reference types

After completing first task:
1. [X] Buy Groceries - Milk, eggs, bread
2. [ ] Study .NET - Learn about reference types

In this example, we use several reference types:

  • Task class instances
  • TaskList class instance
  • List<Task> collection
  • string properties

Notice how modifying a task through the TaskList actually modifies the original task object. This is the reference type behavior in action.

Best Practices for Working with Reference Types

  1. Be cautious with null values:

    • Use null checks before accessing reference type members
    • Consider using nullable reference types feature in C# 8.0+
    • Utilize null conditional operators (?.) and null coalescing operators (??)
  2. Understand when objects become eligible for garbage collection:

    • Set references to null when you're done with them in long-running methods
    • Be aware of how long-lived references can prevent garbage collection
  3. Create immutable reference types when appropriate:

    • Like the built-in string type, immutable types can be more predictable
    • Use readonly fields and properties with private setters
  4. Be mindful of unintentional sharing:

    • Remember that reference assignment creates a shared reference, not a copy
    • If you need independent copies, implement cloning or copying mechanisms

Summary

Reference types are fundamental building blocks in .NET programming. Unlike value types that directly contain their data, reference types store a reference to their data on the heap. This behavior enables powerful object-oriented programming patterns but also introduces complexity around sharing, mutation, and memory management.

Key points to remember:

  • Reference types store references to data on the heap
  • Assignment copies the reference, not the data
  • Multiple variables can reference the same object
  • Changes to an object are visible through all references to it
  • Reference types can be null
  • The garbage collector automatically cleans up unreferenced objects

By understanding reference types thoroughly, you can write more effective and efficient .NET code and avoid common pitfalls like unexpected side effects and null reference exceptions.

Practice Exercises

  1. Create a Book class with properties for title, author, and page count. Create two references to the same book and demonstrate how changing a property through one reference affects the other.

  2. Implement a DeepCopy method for a class with nested reference types.

  3. Create a BankAccount class and implement withdraw and deposit methods. Use reference types to model a banking system where multiple variables might refer to the same account.

  4. Explore how the ref and out keywords interact with reference types in method parameters.

Additional Resources



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)