You are viewing a preview of this lesson. Sign in to start learning
Back to C# Programming

Copying vs Sharing

Understand how assignment and parameter passing behave differently for each semantic

Copying vs Sharing in C#

Master the fundamental difference between copying and sharing data in C# with free flashcards and spaced repetition practice. This lesson covers value type copying behavior, reference type sharing semantics, and how assignment operations differ based on typeβ€”essential concepts for writing predictable, bug-free C# code.

Welcome πŸ’»

When you write int x = 5; int y = x; in C#, you get two independent variables. But when you write List<int> list1 = myList; List<int> list2 = list1;, both variables point to the same list! Understanding this distinction between copying and sharing is absolutely critical to avoiding subtle bugs and writing correct C# programs.

This lesson explores:

  • How value types create independent copies on assignment
  • How reference types share the same object through references
  • The memory implications of each approach
  • Common pitfalls and how to avoid them

Core Concepts πŸ”

What Happens During Assignment?

When you use the assignment operator (=) in C#, what actually happens depends entirely on whether you're working with a value type or a reference type.

πŸ“‹ The Golden Rule

Value TypesAssignment copies the actual data
Reference TypesAssignment copies the reference (address), so both variables share the same object

Value Type Copying Behavior πŸ“¦

Value types (like int, double, bool, struct, enum) store their data directly in the variable's memory location. When you assign one value type variable to another, C# performs a bitwise copy of the entire value.

int original = 42;
int copy = original;  // Copies the value 42

copy = 100;  // Changes only 'copy'

Console.WriteLine(original);  // Still 42
Console.WriteLine(copy);      // Now 100

Memory visualization:

Before assignment:
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ original β”‚
β”‚    42    β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

After: int copy = original;
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ original β”‚    β”‚   copy   β”‚
β”‚    42    β”‚    β”‚    42    β”‚  ← Independent copy
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

After: copy = 100;
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ original β”‚    β”‚   copy   β”‚
β”‚    42    β”‚    β”‚   100    β”‚  ← Changes don't affect original
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

πŸ’‘ Key insight: Each variable has its own independent storage. Modifying one never affects the other.

Reference Type Sharing Behavior πŸ”—

Reference types (like class, interface, delegate, string, arrays, List<T>) store a reference (memory address) to the actual object data, which lives on the heap. When you assign one reference type variable to another, you're copying the reference, not the object itself.

List<int> original = new List<int> { 1, 2, 3 };
List<int> shared = original;  // Copies the REFERENCE, not the list

shared.Add(4);  // Modifies the shared object

Console.WriteLine(original.Count);  // 4 - both see the change!
Console.WriteLine(shared.Count);    // 4

Memory visualization:

After: List original = new List { 1, 2, 3 };

Stack:                    Heap:
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”             β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ original │────────────→│  List object    β”‚
β”‚ (ref)    β”‚             β”‚  [1, 2, 3]      β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜             β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

After: List shared = original;

Stack:                    Heap:
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”             β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ original │────────────→│  List object    β”‚
β”‚ (ref)    β”‚      β”Œβ”€β”€β”€β”€β”€β†’β”‚  [1, 2, 3]      β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜      β”‚      β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”      β”‚       ↑
β”‚  shared  β”‚β”€β”€β”€β”€β”€β”€β”˜       β”‚
β”‚ (ref)    β”‚         Both references
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜         point to SAME object

After: shared.Add(4);

Stack:                    Heap:
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”             β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ original │────────────→│  List object    β”‚
β”‚ (ref)    β”‚      β”Œβ”€β”€β”€β”€β”€β†’β”‚  [1, 2, 3, 4]   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜      β”‚      β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”      β”‚       ↑
β”‚  shared  β”‚β”€β”€β”€β”€β”€β”€β”˜       β”‚
β”‚ (ref)    β”‚         Change visible
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜         through BOTH references

πŸ’‘ Key insight: Both variables are like "remote controls" pointing to the same TV. Press a button on either remote, and the same TV changes.

The String Exception 🎭

Here's a mind-bending fact: string is a reference type in C#, but it behaves like a value type due to immutability!

string original = "Hello";
string copy = original;  // Copies the reference

copy = "World";  // Creates NEW string, doesn't modify original

Console.WriteLine(original);  // "Hello" - unchanged!
Console.WriteLine(copy);      // "World"

