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 Types | Assignment copies the actual data |
| Reference Types | Assignment 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: Listoriginal = 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:
| Variable | X | Y |
|---|---|---|
| p1 | 10 | 20 |
| p2 | 99 | 20 |
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:
- ModifyValue: The
intparameter receives a copy ofx. Changes tonumberdon't affectx. - ModifyReference: The
List<int>parameter receives a copy of the reference. Both the parameter andmyListpoint to the same object, so.Add()affects the shared list. - ReplaceReference: Assigning
new List<int>tolistchanges only the local parameter variable. The caller'smyListstill 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.Xis 1 (value type copied, independent)rp1.Xis 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
- Microsoft Docs - Value Types: https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/value-types
- Microsoft Docs - Reference Types: https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/reference-types
- 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.