You must make a distinction between the static type of a variable (the type known at compile time) and the run time type of an object reference assigned to a variable.
The static type of p
is Parent
in both cases because it is declared as Parent p
. No matter what you assign it. You may even assign it null
.
The run time type of the value of p
after the assignment is Child
in the first case and Parent
in the second case. It would be undetermined if p
was null
.
It is okay to assign a Child
to a Parent
, because the Child
class derives from Parent
. A Child
is therefore a Parent
(in OO terms).
However, a Parent
is not a Child
, because it does not derive from it. Therefore, in the second case, you cannot cast p
to Child
. In the first case the cast is valid, because you cast the object to its actual run time type. It tells the compiler, I know that this object assigned to a Parent
variable is a Child
, so, please, treat it as a Child
. This does not involve any conversion; however, a runtime check will be performed, possibly throwing an exception, if the cast was not allowed.
You were asked to explain it from point of view of stack and heap memory. I'm sorry to say, but this has absolutely nothing to do with stack or heap memory. It has to do with inheritance rules and assignment compatibility.
Of course, here we are dealing with reference types (classes). This allows us to derive Child
from Parent
. This would not be possible with value types (structs). The point is reference type versus value type, not heap versus stack, which is an implementation detail. And btw., a value type (struct) field in a class will also be stored on the heap.
See also: The Stack Is An Implementation Detail, Part One and Two (Eric Lippert's blog)
You did not ask for it but casting to a derived type is mostly preceded by a type test, because you usually do not know the run time type in advance. Let us say that the Child
class adds a new method DriveParentsToDespair()
(only children can do this). Then you could use Type testing with pattern matching to test the type and assign it to a new variable at the same time:
if (p is Child child) {
child.DriveParentsToDespair();
}
This avoids an exception in case p
has a run time type of Parent
.