What this lesson puts together
This lesson combines the main ideas of the chapter in one small program, an animal hierarchy:
- Inheritance.
Dog,CatandBirdall derive fromAnimal. - Constructors. Each subclass receives a name and forwards it to
Animalwithbase(name). - An abstract method.
AnimaldeclaresSound()without a body. Every concrete subclass must supply one. - Shared behaviour.
Animalhas a concreteSpeak()that prints the name and callsSound(). All subclasses inherit it unchanged. - Polymorphism. An
Animal[]can hold a dog, a cat and a bird. CallingSpeak()on each element runs the rightSound()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 classmeans you can't writenew 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
- Write the three subclasses. Each one has a public constructor that takes
string nameand passes it up with: base(name). Animal has no parameterless constructor, so without this call a subclass fails to compile. Each also has apublic override string Sound()that returns its own text:"Woof","Meow"or"Tweet". - Build the array in
MainFunction. Declare anAnimal[]holdingnew Dog("Rex"),new Cat("Whiskers")andnew Bird("Tweety"), in that order, because the output follows the array's order. - Loop and call
Speak(). Aforeach (Animal a in animals)loop visits each element, anda.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
overrideis mandatory. In C#, a method that implements an abstract or virtual one must sayoverride. Leaving it out declares a new, unrelated method, and the class then fails to compile because the abstractSoundis still unimplemented.- The signatures must match.
public override string Sound()repeats the access level, return type, name and parameters of the abstract method. A lowercasesound()would be a different method. - Array initialiser.
Animal[] animals = { ... };is shorthand fornew Animal[] { ... }. Elements of a subclass type are accepted because each one is anAnimal. - Constructors are not inherited.
Doghas 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 parameterlessAnimal()that doesn't exist. - Forgetting
public. An override must have the same access as the abstract method, so a private or default-accessSoundis rejected. - Creating an
Animaldirectly.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 inAnimal.Speak(). CallingSpeak()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 withoverride. - A collection typed as the parent runs each element's own override, so consumer code stays unchanged when you add a new subclass.