virtual
, partial
, override
are keywords that provide each a specific functionality to classes, method or properties.
Let’s start with partial
.
partial
Keyword
The partial
keyword allows a class or struct to be split into multiple files, though I’ve mainly used it with classes. This can be useful for organizing code, especially in large projects or when working with code generators.
For example
- File 1:
Person.Part1.cs
|
|
- File 2:
Person.Part2.cs
|
|
In this example, the Person
class is split into two parts. The compiler will combine these partial classes into one class at compile time.
In my experience, it’s a very useful feature when you want to refactor legacy code on which you can’t apply the S.O.L.I.D. principles right away. See that keyword has the gateway to S.O.L.I.D. principles by breaking down into smaller pieces your legacy code to reveal how you can refactor it into clean code.
virtual
Keyword
The virtual
keyword is used to mark a method or property in a base class that can be overridden by derived classes. It allows derived classes to provide a specific implementation for the method or property.
For example:
|
|
Here, the Speak
method is marked as virtual
, meaning it can be overridden in any class that inherits from Animal
.
That leads us to the third keyword
override
Keyword
The override
keyword is used to extend or modify the virtual/abstract method or property defined in a base class. When a method is marked with override
, it provides a new implementation for a method inherited from a base class.
For example, taking our previous Animal
class:
|
|
In this example, the Dog
class overrides the Speak
method of the Animal
base class. When Speak
is called on an instance of Dog
, it will execute the overridden method in the Dog
class.
Combined Example
Let’s combine all three concepts in one example for better understanding.
|
|
In this combined example:
- The
Vehicle
class is split into two partial files. - The
Vehicle
class has avirtual
methodDisplayInfo
and avirtual
propertyWheelCount
. - The
Car
class inherits fromVehicle
and- overrides the
DisplayInfo
method with theoverride
keyword. - overrides the
WheelCount
property to set a value.
- overrides the
When DisplayInfo
is called on an instance of Car
, it uses the overridden method from the Car
class.
Caveats when using virtual and override
Using virtual
and override
in C# provides powerful mechanisms for creating flexible and extensible object-oriented designs. However, there are some caveats that can affect code maintainability and understanding, especially for new developers. Here are some potential issues:
Complexity and Indirection
When using virtual
and override
, the actual implementation of a method might not be in the class where it’s called. This can make the code harder to follow, as developers need to understand the class hierarchy and locate the overridden methods.
In the Animal
and Dog
example, myDog.Speak()
calls the overridden Speak
method in Dog
, not in Animal
. Understanding this requires knowledge of both the base class and the derived class.
Maintenance Overhead
As the hierarchy of classes grows, maintaining the code can become challenging. Changes in the base class can have unintended effects on derived classes. Developers must carefully consider the impact of modifying virtual
methods.
Hidden Behavior
Overridden methods can sometimes hide important behavior from the base class, leading to unexpected results or bugs that are hard to trace.
For example:
|
|
In this example, the PremiumAccount
class’s Withdraw
method doesn’t call the base class’s BankAccount
method, missing important behavior (e.g., validation logic) defined in the base class.
Fragile Base Class Problem
The “fragile base class” problem occurs when changes to a base class break the functionality of derived classes. This can happen if derived classes rely on specific behaviors of base class methods that are modified.
Increased Testing Efforts
Testing becomes more complex because you need to test both the base class and each derived class’s behavior. This is especially true if the virtual
methods have complex logic.
What Can You Do To Mitigate These Issues
First, clearly document the purpose and expected behavior of virtual
methods and their overrides. This helps new developers understand the intended use.
Then, follow design principles like S.O.L.I.D. to create more maintainable and understandable code. For instance, the Liskov Substitution Principle (LSP) can guide you in ensuring that derived classes can replace base classes without causing issues.
Next, ensure that overridden methods call the base method, when necessary, to maintain expected behavior.
Also, use the sealed
keyword on overrides to prevent further overriding in derived classes when appropriate.
Finally, avoid deep and complex class hierarchies where possible. Favor composition over inheritance if it makes the design simpler and more understandable.
More About sealed
Keyword And Composition
The sealed
Keyword
In C#, it’s used to prevent a class from being inherited or a method from being overridden further.
When applied to a class, it indicates that the class can’t serve as a base class.
When applied to a method that overrides a virtual method, it prevents further overriding of that method by derived classes.
|
|
-
Sealed Method: in a derived class, it prevents further overriding of the method.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
public class BaseClass { public virtual void Display() { Console.WriteLine("Base class display method."); } } public class DerivedClass : BaseClass { public sealed override void Display() { Console.WriteLine("Derived class display method."); } } // The following will cause a compile-time error public class SubDerivedClass : DerivedClass { public override void Display() { } }
Composition
Composition is a design principle where a class is composed of one or more objects of other classes, rather than inheriting from them. It’s a “has-a” relationship as opposed to inheritance’s “is-a” relationship. Composition provides greater flexibility and promotes code reuse.
For example:
-
Example 1: Simple Composition
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
public class Engine { public void Start() { Console.WriteLine("Engine started."); } } public class Car { private Engine _engine = new Engine(); public void StartCar() { _engine.Start(); Console.WriteLine("Car started."); } } public class Program { public static void Main() { Car myCar = new Car(); myCar.StartCar(); // Output: Engine started. Car started. } }
In this example, the Car
class uses an instance of the Engine
class. The car “has an” engine, demonstrating composition.
-
Example 2: Interface-Based Composition (or what you may know Dependency Injection, generally preferred to the example 1 code)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
public interface IEngine { void Start(); } public class ElectricEngine : IEngine { public void Start() { Console.WriteLine("Electric engine started."); } } public class GasolineEngine : IEngine { public void Start() { Console.WriteLine("Gasoline engine started."); } } public class Car { private IEngine _engine; public Car(IEngine engine) { _engine = engine; } public void StartCar() { _engine.Start(); Console.WriteLine("Car started."); } } public class Program { public static void Main() { IEngine electricEngine = new ElectricEngine(); Car myElectricCar = new Car(electricEngine); myElectricCar.StartCar(); // Output: Electric engine started. Car started. IEngine gasolineEngine = new GasolineEngine(); Car myGasolineCar = new Car(gasolineEngine); myGasolineCar.StartCar(); // Output: Gasoline engine started. Car started. } }
In this example, the Car
class is composed with an IEngine
interface, allowing it to use different engine implementations. This demonstrates the flexibility of composition.
Sources for This Article
You can read more on the topic with the following resources. What I shared here is only the tip of the iceberg and feel free dive deeper.
- Microsoft Documentation: The official documentation provides detailed explanations of C# keywords and design principles.
- Design Patterns: The book “Design Patterns: Elements of Reusable Object-Oriented Software” by Erich Gamma et al. provides insights into composition and other design principles.
- SOLID Principles: Various online resources and articles explain SOLID principles, such as the Liskov Substitution Principle, which relates to inheritance and composition.
These sources offer a comprehensive foundation for understanding the use of these keywords and design techniques in C#.
Follow me
Thanks for reading this article. Make sure to follow me on X, subscribe to my Substack publication and bookmark my blog to read more in the future.
Photo by Đan Thy Nguyễn Mai