Search code examples
c#polymorphismcovariance

Why does setting a base class equal to a derived type only work within scope of where it is set?


I found it difficult to come up with a descriptive enough title for this scenario so I'll let the code do most of the talking.

Consider covariance where you can substitute a derived type for a base class.

class Base
{

}

class Derived : Base
{

}

Passing in typeof(Base) to this method and setting that variable to the derived type is possible.

private void TryChangeType(Base instance)
{
  var d = new Derived();
  instance = d;
  Console.WriteLine(instance.GetType().ToString());
}

However, when checking the type from the caller of the above function, the instance will still be of type Base

private void CallChangeType()
{
  var b = new Base();
  TryChangeType(b);
  Console.WriteLine(b.GetType().ToString());
}  

I would assume since objects are inherently reference by nature that the caller variable would now be of type Derived. The only way to get the caller to be type Derived is to pass a reference object by ref like so

private void CallChangeTypeByReference()
{
  var b = new Base();
  TryChangeTypeByReference(ref b);
  Console.WriteLine(b.GetType().ToString());
}  
private void TryChangeTypeByReference(ref Base instance)
{
  var d = new Derived();
  instance = d;
}

Further more, I feel like it's common knowledge that passing in an object to a method, editing props, and passing that object down the stack will keep the changes made down the stack. This makes sense as the object is a reference object.

What causes an object to permanently change type down the stack, only if it's passed in by reference?


Solution

  • You have a great many confused and false beliefs. Let's fix that.

    Consider covariance where you can substitute a derived type for a base class.

    That is not covariance. That is assignment compatibility. An Apple is assignment compatible with a variable of type Fruit because you can assign an Apple to such a variable. Again, that is not covariance. Covariance is the fact that a transformation on a type preserves the assignment compatibility relationship. A sequence of apples can be used somewhere that a sequence of fruit is needed because apples are a kind of fruit. That is covariance. The mapping "apple --> sequence of apples, fruit --> sequence of fruit" is a covariant mapping.

    Moving on.

    Passing in typeof(Base) to this method and setting that variable to the derived type is possible.

    You are confusing types with instances. You do not pass typeof(Base) to this method; you pass a reference to Base to this instance. typeof(Base) is of type System.Type.

    As you correctly note, formal parameters are variables. A formal parameter is a new variable, and it is initialized to the actual parameter aka argument.

    However, when checking the type from the caller of the above function, the instance will still be of type Base

    Correct. The argument is of type Base. You copy that to a variable, and then you reassign the variable. This is no different than saying:

    Base x = new Base();
    Base y = x;
    y = new Derived();
    

    And now x is still Base and y is Derived. You assigned the same variable twice; the second assignment wins. This is no different than if you said a = 1; b = a; b = 2; -- you would not expect a to be 2 afterwards just because you said b = a in the past.

    I would assume since objects are inherently reference by nature that the caller variable would now be of type Derived.

    That assumption is wrong. Again, you have made two assignments to the same variable, and you have two variables, one in the caller, and one in the callee. Variables contain values; references to objects are values.

    The only way to get the caller to be type Derived is to pass a reference object by ref like so

    Now we're getting to the crux of the problem.

    The correct way to think about this is that ref makes an alias to a variable. A normal formal parameter is a new variable. A ref formal parameter makes the variable in the formal parameter an alias to the variable at the call site. So now you have one variable but it has two names, because the name of the formal parameter is an alias for the variable at the call. This is the same as:

    Base x = new Base();
    ref Base y = ref x; // x and y are now two names for the same variable
    y = new Derived(); // this assigns both x and y because there is only one variable, with two names
    

    Further more, I feel like it's common knowledge that passing in an object to a method, editing props, and passing that object down the stack will keep the changes made down the stack. This makes sense as the object is a reference object.

    Correct.

    The mistake you are making here is very common. It was a bad idea for the C# design team to name the variable aliasing feature "ref" because this causes confusion. A reference to a variable makes an alias; it gives another name to a variable. A reference to an object is a token that represents a specific object with a specific identity. When you mix the two it gets confusing.

    The normal thing to do is to not pass variables by ref particularly if they contain references.

    What causes an object to permanently change type down the stack, only if it's passed in by reference?

    Now we have the most fundamental confusion. You have confused objects with variables. An object never changes its type, ever! An apple is an object, and an apple is now and forever an apple. An apple never becomes any other kind of fruit.

    Stop thinking that variables are objects, right now. Your life will get so much better. Internalize these rules:

    • variables are storage locations that store values
    • references to objects are values
    • objects have a type that never changes
    • ref gives a new name to an existing variable
    • assigning to a variable changes its value

    Now if we ask your question again using correct terminology, the confusion disappears immediately:

    What causes the value of a variable to change its type down the stack, only if it's passed in by ref?

    The answer is now very clear:

    • A variable passed by ref is an alias to another variable, so changing the value of the parameter is the same as changing the value of the variable at the call site
    • Assigning an object reference to a variable changes the value of that variable
    • An object has a particular type

    If we don't pass by ref but instead pass normally:

    • A value passed normally is copied to a new variable, the formal parameter
    • We now have two variables with no connection; changing one of them does not change the other.

    If that's still not clear, start drawing boxes, circles and arrows on a whiteboard, where objects are circles, variables are boxes, and object references are arrows from variables to objects. Making an alias via ref gives a new name to an existing circle; calling without ref makes a second circle and copies the arrow. It'll all make sense then.