Codementor Events

Method overloads vs optional parameters in C#

Published Jul 06, 2022Last updated Jul 12, 2022
Method overloads vs optional parameters in C#

I have a colleague who hasn't yet seen the light, but is transitioning across to C# from Visual Objects, which had a larger market share back in the 90s than it's current presence on the internet would suggest...

We came to a discussi,on about why C# supports both method overloads and optional parameters and my colleague's position was that "good programming" requires neither of these concepts.

"They only serve to confuse other developers and they lead to code that violates DRY and SOLID principals."

Whilst I strongly disagree with my colleague, it wasn't an easy conversation to control before it degraded into an argument. I am very proud and defensive of my C# so I thought I'd take the time now to collect my thoughts on the subject in a way that hopefully helps other people in my position.

More on DRY and SOLID principals:

Method Overloads

In C# our code bases and the framework itself are littered with examples of method overloads, especially in the form of extension methods, System.Convert is a classic example. Because C# is strongly typed method overloads become not only helpful but necessary. BUT, we don't use them for everything. If you were following good design principals you would have a single method with the specific input parameters, you wouldn't create multiple versions of the same method with slightly different implementations. For the case of System.Convert, In loosely typed languages (like Visual Objects) you might use an untyped parameter to represent the value parameter instead of all the individual type specific overloads for method like Convert.ToDouble.

In C# we could specify an object parameter, effectively making the parameter untyped through the process of boxing. But this comes with performance penalties and makes the code ambiguous, especially given that not all possible types are supported, only a specific subset of types. For a global API in a language that prides itself on self-documentation, using object typed parameters is strongly discouraged as it doesn't provide any information at all about the types that this method was designed and assumably tested.

You might say that it would still be better for the ConvertTo methods to use object and internally resolve the different type implementations, however doing this would mean that every call to ConvertTo would be forced to implement boxing, and so must always wear the performance penalty, if instead we force the caller to manage any type resolution that might be needed, then worst case is that we are allowing the caller to determine if or when boxing might be required. Overall, the methods are designed to help the caller avoid any boxing at all.

And that it where we come to an impasse regarding the implementation of Method Overloads. It is more code than is strictly necessary, creates more combinations of logic that must be tested and documented which overall makes this code harder to maintain. Therefor we need to justify this additional level of effort, you must be able to justify the existence of each of the overloads and they should provide value to the developers who are going to consume those methods.

This is the key point, situations where overloads are easily justified are in public APIs or when you are writing library code with the specific desire for other developers to consume it. The reason that we use overloads and not un-typed parameters is so that we can specifically convey the expected usage of the methods and importantly the usage variants that have been tested.

Every line of code I write to consume someone else's assemblies has the potential to fail, but it will be MY fault. A good framework or API author will provide different implementation options that suit standard patterns of compatibility so that callers are less likely to make silly mistakes. [1]

The author in this case is taking responsibility to test these implementations so that you don't have to. The overloads in ConvertTo also greatly assist in code discovery, because we know the implementation is exhaustive (as idicated by the number of overloads) then we know how to explore conversion logic for other types that we might not be familiar with.

For System.Convert the added value of this overloaded implementation is that each method has been optimised to achieve the best practical performance. This ubiquitous API is used so often by much of your own code, but also that of 3rd party libraries and the rest of the framework itself, that it makes sense that a lot of effort has been put into it. You wouldn't normally define as many input variations via overloads unless you expected a similar level of usage as seen with System.Convert.

It is worth pointing out that System.Convert technically violates a number of SOLID principals, or it would if this class was intended for extension or inheritance, but it is a static collection of utilities and is not intended to support inheritance or extension.

Generics

Later, in C# v2.0 Generic Types were introduced, which allow us to define a type filter pattern, that will match specific types instead of using an un-typed object parameter. For the case of System.Convert generics still does not reduce the complexity of the class because there is no type expression that can be used to qualify all or even a sub-set of the supported primitive input types.

For the application code that you write, you should consider Generic implementations before overloads, especially if the overload exists only to give support to multiple types that share a common base implementation or interface.

A good solution in your own API designs that can help with Generic implementations is to define interfaces of specific structures that your API requires and use those interfaces as parameters or Generic method implementations. This allows developers to define their own structures and encourages them to follow good SOLID principals without enforcing a common base type to inherit from, only that their types should conform to a specific interface.

This leads us to the Composition over Inheritance discussion, when we are writing libraries and public APIs for re-use, composition patterns are a lot easier for other developers to conform to.

  • I am actually not happy with most of the examples of this argument I find on the net, everyone seems to understand Inheritance but many examples of Composition are misleading or at least do not relate to realistic commercial code implementations. This non-C# article is acceptable for now Inheritance vs. Composition especially this statement:

    Sometimes, it’s true that we use inheritance inappropriately. The Liskov Substitution principle would be the most important guideline to determine whether inheritance fits your design or not.

    For instance: B is inherited from A. Use Liskov Substitution to justify the relationship between B and A, then reverse the relationship and justify it again. If the relationship seems to make sense in both directions, you’re better off not using inheritance.

  • I'll follow up with my own post later, but if anyone can find a source that better describes the "has-a" relationship with a real-world code scenario, not using objects in a house, then please comment to this post!

It is interesting to note that while this advice recommends Composition over Inheritance, as does Microsoft .NET: Architecting Applications for the Enterprise, Second Edition, it doesn't mean you shouldn't use or encourage Inheritance. In API and utility library designs, Interfaces and other composition concepts, will make your code easier to consume because the calling code will not have to make significant structural changes to their class designs. This post on Stack Overflow is an example where OP took the concept too literally: Composition instead of inheritance without recognising how composition can be achieved.

