CodeGym /Courses /C# SELF /Difference Between Interfaces and Abstract Classes

Difference Between Interfaces and Abstract Classes

C# SELF
Level 23 , Lesson 1
Available

1. Two Sides of the Same Coin

We've already dug pretty deep into the concept of abstraction and checked out its two most powerful tools in C# – abstract classes and interfaces. You probably already feel they're kinda similar, since both let us define the "skeleton" of behavior that concrete classes then have to implement. But trust me, it's like comparing a hammer and a screwdriver: both are tools, but for totally different jobs.

Let's start with the main thing: why do we need two tools for the same job? In programming, just like in life, nothing is "just because." If there are two similar tools, it means each has its own unique strengths and use cases.

Let's just throw them into a quick comparison table so you get the big picture. It's like a cheat sheet to help you catch the most important points.

Characteristic Abstract Class Interface
Instance Creation Can't create directly (
new AbstractClass()
).
Can't create directly (
new IMyInterface()
).
Member Implementation Can have:
– Fully implemented methods/properties.
– Abstract methods/properties (no implementation).
– Fields, constructors, static methods.
Can have methods with default implementation, static abstract and static non-abstract members.
Can't have instance fields or constructors.
Access Modifiers Can have any (public, protected, internal, private). All interface members are public by default. You don't specify access modifiers for them (exceptions: methods with default implementation and static members).
Inheritance A class can inherit from only one abstract class. A class can implement as many interfaces as it wants.
Relationship Type "Is-a" (is-a). Defines a base type and the common part of the hierarchy. "Can do" (has-a or can-do). Defines a contract for behavior/abilities.
State Can store state (instance fields). Can have static fields, but not instance fields.
Extension You can add new implemented methods without changing inheritors. You can add methods with default implementation without breaking inheritors.

Pretty cool, right? Let's break down these points in more detail.

2. Multiple Inheritance

This is probably the most fundamental and easy-to-remember difference. In C#, just like in a bunch of other languages (like Java), a class can inherit from only one parent class. Doesn't matter if it's a regular class or an abstract one – just one! This is to avoid the famous "diamond problem," where inheriting from multiple classes creates ambiguity about which inherited method to use if they have the same name.

But a class can implement as many interfaces as it wants! Imagine your class is a person. They can be a "student" (inherit from Student), but at the same time they "can cook" (ICookable), "can drive" (IDriveable), and "can sing" (ISingable). Super flexible!


abstract class Animal
{
    public string Name;
    public abstract void MakeSound();
    public void Eat() => Console.WriteLine($"{Name} eats.");
}

interface IFlyable { void Fly(); double MaxFlyingAltitude { get; } }
interface ISwimable { void Swim(); }

class Duck : Animal, IFlyable, ISwimable
{
    public double MaxFlyingAltitude => 1000;
    public override void MakeSound() => Console.WriteLine($"{Name} quacks!");
    public void Fly() => Console.WriteLine($"{Name} flies!");
    public void Swim() => Console.WriteLine($"{Name} swims!");
}

class Program
{
    static void Main()
    {
        var duck = new Duck { Name = "Donald" };
        duck.MakeSound();
        duck.Eat();
        duck.Fly();
        duck.Swim();

        // Working with the object through different types:
        Animal a = duck;  a.Eat();
        IFlyable f = duck; f.Fly();
        ISwimable s = duck; s.Swim();
    }
}

If your class needs to be part of some hierarchy (like Dog is an Animal), use inheritance from a class (abstract or regular). If your class needs to have some ability (like Dog can run, Cat can run), but those abilities aren't tied to a strict hierarchy, use interfaces. That's the main advantage of interfaces – they let you create contracts for totally different, unrelated classes.

3. Where Do Data Live, and Where Are There Only Promises?

Abstract classes can contain whatever you want:

  • Regular (non-abstract) methods and properties with full implementation.
  • Abstract methods and properties without implementation (these are what we override).
  • Fields (instance variables) that store the object's state.
  • Constructors, which are used to initialize that state.
  • Even static methods and properties.
  • And of course, they can have any access modifiers: public, protected, private, etc.

