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
20: Inheritance and Polymorphism
I was working on a small billing module the other day and realized I was writing the same three properties—Amount, TransactionDate, and PaymentId—over and over again for every single payment method. It felt wrong. When you find yourself copy-pasting code across three different classes, that's usually a signal that your architecture is missing a layer.
Stopping the repetition
Initially, I had separate classes for CreditCardPayment and PaypalPayment. Both had the exact same basic data. To fix this, I decided to pull those common pieces of data up into a "parent" class. I'll call it Payment.
public class Payment
{
public decimal Amount { get; set; }
public DateTime TransactionDate { get; set; }
public string PaymentId { get; set; }
public void Process()
{
Console.WriteLine($"Processing generic payment of {Amount}...");
}
}
public class CreditCardPayment : Payment
{
public string CardNumber { get; set; }
}
public class PaypalPayment : Payment
{
public string EmailAddress { get; set; }
}
By using the : syntax, CreditCardPayment now "inherits" everything from Payment. I don't have to redeclare the amount or the date; it's just there. This is a huge win for maintenance. If I need to add a Currency property later, I add it once to the base class, and every single payment type gets it automatically.
The "Wrong Method" trap
Here is where I hit a wall. I wanted to process a list of different payments in one loop. I thought, "Since they are all Payments, I can just put them in a List<Payment>." It seemed logical. But watch what happens when I try to specialize the Process method for the credit card.
public class CreditCardPayment : Payment
{
public string CardNumber { get; set; }
public void Process()
{
Console.WriteLine($"Charging credit card {CardNumber} for {Amount}...");
}
}
// ... in my main code ...
List<Payment> payments = new List<Payment> {
new CreditCardPayment { Amount = 100, CardNumber = "1234-..." },
new PaypalPayment { Amount = 50, EmailAddress = "user@example.com" }
};
foreach (var p in payments)
{
p.Process();
}
I expected to see "Charging credit card..." for the first item. Instead, the console printed "Processing generic payment..." for both. I was baffled for a second. The problem is that C# looks at the type of the reference (which is Payment), not the type of the actual object (which is CreditCardPayment), when deciding which method to call. My CreditCardPayment.Process() method wasn't replacing the base one; it was just hiding it.
Unlocking Polymorphism
To fix this, I need to tell C# that the Process method is intended to be swapped out by child classes. This is where virtual and override come in. I have to explicitly grant permission for the method to be changed.
First, I'll mark the base method as virtual. Then, in the child class, I'll use the override keyword. This is the core of polymorphism: the ability of an object to behave differently based on its actual derived type, even when treated as its base type.
public class Payment
{
// "virtual" means: "Children are allowed to change how this works"
public virtual void Process()
{
Console.WriteLine("Processing generic payment...");
}
}
public class CreditCardPayment : Payment
{
public string CardNumber { get; set; }
// "override" means: "I am replacing the base version with my own"
public override void Process()
{
Console.WriteLine($"Charging credit card {CardNumber} for {Amount}...");
}
}
Now, when I run that same foreach loop, the magic happens. Even though the list is defined as List<Payment>, C# checks the actual object at runtime. It sees that the first object is a CreditCardPayment and calls the overridden version. The second object is just a Payment (or a PaypalPayment without an override), so it uses the base logic.
I personally find this incredibly powerful because it lets me write code that says "Process all these payments" without needing to know exactly what kind of payments they are. The objects themselves handle the specifics.
📋 Practical Task
Build a Versatile Game Character Ability System
You are building a combat system for a game. Instead of writing separate logic for every character type, you will use inheritance and polymorphism to handle their special attacks.
- Create a base class called
GameCharacterwith a property forNameand avirtualmethod calledPerformAttack()that prints "The character performs a basic attack!". - Create a derived class
Warriorthat overridesPerformAttack()to print "[Name] swings a massive claymore!". - Create a derived class
Magethat overridesPerformAttack()to print "[Name] casts a fireball!". - In your main program, create a
List<GameCharacter>containing at least one Warrior and one Mage. - Loop through the list and call
PerformAttack()on each character to demonstrate that the correct specialized attack is triggered for each type.
There are no comments for now.