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
22: Abstract Classes
A few years ago, I was lead dev on a project for a logistics company. We were building a system to handle different types of shipping carriers—FedEx, UPS, DHL, and a few local couriers. Initially, the junior dev on the team created a separate class for every single carrier. It worked for a week, but then we realized we were duplicating the same 50 lines of logging and validation code in every single class. When we tried to fix it by creating a base Carrier class, we hit a snag: someone accidentally instantiated a generic Carrier object in the code, which made zero sense. You can't "ship" something via a generic carrier; it has to be a specific one.
That's exactly where abstract classes come in. They allow you to say, "This thing is a blueprint for other things, but it's too generic to exist on its own."
Bridging the Gap Between Interfaces and Concrete Classes
You've already worked with interfaces, which are basically strict contracts. An interface tells a class what it must do, but it doesn't care how it does it. On the flip side, a concrete class is a fully realized object. An abstract class is the middle ground. It lets you provide some shared implementation (so you aren't repeating yourself) while still forcing derived classes to fill in the blanks for the parts that are unique.
When you mark a class with the abstract modifier, you're telling the C# compiler that this class cannot be instantiated. If you try to call new PaymentProvider() on an abstract class, the compiler will stop you dead in your tracks. This is a safety feature. It ensures that your code only ever deals with specific, fully-implemented versions of that base concept.
Enforcing Rules with Abstract Members
Inside an abstract class, you can have regular methods with bodies—these are the "shared" behaviors. But you can also define abstract methods. An abstract method is a method signature without any implementation. It's essentially a hole in the class that must be filled by any non-abstract class that inherits from it.
Check out this example. We want every payment provider to log the transaction the same way, but the actual processing logic is totally different for a Credit Card versus PayPal.
public abstract class PaymentProvider
{
// This is a concrete method. Every provider uses this exact logic.
public void LogTransaction(string transactionId)
{
Console.WriteLine($"Logging transaction {transactionId} to the secure audit trail...");
}
// This is an abstract method. There is no "generic" way to process a payment.
// We force the derived classes to figure this part out.
public abstract bool ProcessPayment(decimal amount);
}
public class CreditCardProvider : PaymentProvider
{
public override bool ProcessPayment(decimal amount)
{
Console.WriteLine($"Charging {amount} to Credit Card via Gateway...");
return true;
}
}
public class PayPalProvider : PaymentProvider
{
public override bool ProcessPayment(decimal amount)
{
Console.WriteLine($"Redirecting to PayPal API for {amount}...");
return true;
}
}
Notice the override keyword in the child classes. That's how C# knows you are intentionally fulfilling the contract set by the abstract base. I often tell people to think of the abstract class as the "Company Policy"—it defines the general workflow and the mandatory requirements, but leaves the specific execution to the individual employees (the derived classes).
📋 Practical Task
Build a Fleet Management Vehicle System
You need to create a system that handles different types of vehicles for a delivery fleet. Each vehicle has a common way of starting its engine, but the way they calculate their fuel efficiency is completely different based on their engine type.
Your requirements:
- Create an abstract class named
Vehicle. - Add a concrete method to
VehiclecalledStartEngine()that prints "Engine started!" to the console. - Add an abstract method to
VehiclecalledCalculateEfficiency()that returns adouble. - Create two derived classes:
TruckandElectricCar. - In
Truck, overrideCalculateEfficiency()to return15.5(representing MPG). - In
ElectricCar, overrideCalculateEfficiency()to return110.0(representing MPGe). - In your
Mainmethod, create aList<Vehicle>containing one truck and one electric car. Iterate through the list, callStartEngine(), and print the result ofCalculateEfficiency()for each.
There are no comments for now.