As long as I’ve been teaching object-oriented programming, I’ve drummed this phrase (or something like it) into my students’ heads, to get across how interfaces are different than classes:

For classes: A subclass inherits from its superclass dynamically: As new members are added to the superclass, they are automatically available for the subclass (Based on accessibility, of course). A new public method in a superclass is automatically available for all subclass code without having to do anything to the subclass code.

Consider this:

 public class Team
    {
        public int teamID { get; set; }
        public string teamName { get; set; }
        public List<Player> players { get; set; }

        public void ListStarPlayers()
        {
            var averageRating = players.Average(pl => pl.Rating);
            var aboveAveragePlayers = from p in players
                                      where p.Rating > averageRating
                                      select p;

        }
}

So it’s perfectly legal to add, say, a ListFormerPlayers method to this class, and expect that the inheriting class will be able to use it (or not) immediately.

For Interfaces: An interface is called a contract– a 2-way contract– because once released and implemented by a class(es), the interface guarantees not to change, and the implementing class promises to have code for (implement) all the interface members. If class definitions are dynamic, interface definitions are… well, I can’t use static since that means something else in C#, so how about unchanging?

Let’s say we create an interface like this one:

public interface IPowerable
    {
        public bool IsPowered { get; set; }
        public void TurnOn();
    }

By the rules of C#, the interface contains only member definitions. Notice there’s no { } after the definition of the TurnOn method. Implementing classes agree (by the contract) to have at least these 2 members, including the same data types and method signatures.

At least they did until C# 8, which introduced the concept of default interfaces. The official Microsoft docs define it like this: you can define an implementation when you declare a member of an interface. The most common scenario is to safely add members to an interface already released and used by innumerable clients.

So let’s imagine our IPowerable interface is already being implemented by some classes like this one:

public class Television : IPowerable
    {
        private bool _isPowered = false;
        public bool IsPowered
        { 
            get => _isPowered; 
            set => _isPowered=value;        
        }

        public void TurnOn()
        {
            TogglePower(true);
        }

        private void TogglePower(bool turningOn)
        {
            //Do whatever it takes to turn the TV on or off
            //Assume success
            _isPowered = turningOn;
        }
    }

Before C# 8.0, changing anything about IPowerable would break this class and any other implementers. But what if there are new feature requests or enhancements that require new interface members? We could change our IPowerable definition to look like this (Adding a new method):

 public interface IPowerable
    {
        public bool IsPowered { get; set; }
        public void TurnOn();
        public bool ActivateBatteryBackup()
        {
            IsPowered = CheckIsBackupInstalled();
            return IsPowered;
        }

        private bool CheckIsBackupInstalled()
        {
            return true;
        }
       
    }

Amazingly (If you lived in the world before C# 8), this change requires no change to implementing classes. One important point, though: These classes still are not inheriting from the interface, so the data type of the variable is important if you want to access the new interface members. Note below that the the t variable is of type IPowerable, while tv is of type Television. The red underline show the tv instance in error, because for this type there’s no method of that name available.

You can get a pretty good starter set of some more in-depth samples of real-world use cases for this on Microsoft’s GitHub repo.