Why does this happen? Strings are immutableβ€”you can't change their content after creation. Operations like += or assignment create new string objects. So even though string is technically a reference type, you can't accidentally modify a shared string.

After: string copy = original;
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”             β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ original │────────────→│  "Hello"    β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜      β”Œβ”€β”€β”€β”€β”€β†’β”‚  (immutable)β”‚
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”      β”‚      β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β”‚   copy   β”‚β”€β”€β”€β”€β”€β”€β”˜
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

After: copy = "World";
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”             β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ original │────────────→│  "Hello"    β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜             β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”             β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚   copy   │────────────→│  "World"    β”‚  ← NEW object
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜             β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Arrays: Reference Types That Copy Elements πŸ“Š

Arrays are reference types, so assigning an array variable creates a shared reference:

int[] original = { 1, 2, 3 };
int[] shared = original;  // Shared reference

shared[0] = 99;  // Modifies the shared array

Console.WriteLine(original[0]);  // 99 - changed!

But the individual elements inside the array follow their own type's rules:

// Array of value types (int)
int[] numbers = { 10, 20, 30 };
int x = numbers[0];  // COPIES the value 10
x = 999;  // Does NOT change numbers[0]

// Array of reference types (StringBuilder)
StringBuilder[] builders = { new StringBuilder("A"), new StringBuilder("B") };
StringBuilder sb = builders[0];  // COPIES the reference
sb.Append("!");  // DOES change builders[0]

Detailed Examples πŸ”¬

Example 1: Value Type Independence

struct Point  // struct is a value type
{
    public int X;
    public int Y;
}

Point p1 = new Point { X = 10, Y = 20 };
Point p2 = p1;  // Entire struct is COPIED

p2.X = 99;

Console.WriteLine($"p1: ({p1.X}, {p1.Y})");  // (10, 20) - unchanged
Console.WriteLine($"p2: ({p2.X}, {p2.Y})");  // (99, 20)

Why this works: The Point struct is a value type. Assignment performs a memberwise copy of all fields. p1 and p2 are completely independent.

Memory layout:

VariableXY
p11020
p29920

Example 2: Reference Type Sharing

class Rectangle  // class is a reference type
{
    public int Width;
    public int Height;
}

Rectangle r1 = new Rectangle { Width = 100, Height = 50 };
Rectangle r2 = r1;  // Reference is COPIED, object is SHARED

r2.Width = 200;

Console.WriteLine($"r1: {r1.Width}x{r1.Height}");  // 200x50 - changed!
Console.WriteLine($"r2: {r2.Width}x{r2.Height}");  // 200x50

Why this works: The Rectangle class is a reference type. Both r1 and r2 hold references to the same object on the heap. Changes through either reference affect the shared object.

Critical point: To make an independent copy, you'd need to explicitly create a new object:

Rectangle r3 = new Rectangle { Width = r1.Width, Height = r1.Height };
r3.Width = 300;  // Only affects r3

Example 3: Method Parameter Passing

The copying vs. sharing distinction becomes crucial when passing arguments to methods:

void ModifyValue(int number)
{
    number = 999;  // Modifies the COPY
}

void ModifyReference(List<int> list)
{
    list.Add(999);  // Modifies the SHARED object
}

void ReplaceReference(List<int> list)
{
    list = new List<int> { 1, 2, 3 };  // Replaces the local COPY of the reference
}

int x = 42;
ModifyValue(x);
Console.WriteLine(x);  // 42 - unchanged

List<int> myList = new List<int> { 10, 20 };
ModifyReference(myList);
Console.WriteLine(myList.Count);  // 3 - modified!

ReplaceReference(myList);
Console.WriteLine(myList.Count);  // Still 3 - replacement didn't affect caller

What's happening:

  1. ModifyValue: The int parameter receives a copy of x. Changes to number don't affect x.
  2. ModifyReference: The List<int> parameter receives a copy of the reference. Both the parameter and myList point to the same object, so .Add() affects the shared list.
  3. ReplaceReference: Assigning new List<int> to list changes only the local parameter variable. The caller's myList still points to the original object.
ModifyReference scenario:

