How to bypass upcasting and achieve polymorphism without virtual and override in C#

82 Views Asked by At

I have three classes called Animal, Cat and Dog where Cat and Dog inherit from Animal:

public class Animal
{
    public void Talk()
    {
        Console.WriteLine("Parent");
    }
}

public class Cat : Animal
{
    public new void Talk()
    {
        Console.WriteLine("Child(Cat)");
    }
}

public class Dog : Animal
{
    public new void Talk()
    {
        Console.WriteLine("Child(Dog)");
    }
}

I have another method in another class that calls the Talk() method on an animal:

public static Animal FirstKindOfTalk(Animal animal)
    {
        if (animal.GetType() == typeof(Cat))
        {
            var changed = animal as Cat;

            if (changed is not null)
                changed.Talk();

            return changed;
        }
        else if (animal.GetType() == typeof(Dog))
        {
            var changed = animal as Dog;

            if (changed is not null)
                changed.Talk();

            return changed;
        }
            
        return animal;
    }

FirstKindOfTalk(new Animal()); => Parent gets printed in the console. FirstKindOfTalk(new Cat()); => Child(Cat) gets printed in the console. FirstKindOfTalk(new Dog()); => Child(Dog) gets printed in the console.

I have to explicitly cast the parameter to their own type (Cat or Dog) due to Upcasting, in order to be able to run the specific implementation of those classes from the method Talk(). This is because I'm not using virtual and override in my code, if I used them, then the runtime would run the implementation of the passed in parameter from the method.

The Problem I prefer to write the method as the following:

public static Animal SecondKindOfTalk(Animal animal)
    {
        var changed = animal;
        if (animal.GetType() == typeof(Cat))
        {
            changed = animal as Cat;

            changed.Talk();

            return changed;
        }
        else if (animal.GetType() == typeof(Dog))
        {
            changed = animal as Dog;

            changed.Talk();

            return changed;
        }

        return changed;
    }

FirstKindOfTalk(new Animal()); => Parent gets printed in the console. FirstKindOfTalk(new Cat()); => Parent gets printed in the console. FirstKindOfTalk(new Dog()); => Parent gets printed in the console.

Can someone explain to me, what is causing this behavior?

I know using virtual and override is the better way, but I have to work on an existing project where methods from the base class aren't defined as virtual, and I cannot change them either.

2

There are 2 best solutions below

0
Jon Skeet On

Can someone explain to me, what is causing this behavior?

Yes - the compile-time type of changed in SecondKindOfTalk is still Animal , so you're still calling Animal.Talk() regardless of the execution-time type.

It would be simpler to use pattern matching in a switch statement:

public static Animal Task(Animal animal)
{
    switch (animal)
    {
        case Cat cat:
            cat.Talk();
            break;
        case Dog dog:
            dog.Talk();
            break;
    }
    return animal;
}

Or potentially even a switch expression and a delegate (which is slightly less performant, admittedly):

public static Animal Talk(Animal animal)
{
    Action action = animal switch
    {
        Cat cat => cat.Talk,
        Dog dog => dog.Talk,
        _ => null
    };
    action?.Invoke();
    return animal;
}
0
Guru Stron On

Can someone explain to me, what is causing this behavior?

Usage of var which is just an implicitly-typed local variables declaration, i.e.

public static Animal SecondKindOfTalk(Animal animal)
{
    var changed = animal;

is the same as (var is handled by compiler during compile time)

public static Animal SecondKindOfTalk(Animal animal)
{
    Animal changed = animal;

And the following handling does not change the variable type, so the Animal.Talk will be used.

Check out the type-testing operators and cast expressions doc for some patterns which can be used, like pattern matching:

public static Animal SecondKindOfTalk(Animal animal)
{
    if (animal is Cat cat)
    {
        cat.Talk();
    }
    else if (animal is Dog dog)
    {
        dog.Talk();
    }

    return animal;
}

P.S.

Note that changed variable name and attempt to return it kind of imposes some misunderstanding here. No actual change is done to the passed animal, it remains the same instance, i.e. object.ReferenceEquals(changed, animal) is true.