Start learning free

LESSON 190 OF 347 · OBJECT-ORIENTED PROGRAMMING

OOP Capstone in C#: Abstract Classes and Polymorphism

Lesson Read the full explanation

TL ; DR:

Put it all together: an abstract parent class, three concrete subclasses, and a polymorphic loop that dispatches by runtime type. This is the shape of nearly every real OOP design.


abstract class Animal {
    public string name;
    public Animal(string name) { this.name = name; }
    public abstract string Sound();      // each subclass picks
    public void Speak() {                 // shared by all
        Console.WriteLine(name + ": " + Sound());
    }
}
// Dog, Cat, Bird each override Sound()
// Animal[] zoo = { new Dog("Rex"), new Cat("Whiskers"), ... };
// foreach (Animal a in zoo) a.Speak();


Full lesson:

This problem stitches together everything you've learned in this chapter:

  1. Inheritance — three concrete classes share a common parent.
  2. Constructors — each subclass forwards a name to the parent.
  3. Abstract methods — the parent declares sound() with no body; each child must provide one.
  4. Shared behavior — the parent has a concrete speak() that calls sound(); every subclass inherits it for free.
  5. Polymorphism — a collection typed as Animal holds a Dog, a Cat, and a Bird; speak() dispatches to the right sound() at runtime.

The shape of this code is the shape of nearly every real OOP design you'll see — a small contract at the top (the abstract parent), concrete variants below, and the rest of the program written against the abstract type. New variants get added without touching the consumer code.


Your task

The abstract Animal class is already defined, with a concrete speak() that calls the abstract sound(). Add three subclasses — Dog (returns "Woof"), Cat (returns "Meow"), and Bird (returns "Tweet"). Then in MainFunction / main, build a polymorphic collection holding one of each (Rex, Whiskers, Tweety in that order) and loop through it calling speak().

Expected output:

Rex: Woof
Whiskers: Meow
Tweety: Tweet
STEP 1 OF 2
    Ctrl + Enter to check

    C# · OBJECT-ORIENTED PROGRAMMING

    Oop Capstone in C#, explained

    What this lesson puts together

    This lesson combines the main ideas of the chapter in one small program, an animal hierarchy:

    • Inheritance. Dog, Cat and Bird all derive from Animal.
    • Constructors. Each subclass receives a name and forwards it to Animal with base(name).
    • An abstract method. Animal declares Sound() without a body. Every concrete subclass must supply one.
    • Shared behaviour. Animal has a concrete Speak() that prints the name and calls Sound(). All subclasses inherit it unchanged.
    • Polymorphism. An Animal[] can hold a dog, a cat and a bird. Calling Speak() on each element runs the right Sound() at run time.

    The shape is common: a small contract at the top, concrete variants below, and the rest of the program written against the abstract type. A new variant can be added without changing the code that uses the animals.

    The abstract parent

    The lesson provides this class:

    abstract class Animal {
        public string name;
        public Animal(string name) { this.name = name; }
        public abstract string Sound();
        public void Speak() {
            Console.WriteLine(name + ": " + Sound());
        }
    }
    

    Two keywords carry the design:

    • abstract class means you can't write new Animal("x"). The class is incomplete on purpose.
    • public abstract string Sound(); has no body and ends in a semicolon. It forces every non-abstract subclass to override it, or the code doesn't compile.

    An abstract class can still have fields, a constructor and ordinary methods. That is why Animal stores name and why Speak() is shared: it is written once and calls Sound(), whose body is decided by the object's real class.

    Step by step

    1. Write the three subclasses. Each one has a public constructor that takes string name and passes it up with : base(name). Animal has no parameterless constructor, so without this call a subclass fails to compile. Each also has a public override string Sound() that returns its own text: "Woof", "Meow" or "Tweet".
    2. Build the array in MainFunction. Declare an Animal[] holding new Dog("Rex"), new Cat("Whiskers") and new Bird("Tweety"), in that order, because the output follows the array's order.
    3. Loop and call Speak(). A foreach (Animal a in animals) loop visits each element, and a.Speak() prints one line per animal.

    The solution in C#

    abstract class Animal {
        public string name;
        public Animal(string name) { this.name = name; }
        public abstract string Sound();
        public void Speak() {
            Console.WriteLine(name + ": " + Sound());
        }
    }
    
    class Dog : Animal {
        public Dog(string name) : base(name) { }
        public override string Sound() { return "Woof"; }
    }
    
    class Cat : Animal {
        public Cat(string name) : base(name) { }
        public override string Sound() { return "Meow"; }
    }
    
    class Bird : Animal {
        public Bird(string name) : base(name) { }
        public override string Sound() { return "Tweet"; }
    }
    
    class Solution {
        public void MainFunction() {
            Animal[] animals = { new Dog("Rex"), new Cat("Whiskers"), new Bird("Tweety") };
            foreach (Animal a in animals) {
                a.Speak();
            }
        }
    }
    

    It prints:

    Rex: Woof
    Whiskers: Meow
    Tweety: Tweet
    

    How the dispatch works

    The loop variable a has the static type Animal, so the compiler only checks that Speak() exists on Animal. At run time the first element is a Dog. Speak() executes Animal's code, reaches Sound(), and because Sound is abstract (and so virtual), C# picks Dog's override. For the next element the same line of code ends up in Cat's override instead.

    C# details

    • override is mandatory. In C#, a method that implements an abstract or virtual one must say override. Leaving it out declares a new, unrelated method, and the class then fails to compile because the abstract Sound is still unimplemented.
    • The signatures must match. public override string Sound() repeats the access level, return type, name and parameters of the abstract method. A lowercase sound() would be a different method.
    • Array initialiser. Animal[] animals = { ... }; is shorthand for new Animal[] { ... }. Elements of a subclass type are accepted because each one is an Animal.
    • Constructors are not inherited. Dog has to declare its own constructor even though it adds no fields.

    Edge cases and common mistakes

    • Forgetting : base(name). The subclass constructor then tries to call a parameterless Animal() that doesn't exist.
    • Forgetting public. An override must have the same access as the abstract method, so a private or default-access Sound is rejected.
    • Creating an Animal directly. new Animal("Rex") is a compile error. Create the subclasses.
    • Writing the wrong order. The test expects Rex, Whiskers, Tweety. A different order in the array gives different output lines.
    • Calling Sound() yourself in a loop. The output format, name: sound, lives in Animal.Speak(). Calling Speak() keeps it in one place.

    Key takeaways

    • An abstract class defines what every variant must do and shares what they have in common.
    • Subclasses forward constructor arguments with base(...) and fill the gaps with override.
    • A collection typed as the parent runs each element's own override, so consumer code stays unchanged when you add a new subclass.
    Keep going with C#About 340 step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free