By the end of this session, you will be able to:
| Time | Segment | Type | Duration |
|---|---|---|---|
| 0:00 | Polymorphism | Theory + Demo | 20 min |
| 0:20 | Abstract classes | Theory + Demo | 20 min |
| 0:40 | Interfaces | Theory + Demo | 25 min |
| 1:05 | Break | — | 5 min |
| 1:10 | Abstract class vs interface | Discussion + Demo | 20 min |
| 1:30 | Common .NET interfaces | Theory + Demo | 15 min |
| 1:45 | Lab | Lab | 15 min |
isasvirtual / override. The runtime selects which implementation to execute based on the object's actual type. Covered in Session 05.Shape[] shapes = { new Circle(5), new Rectangle(3, 4) };
foreach (Shape s in shapes)
{
Console.WriteLine(s.Area()); // correct override called at runtime
}
// The variable type is Shape, but the actual object decides which Area() runs
The array holds Shape references — but each call to Area() dispatches to the override in the actual concrete type.
is checks whether an object is of a given type and, with pattern matching, binds it to a typed variable in one stepas attempts a cast and returns null on failure — never throwsis pattern — it is safer and more readable than a cast that can throw InvalidCastExceptionShape s = new Circle(5);
// Pattern matching — checks and binds in one step
if (s is Circle c)
{
Console.WriteLine($"Radius: {c.Radius}");
}
// 'as' operator — returns null instead of throwing
Circle? circle = s as Circle;
if (circle != null)
{
Console.WriteLine(circle.Radius);
}
abstract keywordabstract keyword — the compiler prevents direct instantiationnew AbstractClass() is a compile error. Abstract classes live only as base types.
public abstract class Notification
{
public string Recipient { get; set; }
public string Message { get; set; }
public Notification(string recipient, string message)
{
Recipient = recipient;
Message = message;
}
// No body — derived classes must provide this
public abstract void Send();
// Shared logic — available to all derived classes
public void Log()
{
Console.WriteLine($"[LOG] Sending to {Recipient}: {Message}");
}
}
public class EmailNotification : Notification
{
public EmailNotification(string recipient, string message)
: base(recipient, message) { }
public override void Send()
=> Console.WriteLine($"Email to {Recipient}: {Message}");
}
public class SmsNotification : Notification
{
public SmsNotification(string recipient, string message)
: base(recipient, message) { }
public override void Send()
=> Console.WriteLine($"SMS to {Recipient}: {Message}");
}
Both classes must implement Send() — the compiler enforces it. They inherit Log() for free.
List<Notification> queue = new List<Notification>
{
new EmailNotification("alice@example.com", "Welcome!"),
new SmsNotification("+1234567890", "Your code: 9876")
};
foreach (var n in queue)
{
n.Log(); // concrete — shared method runs for both
n.Send(); // abstract — polymorphic dispatch to the correct override
}
The list type is Notification. The runtime dispatches Send() to the correct subclass — the loop does not need to know which.
interface keyword; by convention the name starts with IThe interface — pure contract:
public interface IPaymentProcessor
{
bool ProcessPayment(decimal amount);
void Refund(decimal amount);
string ProviderName { get; }
}
Any class implementing this interface must supply all three members.
Implementation:
public class StripeProcessor : IPaymentProcessor
{
public string ProviderName => "Stripe";
public bool ProcessPayment(decimal amount)
{
Console.WriteLine(
$"Processing ${amount} via Stripe");
return true;
}
public void Refund(decimal amount)
=> Console.WriteLine(
$"Refunding ${amount} via Stripe");
}
IRepository) and a cross-cutting one (e.g., IDisposable).
public interface ILoggable
{
void LogActivity(string action);
}
// Implements both interfaces
public class StripeProcessor
: IPaymentProcessor, ILoggable
{
public string ProviderName => "Stripe";
public bool ProcessPayment(decimal amount)
{ /* ... */ return true; }
public void Refund(decimal amount) { /* ... */ }
public void LogActivity(string action)
=> Console.WriteLine($"[Stripe] {action}");
}
static void Checkout(IPaymentProcessor processor, decimal amount)
{
processor.ProcessPayment(amount);
}
// The caller decides which implementation to inject
Checkout(new StripeProcessor(), 99.99m);
Checkout(new PayPalProcessor(), 49.50m);
Checkout depends only on the contract, never on StripeProcessor or PayPalProcessor directly. Swapping providers requires no change to the method.
IPaymentProcessor → StripeProcessor in the service container. You will use this from Session 08 onwards.
| Feature | Abstract class | Interface |
|---|---|---|
| Can have fields | Yes | No |
| Can have constructors | Yes | No |
| Can have concrete methods | Yes | Yes (C# 8+ default methods) |
| Multiple inheritance | No — only one base class | Yes — implement many interfaces |
| Models | IS-A relationship with shared state | CAN-DO capability across any type |
Animal subclasses share a Name field and a base constructor — that shared identity and data belongs in an abstract class.Dog and a Car can implement ISerializable — they have nothing in common except the capability.ILogger and BaseLogger. Which do you use and why?" — consider what callers need to know and what code can be shared.
IComparable<T> — natural sort orderIDisposable — resource cleanupIEnumerable<T> — iteration supportIComparable<T> gives a type a natural orderingCompareTo returns a negative number, zero, or a positive number to indicate less-than, equal, or greater-thanList<T>.Sort() and Array.Sort() call CompareTo automatically when the element type implements this interfacepublic class Student : IComparable<Student>
{
public string Name { get; set; }
public double GPA { get; set; }
// Sort ascending by GPA
public int CompareTo(Student other)
=> GPA.CompareTo(other.GPA);
}
var students = new List<Student> { ... };
students.Sort(); // uses CompareTo
public class FileWriter : IDisposable
{
private StreamWriter _writer;
public FileWriter(string path)
=> _writer = new StreamWriter(path);
public void Write(string text)
=> _writer.WriteLine(text);
public void Dispose() => _writer?.Dispose();
}
// The 'using' statement calls Dispose() automatically at the closing brace
using (var writer = new FileWriter("log.txt"))
{
writer.Write("Hello");
} // Dispose() runs here — file handle released even if an exception occurs
Implement IDisposable whenever your class holds unmanaged resources: files, database connections, network sockets.
IEnumerable<T> can be used in a foreach loopList<T>, arrays, Queue<T>, Stack<T> — all implement it; that is why foreach works on all of them uniformlyWhere, Select, OrderBy, ...) operates on IEnumerable<T> — write it once, works on any collectionIEnumerable<T> instead of List<T>, callers only depend on the ability to iterate — the internal representation can change freelyIEnumerable<T> or its async counterpart IAsyncEnumerable<T>.
IPrintable with one method: void Print()Book class with Title and Author properties that implements IPrintable — Print() writes "Book: {Title} by {Author}"Movie class with Title and Year properties that implements IPrintable — Print() writes "Movie: {Title} ({Year})"List<IPrintable> containing several books and movies, loop over it, and call Print() on each itemis pattern matching and the as operator provide safe type checking and casting without exceptionsIComparable<T>, IDisposable, and IEnumerable<T> are three fundamental .NET interfaces you will encounter constantlyNext session: Session 07 — C# Language Features: Enums, Generics, Collections & Delegates
List<T>, Dictionary<K,V>, Queue<T>, Stack<T>Build a shape rendering system that exercises abstract classes, interfaces, and polymorphism.
IDrawable with a method void Draw()Shape with an abstract double Area() method and a concrete void Describe() method that prints the class name and areaCircle and Rectangle that extend Shape and implement IDrawable — each Draw() prints a short ASCII representationShape[] and loop over them calling both Describe() and Draw()Triangle class and verify that everything still compiles and runs without changing the loopIResizable with void Scale(double factor). Implement it on Circle and Rectangle. Write a method that accepts IEnumerable<IResizable> and scales every element by a given factor.