C#
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Object-Oriented C#
-
Section 4: Working with Data
-
Section 5: Error Handling
-
Section 6: Delegates and Events
-
Section 7: Async Programming
-
Section 8: More Language Features
-
Section 9: File I/O and Serialization
-
Section 10: Networking in .NET
-
Section 11: The .NET Ecosystem
-
Section 12: Memory and Performance
-
Section 13: Concurrency Beyond Async
-
Section 14: Reflection and Attributes
-
Section 15: Testing and Best Practices
-
Section 16: Design Patterns in C#
-
Section 17: Standard Library Deep Dive
-
Section 18: Data Structures and Algorithms in C#
-
Section 19: GUI and Desktop Development Overview
-
Section 20: Practical Projects
-
Section 21: More Practice Exercises
-
Section 22: More Standard Library and Text Processing
-
Section 23: More Design Patterns
-
Section 24: More Projects
-
Section 25: Interview Practice
-
Section 26: C# Keywords Reference (Modifiers)
-
Section 27: C# Keywords Reference (Statements)
-
Section 28: C# Keywords Reference (Operators)
-
Section 29: BCL: System.Collections.Generic
-
Section 30: BCL: System.Linq
-
Section 31: BCL: System.Threading
-
Section 32: BCL: System.IO
-
Section 33: BCL: System.Text and System.Text.Json
-
Section 34: BCL: System.Net.Http
-
Section 35: C# Language Specification Topics
-
Section 36: More Practice Exercises
-
Section 37: More Async Patterns
-
Section 38: More BCL: System.Reflection and System.Diagnostics
-
Section 39: Nullable Reference Types In Depth
-
Section 40: C# Records and Pattern Matching In Depth
-
Section 41: Dependency Injection Deep Dive
-
Section 42: More Interview and Whiteboard Practice
21: Interfaces and Default Interface Methods
I've sat through countless code reviews where developers treat interfaces as nothing more than a strict "to-do list" for a class. They assume that if a method is in an interface, the class must write the logic for it, or the code won't compile. For years, that was essentially true. But if you're still thinking that way, you're missing one of the most powerful evolution tools in modern C#.
The "Interfaces are only blueprints" myth
The misconception is simple: "Interfaces define what a class can do, but they can never define how it does it."
If we believed this, adding a new method to an interface used in a large project would be a nightmare. You'd add the method to the interface, and suddenly, fifty different classes across your entire solution would break because they don't implement that new method. You'd spend your entire afternoon writing boilerplate code just to get the project to compile again.
// The "Old Way" headache
public interface ILogger {
void Log(string message);
// If I add 'void LogError(string message)' here,
// every single class implementing ILogger just broke.
}
This is where Default Interface Methods (DIMs) come in. They allow you to provide a "fallback" implementation directly within the interface. It's not a class, and it's not an abstract class; it's an interface that actually has a brain.
Adding behavior without breaking your API
Imagine we have a logging system. Most of our loggers just need a basic Log method. But we've decided that every logger should also be able to log a warning. Instead of forcing every single logger class to implement LogWarning, we can provide a default implementation that simply leverages the existing Log method.
public interface ILogger {
void Log(string message);
// This is a Default Interface Method
void LogWarning(string message) {
Log($"[WARNING]: {message}");
}
}
public class ConsoleLogger : ILogger {
public void Log(string message) {
Console.WriteLine(message);
}
// Notice: I didn't implement LogWarning, but I can still use it!
}
Now, if a specific logger—say, a CloudLogger—needs a very special way of handling warnings (like sending an email), it can still override that default. If it doesn't, it just gets the standard behavior for free. This is a lifesaver when you're maintaining a library used by other teams; you can evolve your interfaces without breaking their code.
The catch: Interface vs. Class references
Here is where people usually get tripped up. Default methods are not inherited by the class in the way a base class method is. They are implemented explicitly by default.
If you try to call LogWarning on a variable typed as ConsoleLogger, the compiler will scream at you. Why? Because ConsoleLogger didn't actually define that method; the interface did. To access the default implementation, you have to treat the object as the interface.
ConsoleLogger myLogger = new ConsoleLogger();
// myLogger.LogWarning("Careful!"); // This will fail to compile!
ILogger interfaceLogger = myLogger;
interfaceLogger.LogWarning("Careful!"); // This works perfectly.
I'll be honest: this design choice by the C# team feels a bit clunky at first. But it's there to prevent "diamond inheritance" problems and to keep the class's own public API clean. Just remember: if the logic lives in the interface default, you need to be talking to the interface.
📋 Practical Task
Implementing a Smart Payment Gateway
You are building a payment processing system. You have an interface IPaymentProcessor that requires a ProcessPayment(decimal amount) method. However, you want to provide a default RefundPayment(decimal amount) method that simply logs a message saying a refund is being requested, as most payment providers handle refunds through a separate, complex API that you'll implement later.
Your Task:
- Create the
IPaymentProcessorinterface with theProcessPaymentmethod and a default implementation ofRefundPayment. - Create two classes:
StripeProcessorandPayPalProcessor.StripeProcessorshould use the default refund logic, butPayPalProcessorshould provide its own customRefundPaymentlogic (e.g., printing "PayPal: Processing refund via PayPal API"). - In your main program, instantiate both processors.
- Call
ProcessPaymenton both. - Call
RefundPaymenton both by casting them to theIPaymentProcessorinterface.
There are no comments for now.