This makes an abstract class a powerful tool for defining partially implemented behavior and shared state for all its inheritors.


abstract class Employee
{
    public string FirstName, LastName;
    public decimal Salary { get; protected set; }
    public Employee(string first, string last) { FirstName = first; LastName = last; }
    public void GetPaid(decimal sum)
    {
        Salary += sum;
        Console.WriteLine($"{FirstName} {LastName} got paid {sum:C}. Salary: {Salary:C}");
    }
    public abstract void PerformWork();
    public abstract void TakeBreak();
}

class Developer : Employee
{
    public Developer(string f, string l) : base(f, l) { }
    public override void PerformWork() => Console.WriteLine($"{FirstName} writes code.");
    public override void TakeBreak() => Console.WriteLine($"{FirstName} drinks coffee.");
}

class Tester : Employee
{
    public Tester(string f, string l) : base(f, l) { }
    public override void PerformWork() => Console.WriteLine($"{FirstName} looks for bugs.");
    public override void TakeBreak() => Console.WriteLine($"{FirstName} plays soccer.");
}

class Program
{
    static void Main()
    {
        Employee[] team = {
            new Developer("Ivan", "Petrov"),
            new Tester("Maria", "Sidorova")
        };

        foreach (var emp in team)
        {
            Console.WriteLine($"\n--- Workday for {emp.FirstName} {emp.LastName} ---");
            emp.PerformWork();
            emp.GetPaid(2000);
            emp.TakeBreak();
        }
    }
}

Interfaces – that's a whole different story. Up to C# 8, they could contain only declarations of methods, properties, indexers, and events. No fields, no constructors, no method implementations! Just the "signature" of a method without a body. And all their members were public by default (even if you didn't write public). This made sure that an interface is a pure contract, with no implementation details or hidden state.

Starting with C# 8, interfaces got a bit "fatter" and can now have methods with default implementation (Default Interface Methods) and static members. This was done so you could add new methods to existing interfaces without "breaking" millions of lines of code that implement them. But even with these updates, interfaces still can't have instance fields or constructors. That's a key limitation to keep interfaces as "behavior contracts," not "state holders."


interface ISaveable
{
    void Save(string file); 
    bool IsDirty { get; }
}

interface ILoadable
{
    void Load(string file);
}

class GameProgress : ISaveable, ILoadable
{
    public int Level { get; set; }
    public string PlayerName { get; set; }
    bool _isDirty = true;
    public bool IsDirty => _isDirty;

    public GameProgress(string name, int level)
    {
        PlayerName = name; Level = level;
    }
    public void Save(string file)
    {
        Console.WriteLine($"Saved: {PlayerName}, level {Level} -> {file}");
        _isDirty = false;
    }
    public void Load(string file)
    {
        Console.WriteLine($"Loaded from {file}");
        PlayerName = "New Player"; Level = 5; _isDirty = true;
    }
    public void UpdateProgress(int newLevel)
    {
        Level = newLevel; _isDirty = true;
        Console.WriteLine($"Updated to {Level}.");
    }
}

class Program
{
    static void Main()
    {
        var game = new GameProgress("Hero", 1);
        game.UpdateProgress(3);

        ISaveable saver = game;
        if (saver.IsDirty) saver.Save("save.dat");

        ILoadable loader = game;
        loader.Load("save.dat");
        Console.WriteLine($"After loading: {game.PlayerName}, {game.Level}");
    }
}

Bottom line: If you need a base class that gives not just a contract but already implemented code (shared logic) or stores shared state, go with an abstract class. If you just need a "checklist" or "agreement" for behavior, with no implementation or state, then interface is your pick.

4. When Should You Use Interfaces?

Interfaces are awesome when you want to define an ability or behavior that totally different, unrelated objects can have. For example:

  • IDisposable: any object that needs to be properly "disposed" after use (file, network connection, database).
  • IEnumerable<T>: any object you can loop through with foreach.
  • IComparable<T>: any object you can compare to another object of the same type.

The important thing is, FileStream and SqlConnection can implement IDisposable, and List<T> and Dictionary<TKey, TValue> can implement IEnumerable<T>. These classes belong to totally different hierarchies, but they share a common ability defined by the interface.

Example: Imagine a system where you have a Car and a Airplane. Both can be a Vehicle (maybe an abstract class). But Car, Airplane, and even Boat (if you add it) – can move. That ability to "move" is a perfect candidate for an IMovable interface.


interface IMovable
{
    void Move(int distance);
}

class Car : IMovable
{
    public string Brand;
    public Car(string brand) => Brand = brand;
    public void Move(int d) => Console.WriteLine($"{Brand} drives {d} km.");
}

class Airplane : IMovable
{
    public string Model;
    public Airplane(string model) => Model = model;
    public void Move(int d) => Console.WriteLine($"{Model} flies {d} km.");
}

class Human : IMovable
{
    public string Name;
    public Human(string name) => Name = name;
    public void Move(int d) => Console.WriteLine($"{Name} walks {d} m.");
}

class Program
{
    static void Main()
    {
        IMovable[] movers = { new Car("Toyota"), new Airplane("Boeing 747"), new Human("Arthur") };
        foreach (var item in movers)
            item.Move(100);
    }
}

See? We can make a list of IMovable and call Move() on each item, without knowing if it's a Car, Airplane, or Human. That's the power of polymorphism through interfaces.

5. When Should You Use Abstract Classes?

Abstract classes are perfect when you want to define shared base functionality for a group of closely related classes that are types of something in common. They provide ready-to-use code that all inheritors need the same way, and at the same time force inheritors to implement their own specific parts.

Imagine you have a group of different types of bank accounts: SavingAccount, CheckingAccount, CreditAccount. All of them are BankAccount. They all have a balance (Balance), and they all can deposit money (Deposit). But the rules for withdrawing money (Withdraw) are different for each. That's where the abstract class BankAccount comes in!


abstract class BankAccount
{
    public string AccountNumber { get; }
    public decimal Balance { get; protected set; }
    public BankAccount(string acc) { AccountNumber = acc; }
    public void Deposit(decimal sum)
    {
        if (sum > 0)
        {
            Balance += sum;
            Console.WriteLine($"{AccountNumber}: +{sum:C}, Balance: {Balance:C}");
        }
    }
    public abstract bool Withdraw(decimal sum);
}

class CheckingAccount : BankAccount
{
    public CheckingAccount(string acc) : base(acc) { }
    public override bool Withdraw(decimal sum)
    {
        if (Balance >= sum)
        {
            Balance -= sum;
            Console.WriteLine($"{AccountNumber}: -{sum:C}, Balance: {Balance:C}");
            return true;
        }
        Console.WriteLine($"{AccountNumber}: Not enough funds");
        return false;
    }
}

class CreditAccount : BankAccount
{
    public decimal CreditLimit { get; }
    public CreditAccount(string acc, decimal limit) : base(acc) => CreditLimit = limit;
    public override bool Withdraw(decimal sum)
    {
        if (Balance - sum >= -CreditLimit)
        {
            Balance -= sum;
            Console.WriteLine($"{AccountNumber}: -{sum:C}, Balance: {Balance:C}");
            return true;
        }
        Console.WriteLine($"{AccountNumber}: Limit exceeded");
        return false;
    }
}

class Program
{
    static void Main()
    {
        var checking = new CheckingAccount("12345");
        checking.Deposit(1000);
        checking.Withdraw(300);
        checking.Withdraw(800);

        Console.WriteLine("\n--- Credit Account ---");
        var credit = new CreditAccount("67890", 500);
        credit.Deposit(200);
        credit.Withdraw(400);
        credit.Withdraw(400);
    }
}

Here, BankAccount gives all accounts the shared deposit logic (Deposit) and manages the account number and balance. But the withdrawal logic (Withdraw) is different, so it's abstract.

