C# Objects, References, Pass-by-Value, ref, and Garbage Collection
C# object references can seem confusing because several different things are happening at once: you can change an object through a method parameter, you cannot normally replace the caller's variable from that method, you can reassign a local variable directly, and the old object may later be reclaimed by the garbage collector.
The rule that explains almost everything is this:
C# passes parameters by value by default.
For a class, the value being copied is the reference to the object, not the object itself.
Start With a Simple Class
class Person
{
public string Name { get; set; }
}
Now create an instance:
var a = new Person { Name = "Bob" };
Conceptually:
a ───────────────► Person object
Name = "Bob"
The variable a does not contain the whole Person. It contains a reference to that object.
Direct Reassignment Works Because You Are Changing the Actual Variable
Consider:
var a = new Person { Name = "Bob" };
Console.WriteLine(a.Name);
a = new Person { Name = "Sarah" };
Console.WriteLine(a.Name);
The output is:
Bob
Sarah
Why does this work? Because there is no method parameter involved. You are reassigning the actual variable a.
Initially:
a ───────────────► Person("Bob")
Then this runs:
a = new Person { Name = "Sarah" };
A new object is created and the variable a is changed to point at it:
a ───────────────► Person("Sarah")
Person("Bob")
There is no copied parameter here. You are assigning directly to a, so a itself changes.
What Happens to the Old Bob Object?
After a is changed to point to the new Sarah object, the old Bob object still exists for the moment, but a no longer points to it.
a ───────────────► Person("Sarah")
Person("Bob") ← no longer referenced by a
If no other reachable variable or object refers to the Bob object, it becomes eligible for garbage collection.
Eligible for garbage collection does not mean "deleted immediately". It means the .NET garbage collector is now free to reclaim that object's memory when a collection occurs.
The lifecycle is roughly:
Create Person("Bob")
↓
a points to Bob
↓
Create Person("Sarah")
↓
a is reassigned to Sarah
↓
Bob has no reachable references
↓
Bob is eligible for garbage collection
↓
GC eventually reclaims Bob's memory
What If Another Reference Still Points to Bob?
var a = new Person { Name = "Bob" };
var b = a;
a = new Person { Name = "Sarah" };
Console.WriteLine(b.Name);
The output is still:
Bob
Before reassignment:
a ──────┐
▼
Person("Bob")
▲
b ───────┘
After reassigning a:
a ───────────────► Person("Sarah")
b ───────────────► Person("Bob")
The Bob object is still reachable through b, so it is not eligible for garbage collection.
Now Compare This With Passing the Object to a Method
Suppose we do this:
static void Change(Person p)
{
p.Name = "David";
}
static void Main()
{
var a = new Person { Name = "Bob" };
Change(a);
Console.WriteLine(a.Name);
}
The output is:
David
This works because when Change(a) is called, the reference stored in a is copied into p.
Main a ───────┐
│
▼
Person object
Name = "Bob"
▲
│
Change p ────────┘
There are two separate variables, but both contain references to the same object.
The reference is copied. The object is not copied.
Why p.Name = "David" Changes the Original Object
static void Change(Person p)
{
p.Name = "David";
}
The statement p.Name = "David" follows the reference stored in p and changes the object at the other end.
Since a and p point at the same object, both observe the change:
Main a ───────┐
│
▼
Person object
Name = "David"
▲
│
Change p ────────┘
Why Replacing the Parameter Does Not Change the Caller
Now compare:
static void Replace(Person p)
{
p = new Person { Name = "Tiffany" };
}
When the method begins:
Main a ───────┐
│
▼
Person("Bob")
▲
│
Replace p ────────┘
Then this line runs:
p = new Person { Name = "Tiffany" };
That line does not change the caller's variable. It changes only the local parameter variable p.
Main a ─────────────► Person("Bob")
Replace p ─────────────► Person("Tiffany")
The variable in Main still points at Bob. The method parameter now points at Tiffany.
When the method returns, the local variable p disappears.
After Reassigning the Parameter, You Have Lost the Original Through That Parameter
Consider:
static void Replace(Person p)
{
p = new Person { Name = "Tiffany" };
p.Name = "David";
}
Once p has been reassigned, it no longer points at the original object. So p.Name = "David" changes the new object.
Caller a ─────────────► Person("Bob")
Method p ─────────────► Person("David")
Unless you kept another reference to the original object, you can no longer access that original through p.
Keeping the Original Reference
static void Replace(Person p)
{
var original = p;
p = new Person { Name = "Tiffany" };
original.Name = "David";
}
Now:
Caller a ───────┐
▼
Person("David")
▲
original ──────┘
Method p ───────────► Person("Tiffany")
The caller sees "David" because original still points to the original object.
Why C# Behaves This Way
The behaviour follows one general design rule rather than a special rule for objects: ordinary parameters receive copies of values.
For an integer:
static void ChangeNumber(int x)
{
x = 20;
}
int number = 10;
ChangeNumber(number);
Console.WriteLine(number);
The result is still:
10
The method got a copy of the integer value.
With a class, the exact same idea applies:
Person a = new Person();
The value stored in a is a reference. Passing a to a normal parameter copies that reference value.
A reference type is not the same thing as pass-by-reference. A class is a reference type, but a normal parameter such as Person p is still passed by value.
Why This Design Is Useful
Imagine normal object parameters were automatically passed by reference.
This innocent-looking call:
ProcessCustomer(customer);
could silently do:
static void ProcessCustomer(Person customer)
{
customer = null;
}
and wipe out the caller's variable. Or:
static void ProcessCustomer(Person customer)
{
customer = new Person { Name = "Someone Else" };
}
could silently replace the caller's reference.
C# avoids that by making ordinary parameters work on a copy of the reference. A method can still mutate the object through that reference, but it cannot normally replace the caller's variable.
Using ref When You Really Want to Replace the Caller's Variable
If replacing the caller's reference is intentional, use ref:
static void Replace(ref Person p)
{
p = new Person { Name = "Tiffany" };
}
and call it like this:
Replace(ref a);
The ref keyword is required in both places. Now the method can update the caller's variable itself:
Replace parameter
│
▼
caller variable a ─────────► Person object
So this:
p = new Person { Name = "Tiffany" };
changes what the caller's a points to.
Why ref Appears at the Call Site Too
C# requires this:
Replace(ref a);
rather than:
Replace(a);
because it makes the potentially important side effect visible to the caller.
Seeing ref a tells you immediately: "This method is allowed to change the actual variable a, not merely work with the object it currently references."
Direct Reassignment Versus Method Reassignment
| Code | What is being changed? | Effect |
|---|---|---|
a.Name = "David"; |
The object | The object changes |
a = new Person(); |
The actual local variable a |
a points to a new object |
void M(Person p) { p.Name = "David"; } |
The shared object | The caller sees the change |
void M(Person p) { p = new Person(); } |
The copied parameter variable | The caller's reference does not change |
void M(ref Person p) { p = new Person(); } |
The caller's actual variable | The caller's reference changes |
A Useful Analogy
Think of an object as a house and a reference as the house address written on a piece of paper.
When you pass an object normally to a method, C# gives the method a photocopy of the address. Both pieces of paper lead to the same house.
If the method goes to the house and paints the door:
p.Name = "David";
everyone sees the changed house.
If the method erases the address on its photocopy and writes down a different address:
p = new Person();
your original piece of paper has not changed.
Using ref is like allowing the method to alter your original piece of paper.
Garbage Collection Fits Naturally Into This Model
Objects do not disappear just because one variable stops pointing at them. They become collectable only when they are no longer reachable through any live reference.
Example:
var a = new Person { Name = "Bob" };
a = new Person { Name = "Sarah" };
If nothing else refers to Bob:
a ─────────────► Person("Sarah")
Person("Bob")
no reachable references
↓
eligible for GC
But:
var a = new Person { Name = "Bob" };
var b = a;
a = new Person { Name = "Sarah" };
leaves Bob alive:
a ─────────────► Person("Sarah")
b ─────────────► Person("Bob")
because b still provides a reachable reference.
Does Garbage Collection Happen Immediately?
No.
The .NET runtime decides when to perform garbage collection based on memory pressure and other runtime conditions. An unreachable object may remain in memory for some time before its storage is reclaimed.
You should generally not write normal application logic that depends on knowing exactly when garbage collection occurs.
What About the String "Bob"?
The old Person object can become eligible for collection, but string literals such as "Bob" are normally interned by .NET. That means the literal itself is typically retained through the runtime's string intern mechanism even after the old Person object is no longer reachable.
Returning a New Object Is Often Clearer Than ref
If a method's job is to create a replacement object, returning it is often clearer:
static Person Replace(Person p)
{
return new Person { Name = "Tiffany" };
}
var a = new Person { Name = "Bob" };
a = Replace(a);
At the call site, it is obvious that a is being reassigned.
Complete Demonstration
using System;
public class Program
{
class Person
{
public string Name { get; set; }
}
static void Change(Person p)
{
p.Name = "David";
}
static void Replace(Person p)
{
p = new Person { Name = "Tiffany" };
}
static void ReplaceWithRef(ref Person p)
{
p = new Person { Name = "Sarah" };
}
static void Main()
{
var a = new Person { Name = "Bob" };
Console.WriteLine(a.Name); // Bob
a = new Person { Name = "Tim" };
Console.WriteLine(a.Name); // Tim
Change(a);
Console.WriteLine(a.Name); // David
Replace(a);
Console.WriteLine(a.Name); // David
ReplaceWithRef(ref a);
Console.WriteLine(a.Name); // Sarah
}
}
Final Mental Model
- A variable of a class type contains a reference to an object.
- Assigning a new object directly to that variable changes the variable itself.
- Passing the variable to a normal method copies the reference.
- The copied reference still reaches the same object, so object properties can be changed.
- Reassigning the copied parameter only changes the local parameter.
- Using
reflets the method change the caller's actual variable. - When an object has no reachable references, it becomes eligible for garbage collection.
- Garbage collection happens later, when the .NET runtime decides it is appropriate.
a.Name = "David" changes the object.
a = new Person() changes the actual local variable.
p = new Person() inside a normal method changes only the copied parameter.
ref Person p allows the method to change the caller's variable.
An old object with no reachable references becomes eligible for garbage collection.

















