Polymorphism with Base Classes

A base class variable or list can hold any derived object, and calling a virtual method runs each object's own override. Use is to reach a derived class's own members.

Journeyman · C# · OOP · Updated

After this lesson you can

  • Hold derived objects in a base class variable, parameter or list
  • Predict which override runs when a virtual method is called through a base class variable
  • Check an object's type with is before using a derived class's own members

Key takeaways

  • A Character variable, parameter or List<Character> can hold any derived object, such as an Archer. This is upcasting, and it needs no conversion.
  • Calling a virtual method runs the object's own override, whatever the variable's type: an archer held as a Character still answers "Rain of arrows".
  • Through a Character variable you can use only what every character has, so calling an archer's AddArrow() on it won't compile.
  • member is Archer archer checks the object's kind and, if it matches, gives you an Archer variable. Use is sparingly; most of that work belongs in an override.
  • A method that hides the base version with new instead of override doesn't take part in polymorphism: through a base class variable, the base version runs.

Here's polymorphism in C#. Every class in this lesson builds on this Character, with overrides in Archer and Wizard.

A base class variable holds any derived object

An archer is a character, so an Archer object can go in a Character variable:

Character hero = new Archer("Robin");
Console.WriteLine(quot;{hero.Name}: {hero.SpecialAbility()}");

public class Character
{
    public string Name { get; }

    public Character(string name)
    {
        Name = name;
    }

    public virtual string SpecialAbility()
    {
        return "None";
    }
}

public class Archer : Character
{
    public Archer(string name) : base(name)
    {
    }

    public override string SpecialAbility()
    {
        return "Rain of arrows";
    }
}
Robin: Rain of arrows

The variable's type is Character, but the object is an Archer, and a virtual method always runs the object's own override. So hero.SpecialAbility() runs Archer's version: Rain of arrows.

Holding a derived object as its base class is called upcasting. It's always allowed, with no conversion to write, because an archer always is a character.

One list, many kinds

The real payoff comes with lists. A List<Character> can hold every kind of character at once, and one loop can handle them all:

List<Character> party = new List<Character>
{
    new Archer("Robin"),
    new Wizard("Merlin"),
    new Character("Villager")
};

foreach (Character member in party)
{
    Console.WriteLine(quot;{member.Name}: {member.SpecialAbility()}");
}

public class Character
{
    public string Name { get; }

    public Character(string name)
    {
        Name = name;
    }

    public virtual string SpecialAbility()
    {
        return "None";
    }
}

public class Archer : Character
{
    public Archer(string name) : base(name)
    {
    }

    public override string SpecialAbility()
    {
        return "Rain of arrows";
    }
}

public class Wizard : Character
{
    public Wizard(string name) : base(name)
    {
    }

    public override string SpecialAbility()
    {
        return "Fireball";
    }
}
Robin: Rain of arrows
Merlin: Fireball
Villager: None

The loop doesn't know or care what kind each character is. It calls SpecialAbility(), and each object answers in its own way. Add a Knight class tomorrow, put knights in the list, and this loop works for them too, without a single change.

Parameters work the same way

A method that takes a Character accepts any kind of character:

Archer robin = new Archer("Robin");
Wizard merlin = new Wizard("Merlin");

Announce(robin);
Announce(merlin);

void Announce(Character character)
{
    Console.WriteLine(quot;{character.Name} enters, ready to use {character.SpecialAbility()}!");
}

public class Character
{
    public string Name { get; }

    public Character(string name)
    {
        Name = name;
    }

    public virtual string SpecialAbility()
    {
        return "None";
    }
}

public class Archer : Character
{
    public Archer(string name) : base(name)
    {
    }

    public override string SpecialAbility()
    {
        return "Rain of arrows";
    }
}

public class Wizard : Character
{
    public Wizard(string name) : base(name)
    {
    }

    public override string SpecialAbility()
    {
        return "Fireball";
    }
}
Robin enters, ready to use Rain of arrows!
Merlin enters, ready to use Fireball!

The variable's type decides what you can ask

Through a Character variable, you can only use what every character has. An archer's own AddArrow() isn't one of them:

Character hero = new Archer("Robin");
hero.AddArrow();

public class Character
{
    public string Name { get; }

    public Character(string name)
    {
        Name = name;
    }
}

public class Archer : Character
{
    public int Arrows { get; private set; }

    public Archer(string name) : base(name)
    {
    }

    public void AddArrow()
    {
        Arrows++;
    }
}
error CS1061: 'Character' does not contain a definition for 'AddArrow' and no accessible extension method 'AddArrow' accepting a first argument of type 'Character' could be found

It's Chloe's teacher again: the object can do it, but the code only knows it as a character.

is: checking the kind

When you do need an archer's own members, ask with is. hero is Archer archer checks whether the object is an archer and, if it is, gives you an Archer variable to use:

List<Character> party = new List<Character>
{
    new Archer("Robin"),
    new Character("Villager"),
    new Archer("Marian")
};

foreach (Character member in party)
{
    if (member is Archer archer)
    {
        archer.AddArrow();
        Console.WriteLine(quot;{archer.Name} now has {archer.Arrows} arrow(s)");
    }
    else
    {
        Console.WriteLine(quot;{member.Name} has no arrows");
    }
}

public class Character
{
    public string Name { get; }

    public Character(string name)
    {
        Name = name;
    }
}

public class Archer : Character
{
    public int Arrows { get; private set; }

    public Archer(string name) : base(name)
    {
    }

    public void AddArrow()
    {
        Arrows++;
    }
}
Robin now has 1 arrow(s)
Villager has no arrows
Marian now has 1 arrow(s)

Use is sparingly. When you find yourself writing if (x is Archer) ... else if (x is Wizard) ... for every kind, that work usually belongs in a virtual method that each class overrides.

Why override, never hide

This is why the last chapter said to treat the hides inherited member warning as an error. A method written in a derived class without override hides the base version, and hiding doesn't take part in polymorphism: through a base class variable, the base version runs.

Character hero = new Archer("Robin");
Console.WriteLine(hero.SpecialAbility());

public class Character
{
    public string Name { get; }

    public Character(string name)
    {
        Name = name;
    }

    public virtual string SpecialAbility()
    {
        return "None";
    }
}

public class Archer : Character
{
    public Archer(string name) : base(name)
    {
    }

    public new string SpecialAbility()
    {
        return "Rain of arrows";
    }
}
None

The object is an archer, yet it answered None. (new marks the hiding as deliberate and silences the warning; leaving the keyword out entirely does the same thing with a warning.) With override, the archer answers for itself wherever it's held.

Check yourself

3 questions to try on the lesson page.

So what

A list of the base class, a loop, and a virtual method: each object answers in its own way, and the loop never changes when new kinds arrive. That's the power of polymorphism, and you'll lean on it in every chapter from here.

Key terms

Cheat sheets