So Foo
is a class
type, in particular a reference type. And you have:
var f = new Foo();
so the value of f
is really a reference to the actual object. And as you say, we may think of the reference as an integer, say 0x2A53BC
(just to make it concrete, we do not really have access to the integer or whatever the reference really is; there is no pointer arithmetic with these references).
Now, when you pass by value, as with the overload:
void Test(Foo g) { g = null; }
which we call with Test(f);
, the value 0x2A53BC
will be copied to a new storage location (a new place on the stack), and that new storage location becomes g
. If you had used g
to mutate the existing instance, then since both storage locations hold identical information 0x2A53BC
, you would have mutated the instance pointed to also by f
. But instead you choose to assign to g
. That leads to a the new storage location for g
holding now the reference 0x000000
(we simply assume that zero is used as the representation for null
). It clearly will not change the storage location for f
which still points to the original instance located at 0x2A53BC
.
In contrast, with the other overload:
void Test(ref Foo g) { g = null; }
which we call with Test(ref f);
, we pass by reference. So the same storage location is used for g
as we already had for f
. The value 0x2A53BC
is not copied. When you assign g = null;
, that is the original 0x2A53BC
being overwritten with 0x000000
(the magic representation of null
). When the method returns, yes even before it returns, f
has changed its value to point to the new "place" 0x000000
.
To sum up: The "value" of a reference type is the reference. When passed by value, a copy of the reference "number" is made and used by the invoked method. When passed by reference, the value is not copied, the same storage location is used by the callee method and the caller method.
Addition: Some quotes from the C# spec:
5.1.4 Value parameters
A parameter declared without a ref
or out
modifier is a
value parameter.
A value parameter comes into existence
upon invocation of the function member (method, instance constructor,
accessor, or operator) or anonymous function to which the parameter
belongs, and is initialized with the value of the argument given in
the invocation. A value parameter normally ceases to exist upon return
of the function member or anonymous function. [...]
5.1.5 Reference parameters
A parameter declared with a ref
modifier is a reference parameter.
A
reference parameter does not create a new storage location. Instead, a
reference parameter represents the same storage location as the
variable given as the argument in the function member or anonymous
function invocation. Thus, the value of a reference parameter is
always the same as the underlying variable. [...]