CodeGym /Courses /C# SELF /How Polymorphism Relates to Abstract Methods

How Polymorphism Relates to Abstract Methods

C# SELF
Level 21, Lesson 4
Available

1. Introduction

Let's get back to our zoo. We have a base class Animal and its derived classes: Dog, Cat, Fish.

In previous lectures, we added a virtual method MakeSound() to Animal:

public class Animal
{
    public string Name { get; set; }
    public int Age { get; set; }

    public virtual void MakeSound() // Virtual method
    {
        Console.WriteLine("Some animal makes a sound."); // Default implementation
    }

    public void Sleep() // Regular method
    {
        Console.WriteLine($"{Name} is sleeping.");
    }
}

public class Dog : Animal
{
    public override void MakeSound() // Override sound for dog
    {
        Console.WriteLine("Woof-woof!");
    }
}

public class Cat : Animal
{
    public override void MakeSound() // Override sound for cat
    {
        Console.WriteLine("Meow!");
    }
}

This works great! When we create a Dog or Cat and call MakeSound(), we hear their unique sounds. But what if we just create an Animal?

Animal genericAnimal = new Animal();
genericAnimal.Name = "Unknown creature";
genericAnimal.MakeSound(); // Will print: "Some animal makes a sound."

Makes sense. But sometimes, the default base implementation just doesn't make sense. What if Animal is not a specific animal, but more of a concept? "Animal" as such doesn't make a specific sound, that's what specific animal types do. Or imagine we have a Shape class and it has a CalculateArea() method. What area should just Shape calculate? None! A Circle or a Square has an area, but not an abstract "Shape".

In these cases, when the base class can't (or shouldn't) provide a meaningful default implementation, but still requires all its derived classes to implement this method, that's where abstract methods and abstract classes come to the rescue.

2. Abstract Classes: When a Blueprint Isn't a House Yet

Imagine you're an architect, and you're creating a blueprint for a typical house. But not just any house, a "Conceptual House". It has common features: walls, roof, foundation. But you don't know yet if it'll be a one-story cottage or a multi-story skyscraper. Some parts of the blueprint are concrete (like the ceiling height on the first floor), and some are just placeholders (like "number of floors", which will be defined later).

In the C# world, this "Conceptual House" is called an abstract class.
Abstract class is a class marked with the abstract keyword.


public abstract class Animal // Now Animal is an abstract class
{
    // ...
}
Declaring an abstract class

Key feature of abstract classes:

  • You can't create them directly. You can't write new Animal(). Why? Because Animal is now something undefined, a concept. You can't build a "Conceptual House", you can only build a specific cottage or skyscraper.
    If you try new Animal(), the compiler will slap your hand:
    Cannot create an instance of the abstract type or interface 'Animal'
    (Cannot create an instance of the abstract type or interface 'Animal')
    This is a super important restriction!
  • They can contain abstract members. And this is where it gets interesting!

3. Abstract Methods: A Contract Without Implementation

If an abstract class is a "Conceptual House", then an abstract method is the part of the blueprint that says "do this", but doesn't say "how". For example, "Build the foundation" is a required part of the house, but the exact size and materials of the foundation depend on the house type.

An abstract method is a method that:

  • Is marked with the abstract keyword.
  • Has no body (no {} code block). It ends with a semicolon ;.
  • Can only be declared inside an abstract class.

Let's make our MakeSound() method abstract:


public abstract class Animal // We made Animal abstract
{
    public string Name { get; set; }
    public int Age { get; set; }

    public abstract void MakeSound(); // Here it is, abstract method! No body!

    public void Sleep() // This method is still regular, "concrete"
    {
        Console.WriteLine($"{Name} is sleeping.");
    }
}

Check out how MakeSound() changed! It no longer has curly braces or a default implementation. Now it just says: "Any animal must be able to make a sound. How exactly — that's up to whoever inherits from me."

Important rule: If your class inherits from an abstract class and isn't abstract itself, it must override (with override) all abstract methods from the base class. This isn't optional, it's a requirement, a contract! The C# compiler is super strict about this. If you forget, it'll remind you right away:

public class Dog : Animal // Regular, non-abstract class
{
    // COMPILATION ERROR!
    // 'Dog' does not implement inherited abstract member 'Animal.MakeSound()'
    // (Class 'Dog' does not implement inherited abstract member 'Animal.MakeSound()')
    // The compiler expects us to have MakeSound() with override!
}