Optional Parameters

Optional Parameters were added in C# 4.0 (VS 2010) to address a specific issue with C++ and COM Interoperability for interfaces like the Microsoft Office Automation APIs. Named and Optional Arguments (C# Programming Guide)

Effectively this resolves to the compiler as method overloads. The common use case for Optional parameters, when not using COM+ is to avoid multiple overloads where each overload adds an additional parameter.

Optional parameters is a solution if your code with overloads looks like this:

Method-Overloading-in-C.png source: Method Overloading in C# with Examples

That is each additional method has the same parameters as the previous one, but with one (or more) additions. Optional and Named Parameters has advanced beyond this simple implementation but ultimately because these methods are resolved by the complier using the same prototype inference rules as method overloads it doesn't make sense to combine overloads with optional parameters. When you do it becomes very confusing, the compiler will only allow unique combinations of parameters in prototypes to compile, but when the compiler fails it can be hard to track down which of the implementations is the duplicate.

It can also attract discussions like this: https://thedailywtf.com/articles/comments/extending-yourself

Optional Parameters do solve another important issue in the evolution of a project. Provided that you make sure that when the parameter is omitted the behaviour of the method is unchanged, then you can use this technique to extend existing code bases without breaking existing functionality. Technically I still regard this as an anti-pattern, but backwards compatibility is an issue if your library or API is widely distributed and you don't want to introduce breaking changes between APIs.

The better solution is sometimes to create a new class to perform the new functionality, the name of the new class should indicate the new behaviours that have been added, or you can be boring and put an incremental indexer on the class name to distinguish between the FactoryWidget and the FactoryWidget2.

If it happens once, it is likely to happen again in the future right?

Then if the old functionality is entirely contained in the new class, you could re-write the original class as a wrapper, in this way you are still adhering to the DRY principal. If you do this, you should decorate the original class with the ObsoleteAttribute and a message directing the user to the new functionality.

Creating a wrapper class in this way does violate the Poltergeist (or Ghost) anti-pattern but when coupled with the [Obsolete] attribute this is the expected way to introduce breaking changes to a component library over an extended product lifecycle or road map. You let developers know that you intend to remove or replace functionality in the codebase a few versions ahead of when you might actually remove the method or class.

You may not ever need to remove features, but you might still want to encourage developers to use the new classes or functionality for performance or security reasons, without forcing them to re-code their legacy applications. MS SQL Server is a great example where due to the extended userbase covering multiple compatibility levels (versions) we have been given fair warning to stop using TEXT, NTEXT and IMAGE data types and that they will be Deprecated in a future version of SQL Server. But these features still remain to ensure backward compatibility, to simplify in-place upgrades to newer compatibility levels.

Conclusion

The C# language specification has really matured and contains multiple different functional paradigms and syntax options to help you express your code in your way. Often different implementations will reflect the ideology of specific versions but the goal has ever been to assist developers to write simple, modern, general purpose managed applications. If Anders Hejlsberg had his way, we would be writing Cool code today![2]

There are many valid scenarios for both Optional Parameters and Method Overloads. These are simply tools that are designed for you to be more expressive in your code that other developers will be consuming. With that said, I advise the following guidelines:

  • Optional Parameters should be generally reserved for use in scenarios where the logic flow within the method and general outcome is unaffected by the omission of the parameter, default parameter value should be intuitive.
    If the interface you are coding against (like COM +) requires the use of optional or named parameters then you must use them, don't try to fight it. But if you are the author of the API ensure you have string justification for their use.

  • Optional Parameters offer a minimal solution to extending existing APIs without affecting existing implementations, however this scenario is usually an indication that the original method was poorly designed as it wasn't Open to extension.

  • Generic Implementations should be preferred to Method Overloads where practical.

  • Do not mix Optional Parameters and Method Overloads in the same class, just don't, OK

  • Beware of Premature Generalization (Anti-Pattern)


For some of this discussion, I had to reflect on the timeline from The history of C#. I have been coding in C# since version 1 and have only positive experiences during the progressive evolution over the years, even when DateTime was dropped from OData. However, there are many aspects of the framework that would likely be designed differently given the new language and syntax features that we have now. That doesn't mean we need to go back and change everything, as that would affect a lot of existing and legacy applications. It does however mean that we should put some effort into finding good examples of code patterns and implementations from authoritative sources.

For all things C#, https://docs.microsoft.com/en-us/dotnet/csharp is the authoritative source.

For another real world example of this issue and an opposing point of view, have a read of this question on Stack Overflow: Why UseWelcomePage is implemented the way it is?

Final thought - Ignore this advice where it is not appropriate!

This can't be overstated enough, I'm even going to make it my email tag line. Ignore this advice where it is not appropriate! This is a thought process, we haven't considered all possible outcomes and I do not profess to know everything about C# or importantly your codebase. If it is beneficial to you, then please use this advice or any of the material that I have referenced, when you have a specific need to violate any of these tenets please do so and even post a summary of your experience.

For those of you who recognise this sentiment, yes I brazenly stole it from Extension Methods Best Practices. You can also read more about this concept here: IGNORE THIS ADVICE 😉


  1. Chris Schaller, https://stackoverflow.com/a/70282058 ↩︎

  2. Hamilton, Naomi (October 1, 2008). "The A-Z of Programming Languages: C#". Computerworld. Archived from the original on May 18, 2019. Retrieved October 1, 2008. ↩︎

Discover and read more posts from Chris Schaller
get started