Think of it this way.
An employee is definitely a person, so the compiler knows that
person1 = emp1
will always be valid, no matter what. However, not all people are
employees. Any given person _might_ be an employee, but then again they
might not. So, something like
emp1 = person1
may or may not be valid, depending upon what the reference in person1
points to. This is why the compiler requires an explicit cast:
emp1 = (employee)person1;
This is your promise to the compiler that yes, you know what you're
doing, and person1 points to a person that was created like this:
person1 = new employee
or assigned from an employee:
person1 = anEmployee
These two lines of code are perfectly legal because, again, employees
are always people.
In all of this, it's important to keep a clear distinction between
compile-time and run-time. At compile time, the compiler cannot tell
what kind of object "person1" is referring to. All it knows is that it
must be a "person" or any of the classes that inherit from "person".
So, when you say
emp1 = (employee)person1;
the compiler builds in code to check _for sure_ at run-time that the
thing that person1 refers to really is an employee (or some class
derived from employee). If it isn't, then the CLR generates an
InvalidCastException exception.
However, as I said, this is at run-time. At run-time, your program is
running with real, created objects. Actual "person" and "employee"
objects with memory assigned to them and all. Once you create an object
of a particular type, _it is that type for its whole existence_. You
_cannot_ change what type of thing it is.
What this means is that when you say:
person1 = emp1;
you are _not_ "changing the employee into a person" as many people seem
to think. You are just changing a variable of type "person" to point to
a specific kind of person: a particular employee. The employee object
itself never changes what it is.
This is where overloading comes in: if you say this:
person1 = emp1;
person1.SayHello();
does _not necessarily_ invoke the SayHello() method in the "person"
class. Because person1 points to an employee object (and it never
changes what it is at run-time), this call will invoke whatever the
employee class says the SayHello() method should do. If "employee"
doesn't define a SayHello, then maybe it falls back to the person
(parent) class, but if it does define a SayHello of its own (using
"override") then _that_ is the method that will be invoked.
Just because there is a "person" variable pointing to the object does
not change what it is or what methods are called on it. (There is an
exception to this: methods declared as "new", but don't worry about it
for now.