Caller:                   Method:                  Heap:
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”             β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”            β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  myList  │────────────→│   list   │───────────→│ List object β”‚
β”‚  (ref)   β”‚  passed     β”‚  (ref)   β”‚  both      β”‚ [10, 20]    β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  as copy    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  point to  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                       same object

After list.Add(999):
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”             β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”            β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  myList  │────────────→│   list   │───────────→│ List object β”‚
β”‚  (ref)   β”‚             β”‚  (ref)   β”‚            β”‚ [10,20,999] β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜             β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜            β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                                   ↑ Both see change

πŸ’‘ Use ref or out keywords if you want to pass the actual variable (not a copy) to a method, allowing the method to reassign it.

Example 4: Collections of References

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

List<Person> team1 = new List<Person>
{
    new Person { Name = "Alice", Age = 30 },
    new Person { Name = "Bob", Age = 25 }
};

List<Person> team2 = team1;  // Shared list reference
List<Person> team3 = new List<Person>(team1);  // NEW list, but elements are shared

// Modify through team2
team2.Add(new Person { Name = "Charlie", Age = 35 });
Console.WriteLine(team1.Count);  // 3 - team1 and team2 share the list
Console.WriteLine(team3.Count);  // 2 - team3 is independent list

// Modify an existing person
team3[0].Age = 31;
Console.WriteLine(team1[0].Age);  // 31 - ALL lists share the Person objects!

The subtle point: new List<Person>(team1) creates a shallow copy. The new list is independent, but the Person objects inside are still shared references.

After creating all three lists:

Stack:                          Heap:
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  team1   │──────────────────→│  List object   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜           β”Œβ”€β”€β”€β”€β”€β”€β”€β†’β”‚  [ref, ref]    β”‚
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”           β”‚        β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β”‚  team2   β”‚β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜               β”‚    β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                            β”‚    β”‚
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  team3   │──────────────────→│  List object   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                    β”‚  [ref, ref]    β”‚  ← Different list
                                β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                       β”‚    β”‚
                                       ↓    ↓
                                β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                                β”‚  Person objects  β”‚  ← Same objects!
                                β”‚  Alice, Bob      β”‚
                                β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

For a true deep copy, you'd need to create new Person objects:

List<Person> team4 = team1.Select(p => new Person 
{ 
    Name = p.Name, 
    Age = p.Age 
}).ToList();

Common Mistakes ⚠️

Mistake 1: Assuming Reference Assignment Creates a Copy

❌ WRONG ASSUMPTION:
List<string> backup = originalList;  // "I've saved a backup!"
originalList.Clear();  // Oops! backup is now empty too

βœ… CORRECT APPROACH:
List<string> backup = new List<string>(originalList);  // True copy
// Or: var backup = originalList.ToList();

Mistake 2: Modifying "Copies" of Reference Types

❌ DANGEROUS:
void ProcessData(Customer customer)
{
    customer.Balance -= 100;  // Modifies the original!
}

Customer alice = new Customer { Balance = 1000 };
ProcessData(alice);
// alice.Balance is now 900

βœ… SAFER (if you want independence):
void ProcessData(Customer customer)
{
    var copy = new Customer { Balance = customer.Balance };
    copy.Balance -= 100;
    return copy;  // Or work with the copy
}

Mistake 3: Forgetting Strings Are Immutable

❌ MISUNDERSTANDING:
string original = "Hello";
string modified = original;
modified.ToUpper();  // Does NOT change 'modified'!
Console.WriteLine(modified);  // Still "Hello"

βœ… CORRECT:
string original = "Hello";
string modified = original.ToUpper();  // ToUpper() RETURNS a new string
Console.WriteLine(modified);  // "HELLO"

Mistake 4: Confusion with ref and out Parameters

// WITHOUT ref - copies the reference
void FailToReplace(List<int> list)
{
    list = new List<int> { 99 };  // Only changes local parameter
}

List<int> myList = new List<int> { 1, 2, 3 };
FailToReplace(myList);
Console.WriteLine(myList.Count);  // Still 3

// WITH ref - passes actual variable
void SuccessfullyReplace(ref List<int> list)
{
    list = new List<int> { 99 };  // Changes caller's variable
}

SuccessfullyReplace(ref myList);
Console.WriteLine(myList.Count);  // 1

Mistake 5: Shallow vs. Deep Copy Confusion

class Node
{
    public int Value { get; set; }
    public Node Next { get; set; }
}

❌ SHALLOW COPY (doesn't copy the linked structure):
Node copy = new Node { Value = original.Value, Next = original.Next };
copy.Next.Value = 999;  // Affects original's linked node!

βœ… DEEP COPY (recursive):
Node DeepCopy(Node node)
{
    if (node == null) return null;
    return new Node 
    { 
        Value = node.Value, 
        Next = DeepCopy(node.Next)  // Recursively copy
    };
}

Key Takeaways 🎯

πŸ“‹ Quick Reference Card

Concept Value Types Reference Types
Examples int, double, bool, struct, enum class, interface, delegate, arrays, string
Assignment behavior Copies the entire value Copies the reference (address)
Independence Always independent after assignment Share the same object
Method parameters Receives a copy of the value Receives a copy of the reference
Memory location Typically stack (or inline) Reference on stack, object on heap
Making a true copy Just assign: copy = original; Must create new object explicitly
Null possibility No (unless Nullable<T>) Yes, can be null

🧠 Memory Mnemonics

  • "Value types are like PHOTOCOPIES" - Each copy is completely independent. Mark up one photocopy, the original stays clean.
  • "Reference types are like WEBSITE LINKS" - Multiple links can point to the same website. Change the website, everyone sees the change.
  • "String is the REBEL" - Technically a reference type, but immutability makes it behave like a value type.

When to Use Each Approach πŸ€”

Use value types (structs) when:

  • Data is small (≀16 bytes recommended)
  • You want copy semantics by default
  • The type represents a single value conceptually (Point, Color, etc.)
  • You want to avoid heap allocations

Use reference types (classes) when:

  • Data is large or variable-sized
  • You want to share object state
  • You need inheritance or polymorphism
  • Identity matters (two objects with same data should be distinguishable)

Creating Copies of Reference Types πŸ”§

Shallow copy methods:

// Collections
var listCopy = new List<T>(original);
var arrayCopy = original.ToArray();

// Custom objects - implement ICloneable or custom method
public Customer ShallowCopy()
{
    return (Customer)this.MemberwiseClone();  // Built-in shallow copy
}

Deep copy approaches:

// Manual (tedious but explicit)
public Customer DeepCopy()
{
    return new Customer
    {
        Name = this.Name,  // string is safe (immutable)
        Address = this.Address.DeepCopy(),  // Recursive
        Orders = this.Orders.Select(o => o.DeepCopy()).ToList()
    };
}

// Serialization-based (easy but slower)
public T DeepCopy<T>(T obj)
{
    var json = JsonSerializer.Serialize(obj);
    return JsonSerializer.Deserialize<T>(json);
}

Try This! πŸ”§

Exercise 1: Predict the output:

struct ValuePoint { public int X, Y; }
class RefPoint { public int X, Y; }

ValuePoint vp1 = new ValuePoint { X = 1, Y = 2 };
ValuePoint vp2 = vp1;
vp2.X = 99;
Console.WriteLine(vp1.X);  // ?

RefPoint rp1 = new RefPoint { X = 1, Y = 2 };
RefPoint rp2 = rp1;
rp2.X = 99;
Console.WriteLine(rp1.X);  // ?
Click to reveal answer
  • vp1.X is 1 (value type copied, independent)
  • rp1.X is 99 (reference type shared)

Exercise 2: Fix this bug:

List<int> ProcessNumbers(List<int> numbers)
{
    numbers.Sort();  // Oops! This modifies the caller's list
    return numbers;
}
Click to reveal solution
List<int> ProcessNumbers(List<int> numbers)
{
    var copy = new List<int>(numbers);  // Make a copy first
    copy.Sort();
    return copy;
}
// Or use LINQ: return numbers.OrderBy(n => n).ToList();

πŸ“š Further Study

  1. Microsoft Docs - Value Types: https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/value-types
  2. Microsoft Docs - Reference Types: https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/reference-types
  3. Stack vs Heap Memory: https://learn.microsoft.com/en-us/dotnet/standard/automatic-memory-management

πŸŽ“ You now understand the fundamental distinction between copying and sharing in C#! This knowledge is critical for reasoning about your program's behavior, avoiding bugs, and making informed decisions about value vs. reference types. Keep these principles in mind every time you write an assignment statement or pass a parameter to a method.