I find it most helpful to think of reference types as holding object IDs. If one has a variable of class type Car
, the statement myCar = new Car();
asks the system to create a new car and report its ID (let's say it's object #57); it then puts "object #57" into variable myCar
. If one writes Car2 = myCar;
, that writes "object #57" into variable Car2. If one writes car2.Color = blue;
, that instructs the system to find the car identified by Car2 (e.g. object #57) and paint it blue.
The only operations which are performed directly on object ID's are creation of a new object and getting the ID, getting a "blank" id (i.e. null), copying an object ID to a variable or storage location that can hold it, checking whether two object IDs match (refer to the same object). All other requests ask the system to find the object referred to by an ID and act upon that object (without affecting the variable or other entity that held the ID).
In existing implementations of .NET, object variables are likely to hold pointers to objects stored on a garbage-collected heap, but that's an unhelpful implementation detail because there's a critical difference between an object reference and any other kind of pointer. A pointer is generally assumed to represent the location of something which will stay put long enough to be worked with. Object references don't. A piece of code may load the SI register with a reference to an object located at address 0x12345678, start using it, and then be interrupted while the garbage collector moves the object to address 0x23456789. That would sound like a disaster, but the garbage will examine the metadata associated with the code, observe that the code used SI to hold the address of the object it was using (i.e. 0x12345678), determine that object that was at 0x12345678 had been moved to 0x23456789, and update SI to hold 0x23456789 before it returned. Note that in that scenario, the numerical value stored in SI was changed by the garbage collector, but it referred to the same object before the move and afterward. If before the move it referred to the 23,592nd object created since program startup, it will continue to do so afterward. Interestingly, .NET does not store any unique and immutable identifier for most objects; given two snapshots of a program's memory, it will not always be possible to tell whether any particular object in the first snapshot exists in the second, or if all traces to it have been abandoned and a new object created that happens to look like it in all observable details.