Inheritance vs. Partial Classes vs. Extension Methods
Possibilities for Code Extension
There are several methods for extending code, not only your own code but also existing code in the form of libraries. Inheritance is a means by which the functionality of classes can be extended, but there are cases where inheritance falls short. We will present two alternatives to extending code functionality, namely partial classes and extension methods.
Partial Classes
The principle of partial classes is simple: we can split a class from the same namespace into multiple source files by applying the partial keyword to the class. In essence, we can declare a class with a portion of its logic in one file and the rest of the methods/properties/fields in another file.
// We can declare this partial class in the file SplitClass.cs
public partial class SplitClass
{
public SplitClass(int field)
{
Field = field;
}
}
// The rest of the class can be declared in the file SplitClass.Properties.cs
public partial class SplitClass
{
public int Field { get; }
}
The question arises: why would we need partial classes declared in separate files when we could declare them all in a single file, as it wouldn't represent a disadvantage but rather complicate code tracking? The reason is that there can be code generation tools that generate the remaining part of a class (properties and methods) automatically, given a part of the class. If the class is partial, the rest of the generated implementation can be kept separate without us having to interfere with it. This generated code can be complex and optimized, and we may not want to modify it. For instance, in .NET MAUI, to implement various interface components, part of the code is automatically generated based on XML files, and the rest of the implementation is left for developers to complete.
Furthermore, if we have private methods and properties, they become accessible for implementing new logic or for code generators to create logic based on them. If inheritance were used, the private or protected access modifier would work in only one direction, not both. However, there are cases where we don't want to expose those fields to other classes.
Extension Methods
More commonly used than partial classes are extension methods. An extension method is a static method in a static class that can be called as if it were a method of another class. With this approach, we can extend existing classes with new methods without using inheritance, while having these methods readily available just like any other class method. This makes the code more elegant and easier to follow.
// Create a static class to group extension methods
public static class IEnumerableExtensions
{
// Create a static method with the "this" keyword as the first parameter along with other parameters
public static IEnumerable<TOut> Map<TIn, TOut>(this IEnumerable<TIn> enumerable, Func<TIn, TOut> selector)
{
foreach (var item in enumerable)
{
yield return selector(item);
}
}
public static IEnumerable<T> Filter<T>(this IEnumerable<T> enumerable, Func<T, bool> predicate)
{
foreach (var item in enumerable)
{
if (predicate(item)) {
yield return item;
}
}
}
}
IEnumerable<int> items = new int[] { 0, 1, 2, 3, 4 };
Console.WriteLine("The projected items are: ");
foreach (var item in items.Filter(e => e % 2 == 1).Map(e => (3 * e).ToString()))
{
Console.Write("{0}, ", item);
}
As seen, we have declared an extension method and used it like any other method of the IEnumerable<T> interface. This is possible because there is no distinction between static and non-static methods in terms of the compiler; non-static methods differ only in that hidden 'this' parameter. Here, this is achieved by creating a static method with a 'this' parameter at the beginning to simulate a non-static method of a class.
Extension methods have the limitation that they don't have access to non-public fields. However, they are extremely useful and commonly used in C# to extend existing code. They not only ensure that new code is compatible with the old, but they also provide an artifice to seamlessly and safely introduce new functionalities.
An example of this is LINQ, where the IEnumerable<T> interface existed long before extension methods were introduced, as did many collections. If additional inherited methods were added, it could have led to intermediate code compatibility issues at runtime for various reasons (related to the implementation of inherited methods through virtual function tables). This could have resulted in many libraries becoming incompatible with applications that use them. Thus, extension methods elegantly solve this problem by avoiding incompatibilities and the need to rewrite existing libraries with new classes and interfaces.
Extension methods are beneficial for maintaining backward compatibility and for enhancing code quality and comprehension. A pattern that can be applied through extension methods is method chaining, similar to LINQ methods, or using this pattern to lead to the builder pattern. In the builder pattern, methods are chained together to configure a complex object based on the desired configuration.