Why reference types inside structs behave like value types?

The best way to understand this is to fully understand what variables are; variables are, simply put, placeholders that hold values.

So what exactly is this value? In a reference type, the value stored in the variable is the reference (the address so to speak) to a given object. In a value type, the value is the object itself.

When you do AnyType y = x; what really happens is that a copy of the value stored in x is made and is then stored in y.

So if x is a reference type, both x and y will point to the same object because they will both hold identical copies of the same reference. If x is a value type then both x and y will hold two identical but distinct objects.

Once you understand this, it should start to make sense why your code behaves the way it does. Lets study it step by step:

Person person_1 = new Person();

Ok we are creating a new instance of a value type. According to what I explained previously, the value stores in person_1 is the newly created object itself. Where this value is stored (heap or stack) is an implementation detail, its not relevant at all to how your code behaves.

person_1.name = "Person 1";

Now we are setting the variable name which happens to be a field of person_1. Again according to previous explanations, the value of name is a reference pointing to somewhere in memory where the string "Person 1" is stored. Again, where the value or the string are stored is irrelevant.

Person person_2 = person_1;

Ok, this is the interesting part. What happens here? Well, a copy of the value stored in person_1 is made and stored in person_2. Because the value happens to be an instance of a value type, a new copy of said instance is created and stored in person_2. This new copy has its own field name and the value stored in this variable is, again, a copy of the value stored in person_1.name (a reference to "Person 1").

person_2.name = "Person 2";

Now we are simply reassigning the variable person_2.name. This means we are storing a new reference that points to a new string somewhere in memory. Do note, that person_2.name originally held a copy of the value stored in person_1.name so whatever you do to person_2.name has no effect on whatever value is stored in person_1.name because you are simply changing... yeah exactly, a copy. And thats why your code behaves the way it does.

As an exercise, try to reason out in a similar way how your code would behave if Person were a reference type.


strings are reference types that have pointers stored on stack while their actual contents stored on heap

No no no. First off, stop thinking about stack and heap. This is almost always the wrong way to think in C#. C# manages storage lifetime for you.

Second, though references may be implemented as pointers, references are not logically pointers. References are references. C# has both references and pointers. Don't mix them up. There is no pointer to string in C#, ever. There are references to string.

Third, a reference to a string could be stored on the stack but it could also be stored on the heap. When you have an array of references to string, the array contents are on the heap.

Now let's come to your actual question.

    Person person_1 = new Person();
    person_1.name = "Person 1";
    Person person_2 = person_1; // This is the interesting line
    person_2.name = "Person 2";

Let's illustrate what the code does logically. Your Person struct is nothing more than a string reference, so your program is the same as:

string person_1_name = null; // That's what new does on a struct
person_1_name = "Person 1";
string person_2_name = person_1_name; // Now they refer to the same string
person_2_name = "Person 2"; // And now they refer to different strings

When you say person2 = person1 that does not mean that the variable person1 is now an alias for the variable person2. (There is a way to do that in C#, but this is not it.) It means "copy the contents of person1 to person2". The reference to the string is the value that is copied.

If that's not clear try drawing boxes for variables and arrows for references; when the struct is copied, a copy of the arrow is made, not a copy of the box.