To get rid of the error, Dog and Cat must override MakeSound():

public class Dog : Animal
{
    public override void MakeSound() // Must override!
    {
        Console.WriteLine("Woof-woof!");
    }
}

public class Cat : Animal
{
    public override void MakeSound() // Here too!
    {
        Console.WriteLine("Meow!");
    }
}

Comparing virtual, abstract, and override

Characteristic virtual method abstract method override keyword
Where it can be used In a regular or abstract class Only in an abstract class In a derived (child) class
Method body Has a body (default implementation) No body (ends with ;) Has a body (new implementation)
Purpose Provides a default implementation, but allows derived classes to change it Declares a contract: derived classes must provide their own implementation Provides a specific implementation for a virtual or abstract method from the base class
Can you create an instance of the base class? Yes (if the base class isn't abstract) No (if the base class is abstract) N/A (applies to method, not class)
Requirement for derived class Optionally override (override) Must override (override) if the derived class isn't abstract N/A

4. Polymorphism in Action with Abstract Methods

Now it gets really fun! We already know that polymorphism lets us work with objects of different derived classes through a common reference to the base class. And this works perfectly even when the base class is abstract!

Even though we can't create an Animal instance directly (remember, new Animal() will throw an error), we can use the Animal type as a reference to objects of derived classes. That's super powerful!

Let's continue our zoo. Imagine we have a farm, and different animals live there. We want each animal to make its own sound.


using System;

// Abstract class Animal
public abstract class Animal
{
    public string Name;
    public int Age;
    public Animal(string name, int age) { Name = name; Age = age; }
    public abstract void MakeSound();
    public void Sleep() { Console.WriteLine($"{Name} is sleeping."); }
}

public class Dog : Animal
{
    public Dog(string name, int age) : base(name, age) { }
    public override void MakeSound() { Console.WriteLine("Woof-woof!"); }
}

public class Cat : Animal
{
    public Cat(string name, int age) : base(name, age) { }
    public override void MakeSound() { Console.WriteLine("Meow!"); }
}

public class Fish : Animal
{
    public Fish(string name, int age) : base(name, age) { }
    public override void MakeSound() { Console.WriteLine("Blub-blub"); }
}

class Program
{
    static void Main()
    {
        Animal[] animals = {
            new Dog("Sharik", 3),
            new Cat("Murzik", 5),
            new Fish("Nemo", 1)
        };

        foreach (Animal animal in animals)
        {
            Console.WriteLine($"\nHi, I'm {animal.Name}, I'm {animal.Age} years old.");
            animal.MakeSound();
            animal.Sleep();
        }
    }
}

What's happening in this code?

  1. We declared Animal as an abstract class. This tells the compiler: "This class is a template, you can't create it directly, but you can inherit from it".
  2. We declared public abstract void MakeSound(); inside Animal. This says: "Any class that inherits from Animal (and isn't abstract itself) must implement the MakeSound() method". That's our contract!
  3. Dog, Cat, and Fish obediently follow this contract, overriding MakeSound() with their own unique implementation. If we forgot to do this for even one of them, the compiler wouldn't let our code through.
  4. In the Main method, we create an Animal[] array. Even though Animal is abstract, the array can store references to objects of derived classes (Dog, Cat, Fish), because they are Animal!
  5. When we loop through the array with foreach and call animal.MakeSound(), thanks to polymorphism, C# "knows" which MakeSound() to call: Dog.MakeSound(), Cat.MakeSound(), or Fish.MakeSound(). The method called is the one that belongs to the actual object type the Animal reference points to at the moment, not the reference type. That's the beauty of polymorphism!
  6. But animal.Sleep() calls the concrete implementation from the base Animal class, because this method wasn't marked as virtual or abstract, and wasn't overridden in the child classes.

5. Why Does This Matter in Real Life?

"Okay, zoo, animals... But where will I use this when I'm writing a real app for a bank or a store?" — you might ask. And that's a great question! Abstract classes and methods are a super powerful tool for designing flexible and scalable systems.

Enforcing a contract: This is the main advantage. Imagine you're building a framework for payment systems. You have a base abstract class PaymentProcessor. And you know for sure that any payment processor must be able to ProcessPayment(), RefundPayment(), and CheckStatus(). But how exactly this happens for PayPal, for a credit card, or for Bitcoin — totally different things.
You declare these methods as abstract in PaymentProcessor.


public abstract class PaymentProcessor
{
    public abstract bool ProcessPayment(decimal amount, string currency, string cardNumber);
    public abstract bool RefundPayment(string transactionId);
    public abstract string CheckStatus(string transactionId);
    // ... other methods that can be concrete, like logging
    public void LogTransaction(string message)
    {
        Console.WriteLine($"[LOG]: {message}");
    }
}

public class PayPalProcessor : PaymentProcessor
{
    public override bool ProcessPayment(decimal amount, string currency, string cardNumber)
    {
        // Here goes the complex logic for PayPal API
        Console.WriteLine($"PayPal: processing {amount} {currency}...");
        return true;
    }
    public override bool RefundPayment(string transactionId) { /* ... */ return true; }
    public override string CheckStatus(string transactionId) { /* ... */ return "Completed"; }
}

public class CreditCardProcessor : PaymentProcessor
{
    public override bool ProcessPayment(decimal amount, string currency, string cardNumber)
    {
        // Here goes the logic for the acquiring bank
        Console.WriteLine($"CreditCard: {amount} {currency} from card {cardNumber.Substring(0,4)}XXXX...");
        return true;
    }
    public override bool RefundPayment(string transactionId) { /* ... */ return true; }
    public override string CheckStatus(string transactionId) { /* ... */ return "Processing"; }
}

Now any developer who wants to create a new payment processor (like BitcoinProcessor) will have to implement all three of these methods. They can't accidentally forget about RefundPayment(), because the compiler won't let them! This keeps your system consistent.

Flexibility and scalability: You can write code that works with PaymentProcessor without knowing which specific implementation will be used. For example, in your online store's cart code, you just call currentProcessor.ProcessPayment(), and the system plugs in the right processor depending on the payment method the user picked. Tomorrow a new payment method appears — you just create a new class, inherit from PaymentProcessor, implement the abstract methods, and you don't even have to change your main store code!

Avoiding empty implementations: If we used virtual methods instead of abstract, we'd have to give them an empty default implementation, which could be misleading. abstract clearly says: "There's no implementation here, and there can't be — go to the inheritors!"

Better code architecture: Abstract classes help you clearly separate common logic from specific logic. Common stuff (like LogTransaction in PaymentProcessor) lives in the base class, and specific stuff (like ProcessPayment) — in the derived ones. This makes your code more readable, maintainable, and testable. It lets framework and library designers define "extension points" that users of their libraries must fill in.

6. Common Mistakes and Gotchas

Mistake #1: Trying to create an instance of an abstract class.
This is always a compile error. An abstract class is a concept, not a concrete object. You can use it as a reference type, but you can't write new Animal().

Mistake #2: Forgot to override an abstract method.
If a derived class isn't abstract, it must implement all abstract methods from the base class. Otherwise — compile error. The only way around this is to make the derived class abstract too, but usually that's not what you want.

Mistake #3: Mixing up abstract and virtual.
abstract forces you to override the method, it has no body. virtual gives a default implementation and can be overridden. You can't declare an abstract method with a body or a virtual method without a body — that's a syntax error.

Mistake #4: Using new instead of override.
If you don't use override and just create a method with the same name in the inheritor, you're not overriding, you're hiding the base class method. This will lead to unexpected behavior with polymorphic calls: the base class method will be called.


public class Base
{
    public void DoSomething() { Console.WriteLine("Base"); }
}

public class Derived : Base
{
    public new void DoSomething() { Console.WriteLine("Derived"); }
}

Base obj = new Derived();
obj.DoSomething(); // Will print: Base
2
Task
C# SELF, level 21, lesson 4
Locked
Creating an abstract class and its implementation
Creating an abstract class and its implementation
2
Task
C# SELF, level 21, lesson 4
Locked
Polymorphism with Abstract Methods
Polymorphism with Abstract Methods
1
Survey/quiz
The Concept of Polymorphism, level 21, lesson 4
Unavailable
The Concept of Polymorphism
Polymorphism and Method Overloading
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION