ZetCode

C# Copy by Value vs Copy by Reference

last modified May 16, 2025

This tutorial explains how C# handles copy by value and copy by reference, affecting variable assignment and parameter passing. Understanding these concepts is crucial for writing correct and efficient C# code, as it determines how data is shared or isolated between variables and methods.

Value Types vs Reference Types

C# types are split into value types and reference types, which dictate how data is copied, stored, and managed in memory.

Characteristic Value Types Reference Types
Storage Stack (direct data) Heap (reference to data)
Copy Behavior Copy by value (independent copy) Copy by reference (shared object)
Examples int, double, struct class, array, string

When you assign a value type variable to another, a new copy of the data is created. For reference types, only the reference is copied, so both variables point to the same object in memory.

Copy by Value Example

Value types create an independent copy when assigned, so changes to one variable don't affect the other. This is true for both built-in types and user-defined structs.

Program.cs
int a = 10;
int b = a; // Copy by value

Console.WriteLine($"Original: a = {a}, b = {b}");
b = 20; // Doesn't affect a
Console.WriteLine($"After change: a = {a}, b = {b}");

// Structs are value types
Point p1 = new (1, 2);
Point p2 = p1; // Copy by value

p2.X = 10; // Doesn't affect p1
Console.WriteLine($"p1 = ({p1.X}, {p1.Y}), p2 = ({p2.X}, {p2.Y})");

struct Point(int x, int y)
{
    public int X = x;
    public int Y = y;
}

The example shows value types (int, struct) creating independent copies. Modifying one does not affect the other.

$ dotnet run
Original: a = 10, b = 10
After change: a = 10, b = 20
p1 = (1, 2), p2 = (10, 2)

Copying Arrays of Value Types

Arrays themselves are reference types, even if their elements are value types. Assigning one array to another copies the reference, not the elements.

Program.cs
int[] arr1 = { 1, 2, 3 };
int[] arr2 = arr1; // arr2 references the same array as arr1

arr2[0] = 99;
Console.WriteLine($"arr1[0] = {arr1[0]}, arr2[0] = {arr2[0]}");

This shows that modifying arr2 also affects arr1, because they reference the same array. The output will be:

$ dotnet run
arr1[0] = 99, arr2[0] = 99

To create a true copy of the array, use Array.Copy or Clone:

Program.cs
int[] arr1 = { 1, 2, 3 };    
int[] arr2 = (int[])arr1.Clone();

arr2[0] = 123;
Console.WriteLine($"arr1[0] = {arr1[0]}, arr2[0] = {arr2[0]}");

This creates a new array with the same elements as arr1. Changes to arr2 do not affect arr1. The Clone method creates a shallow copy of the array, meaning it copies the elements but not the objects they reference. For reference types, a deep copy is needed to create a new instance of the objects in the array.

Strings: Reference Type with Immutable Behavior

Strings in C# are reference types, but they are immutable. This means that once a string object is created, it cannot be changed. Any operation that appears to modify a string actually creates a new string object in memory. As a result, assigning one string variable to another copies the reference, but changing one variable does not affect the other.

Program.cs
string s1 = "hello";
string s2 = s1; // Copy by reference (but strings are immutable)

Console.WriteLine($"Original: s1 = {s1}, s2 = {s2}");
s2 = "world"; // s1 is not affected
Console.WriteLine($"After change: s1 = {s1}, s2 = {s2}");

In this example, s1 and s2 initially reference the same string object. When s2 is assigned a new value, it points to a new string object, while s1 remains unchanged. This demonstrates the immutable nature of strings in C#.

$ dotnet run
Original: s1 = hello, s2 = hello
After change: s1 = hello, s2 = world

Because of immutability, strings are safe to share between variables and methods without risk of accidental modification. However, frequent string modifications can lead to performance issues due to the creation of many temporary string objects. For scenarios requiring many changes, consider using StringBuilder.

Copy by Reference Example

Reference types in C# store memory addresses rather than actual values. When a reference type variable is assigned to another variable, the memory address is copied, meaning both variables point to the same data. This behavior allows modifications to one variable to be reflected in the original object.

Unlike value types, which create separate copies when assigned, reference types maintain shared data across multiple variables. This is especially important when working with complex objects, such as arrays and class instances, where multiple references can interact with the same underlying memory.

Program.cs
int[] arr1 = { 1, 2, 3 };
int[] arr2 = arr1; // Copy by reference

Console.WriteLine($"Original: arr1[0] = {arr1[0]}, arr2[0] = {arr2[0]}");

arr2[0] = 100; // Affects arr1
Console.WriteLine($"After change: arr1[0] = {arr1[0]}, arr2[0] = {arr2[0]}");

In the example above, both arr1 and arr2 point to the same array. When modifying arr2[0], the change is also applied to arr1. Since arrays are reference types, their contents are not duplicated during assignment.

Similarly, objects instantiated from a class behave in the same manner. When one object reference is assigned to another variable, both variables share the same memory address. Modifying one will affect the other.

Program.cs
Person person1 = new("Alice");
Person person2 = person1; // Copy by reference

person2.Name = "Bob"; // Affects person1
Console.WriteLine($"person1.Name = {person1.Name}, person2.Name = {person2.Name}");

class Person(string name)
{
    public string Name { get; set; } = name;
}

The example demonstrates how class instances behave as reference types. Initially, person1 contains the name "Alice." When assigned to person2, both variables reference the same object. Changing person2.Name updates person1.Name because they share the same memory.

This characteristic of reference types enables efficient memory usage but also requires careful handling. Unintentional modifications to shared objects can introduce unintended side effects, making it essential to understand how variables reference data.

$ dotnet run
Original: arr1[0] = 1, arr2[0] = 1
After change: arr1[0] = 100, arr2[0] = 100
person1.Name = Bob, person2.Name = Bob

To prevent accidental modifications, developers often use techniques such as cloning objects or using immutable types to ensure data integrity. Understanding how reference types function is fundamental to writing efficient and predictable C# programs.

Parameter Passing: Value, Reference, ref, and out

C# handles parameter passing in different ways, depending on whether a variable is a value type or a reference type. By default, value type parameters are copied, meaning modifications inside a method do not affect the original variable. Reference type parameters, on the other hand, pass the reference, allowing changes inside the method to be reflected outside it. Additionally, C# provides the ref and out keywords to explicitly pass variables by reference, enabling methods to modify the caller's variable.

Default Behavior: Value vs. Reference Types

When passing a value type parameter, a copy of the value is sent to the method, meaning modifications do not affect the original variable.

Program.cs
int number = 10;
ModifyValue(number);
Console.WriteLine($"After ModifyValue: {number}"); // Output: 10

void ModifyValue(int x)
{
    x = 20; // Changes the local copy, not the original
}

In this example, the ModifyValue method does not affect the original variable number. The value of number remains 10 after the method call. This is because number is a value type, and its value is copied when passed to the method.

Reference type parameters pass a reference to the original object, meaning modifications inside the method will impact the original data.

Program.cs
int[] numbers = { 1, 2, 3 };
ModifyReference(numbers);
Console.WriteLine($"After ModifyReference: {string.Join(", ", numbers)}");

void ModifyReference(int[] arr)
{
    arr[0] = 100; // Changes the actual array
}

In this example, the ModifyReference method modifies the first element of the array numbers. The change is reflected outside the method because the array is a reference type. The method receives a reference to the original array, allowing it to modify the data directly. As a result, the output shows that the first element of the array has been changed to 100.

Explicitly Passing by Reference Using ref

The ref keyword allows a method to modify a variable's value directly in the caller's scope. Unlike standard value passing, changes inside the method persist outside it.

Program.cs
int y = 5;
SetToTen(ref y);
Console.WriteLine($"y = {y}"); // Output: y = 10

void SetToTen(ref int n)
{
    n = 10; // Modifies the original variable
}

The ref keyword must be used in both the method signature and the method call. This indicates that the variable is passed by reference, allowing the method to modify its value directly. This is particularly useful when you need to return multiple values or when you want to avoid copying large data structures.

Using out for Mandatory Output

The out keyword works similarly to ref, but it indicates that the parameter must be assigned inside the method before exiting. It is primarily used for returning multiple values.

Program.cs
int z;
Initialize(out z);
Console.WriteLine($"z = {z}"); // Output: z = 42

void Initialize(out int n)
{
    n = 42; // Must be assigned before the method returns
}

The out keyword allows the method to return multiple values by assigning values to the output parameters. Unlike ref, the variable does not need to be initialized before being passed to the method. The method is responsible for assigning a value to the out parameter before returning. This is useful for methods that need to return multiple values or when the output value is not known until the method executes.

Key Differences Between ref and out

The ref keyword requires that the variable be initialized before it is passed into the method, as it maintains its original value while allowing modifications. The out keyword, however, does not require initialization beforehand—the method is responsible for assigning a value before returning.

Using these techniques strategically can optimize memory usage and enhance functionality when working with complex data structures or scenarios requiring multiple return values.

Summary and Best Practices

This tutorial covered copy by value and copy by reference in C#, including memory allocation, assignment, parameter passing, and best practices.

Source

C# value types documentation

C# reference types documentation

C# parameter passing

Author

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all C# tutorials.