# 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 November 3, 2026

## 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:

```csharp
Character hero = new Archer("Robin");
Console.WriteLine($"{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";
    }
}
```

```text
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:

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

foreach (Character member in party)
{
    Console.WriteLine($"{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";
    }
}
```

```text
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:

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

Announce(robin);
Announce(merlin);

void Announce(Character character)
{
    Console.WriteLine($"{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";
    }
}
```

```text
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:

```csharp
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++;
    }
}
```

```text
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:

```csharp
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($"{archer.Name} now has {archer.Arrows} arrow(s)");
    }
    else
    {
        Console.WriteLine($"{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++;
    }
}
```

```text
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.

```csharp
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";
    }
}
```

```text
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

- [Polymorphism](https://www.ask-codey.com/glossary/polymorphism-c-sharp)
- [Upcasting](https://www.ask-codey.com/glossary/upcasting)
- [Dynamic Dispatch](https://www.ask-codey.com/glossary/dynamic-dispatch-oop)
- [is Pattern](https://www.ask-codey.com/glossary/is-pattern)
- [Method Hiding](https://www.ask-codey.com/glossary/method-hiding)

## Cheat sheets

- [Polymorphism with Base Classes](https://www.ask-codey.com/cheat-sheets/polymorphism-in-csharp-polymorphism-with-base-classes-cheat-sheet)

Source: https://www.ask-codey.com/lessons/polymorphism-in-csharp-polymorphism-with-base-classes