6. When They Work Together: The Perfect Pair

The most elegant and powerful design often combines abstract classes and interfaces. An abstract class can even implement one or more interfaces itself!

Imagine: you have abstract Animal that defines the basics. But some Animal can be IMovable, ICarnivore, IPredator, and so on. Your Animal can even provide a base implementation for IMovable (like a Move(int speed) method), but then concrete classes like Lion or Fish will override that implementation to move their own way.


public interface ISaveable
{
    void SaveState(string path);
    bool HasChanges { get; }
}

public class Vector3
{
    public float X, Y, Z;
    public Vector3(float x, float y, float z) { X = x; Y = y; Z = z; }
    public override string ToString() => $"({X}, {Y}, {Z})";
}

public abstract class GameObject
{
    public string Id { get; }
    public Vector3 Position { get; }
    protected GameObject(string id, Vector3 pos) { Id = id; Position = pos; }
    public abstract void Update();
    public void Destroy() => Console.WriteLine($"{Id} destroyed");
}

public class Player : GameObject, ISaveable
{
    public int Health { get; private set; }
    private bool _hasChanges = true;
    public bool HasChanges => _hasChanges;

    public Player(string id, Vector3 pos, int health) : base(id, pos) => Health = health;

    public override void Update() =>
        Console.WriteLine($"{Id} updates, HP: {Health}, Pos: {Position}");

    public void TakeDamage(int dmg)
    {
        Health -= dmg;
        _hasChanges = true;
        Console.WriteLine($"{Id} took {dmg} damage. HP: {Health}");
    }

    public void SaveState(string path)
    {
        Console.WriteLine($"Saved {Id} (HP:{Health}) to {path}");
        _hasChanges = false;
    }
}

class GameEngine
{
    static void Main()
    {
        var player = new Player("P1", new Vector3(0, 0, 0), 100);
        player.Update();
        player.TakeDamage(20);

        ISaveable saver = player;
        if (saver.HasChanges) saver.SaveState("save.json");

        GameObject obj = player;
        obj.Update();
        player.Destroy();
    }
}

In this example, Player is a GameObject (because it inherits from it, using shared fields Id, Position, and the Destroy method). And at the same time, Player can be saved (because it implements the ISaveable interface). That's a super flexible and powerful approach!

7. Nuances and Typical Mistakes

Trying to create an instance of an abstract class:

Animal myAnimal = new Animal();
– this is a common newbie mistake. Remember: an abstract class is a template, not a ready-to-use object. The compiler will call you out on this right away.

Forgetting to implement abstract methods: If you inherit from an abstract class and your class isn't abstract, you have to override all abstract methods from the base class. Otherwise, the compiler will complain too.

Trying to add fields to an interface: This is one of those "nope" things that clearly separates interfaces. Since C# 8+ there are static fields, but instance fields (the ones that belong to a specific object) are still forbidden. An interface is a contract for behavior, not a data store.

Mixing up virtual and abstract:

  • A virtual method/property has a default implementation and can be overridden in inheritors.
  • An abstract method/property doesn't have an implementation and must be overridden in the first non-abstract inheritor.
  • Using virtual where the logic is always different leads to writing empty implementations and then overriding them. That's a less clear contract than abstract.

Understanding these differences and making the right choice between abstract class and interface is a key skill for every C# dev. It's not just about syntax, it's about architectural thinking. When you're designing a system, ask yourself: "Do these classes have a shared 'is-a' hierarchy?" and "Do these classes have a shared 'ability' that unrelated types need?" The answers will help you make the right choice.

In the next lectures, we'll keep diving deeper into the world of interfaces, checking out their more advanced features that showed up in the latest C# versions. Stay tuned!

2
Task
C# SELF, level 23, lesson 1
Locked
Common Interface for a Media Player
Common Interface for a Media Player
2
Task
C# SELF, level 23, lesson 1
Locked
Interfaces for objects with different abilities
Interfaces for objects with different abilities
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION