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
163: The abstract and sealed Modifiers
A few years back, I was reviewing a pull request for a junior developer working on a payment gateway. He'd built a PaymentProcessor class with a ProcessPayment() method. The problem was, the PaymentProcessor itself didn't actually do anything—it was just there to ensure that CreditCardProcessor and PayPalProcessor shared the same interface. But because he forgot to mark the base class as abstract, another developer on the team accidentally instantiated new PaymentProcessor() in a factory method. The code compiled perfectly, but at runtime, it did absolutely nothing. We spent an entire afternoon hunting a bug that a single keyword could have prevented at compile-time.
Enforcing a Blueprint with Abstract
When you mark a class as abstract, you're telling the C# compiler, "This class is an idea, not a thing." You cannot create an instance of an abstract class using the new keyword. Its sole purpose is to be inherited from. This is incredibly useful when you have a general concept that doesn't make sense on its own, but provides a foundation for more specific versions.
Within an abstract class, you can also define abstract methods. These are methods that have no body—just a signature. By doing this, you're forcing any non-abstract class that inherits from it to provide its own implementation. It's essentially a contract. If you don't implement that method in the child class, the code won't even compile.
public abstract class PaymentMethod
{
public string AccountHolder { get; set; }
// Every payment method must have a way to authorize,
// but the "how" depends entirely on the specific method.
public abstract bool Authorize(decimal amount);
}
public class CreditCard : PaymentMethod
{
public override bool Authorize(decimal amount)
{
Console.WriteLine($"Authorizing credit card for {amount:C}...");
return true; // Logic for CC authorization goes here
}
}
I've found that using abstract classes is often cleaner than using interfaces when your derived classes share some actual logic (like the AccountHolder property above) but need to diverge on the critical behavior.
Locking Down Logic with Sealed
On the opposite end of the spectrum, we have the sealed modifier. If abstract is an open invitation to inherit, sealed is a "No Trespassing" sign. When you mark a class as sealed, no other class can inherit from it.
You might wonder why you'd ever want to prevent inheritance. In my experience, this usually comes down to two things: security and intent. Sometimes, a class is so specialized that allowing someone to override its behavior could introduce critical bugs or security vulnerabilities. Other times, it's a performance hint to the JIT compiler; since the compiler knows there are no derived classes, it can sometimes optimize method calls (devirtualization) because it doesn't have to look up the method in a virtual table.
public sealed class SecureVaultProcessor : PaymentMethod
{
public override bool Authorize(decimal amount)
{
// This logic is highly sensitive and must not be altered by a subclass
Console.WriteLine("Performing high-security vault authorization...");
return true;
}
}
// This would cause a compile-time error:
// public class HackyVault : SecureVaultProcessor { }
You can also use sealed on specific methods that are overrides of a base class. This allows a class to be inheritable, but tells any further descendants, "You can inherit from me, but you cannot change how this specific method works." This is a great way to maintain control over a class hierarchy as it grows.
📋 Practical Task
Building a Restricted Banking Transaction Hierarchy
Your task is to implement a simplified banking system that uses both abstract and sealed modifiers to ensure the system is robust and cannot be misused.
- Create an
abstractclass calledBankTransaction. It should have a property forAmountand anabstractmethod calledExecute()that returns a boolean. - Create a class called
Depositthat inherits fromBankTransactionand implements theExecute()method to print "Depositing funds..." and return true. - Create a
sealedclass calledInternalAuditTransferthat inherits fromBankTransaction. This class represents a sensitive system-level transfer that should never be modified or inherited from. ImplementExecute()to print "Executing secure audit transfer..." and return true. - In your
Mainmethod, instantiate aDepositand anInternalAuditTransferand callExecute()on both. - To verify your understanding, try to create a class that inherits from
InternalAuditTransferand observe the compiler error.
There are no comments for now.