Why I "hate" optional parameters in C#

Why I "hate" optional parameters in C#

Mar 08, 2022

Let me start by saying that the title of this post is a little exaggerated in regards to how I feel about optional parameters. “Hate” is a strong word and I certainly don’t mean that optional parameters are a useless feature or that it is a bad practice to use them. It’s just that, I’ve rarely seen them used correctly and in almost all cases I see them being used, they bring more problems than solutions. After many long and painful debugging sessions, the sight of optional parameters in method declarations are accompanied by a despicable code smell the equivalent of a rotten egg, until proven otherwise. Allow me to elaborate and share my experiences.

What are they about?

Optional arguments, also known as default parameters (the most misleading AKA if I’ve ever heard one), were introduced in C# 4. Their intention was to provide a convenient and flexible way to omit function arguments for certain parameters whenever they are not required.

Optional does not mean Default

One of the biggest misuses of optional parameters is that of providing a default argument to a function. Although it is mandatory to provide a default value to an optional argument, it doesn’t mean that this feature should be used whenever you want to have some default value in a function. Specifying a default value is simply a requirement to use this feature, it is not the reason of it’s existence.

In fact, take a close look at the definition of optional arguments in Microsoft’s official C# Guide, and you’ll notice that there is nothing mentioned of using it for the purpose of passing a default value to a function.

Optional arguments are compile-time constants

There are quite a few issues that may arise when using optional arguments for the purpose of providing a default value to a function.

Optional arguments are compile-time constants, meaning that the default value is embedded at the caller side, not the callee. This may lead to unexpected results at the caller side. Consider the following segment of code where a function DoSomething is defined in a class called LibA:

public class LibA
{
   public string DoSomething(string optional = "FromLibA")
   {
       return optional;
   }
}

The above class exists in its own separate library. Now, consider the following code where the DoSomething function is consumed:

LibA libA = new LibA();
Console.Write(libA.DoSomething());

The output of the above will obviously be “FromLibA”. But now consider the scenario where a change is made in the LibA class, and the default value of the optional parameter is changed to something else. The above console output would still be “FromLibA”. In order for the change to “take” then both the LibA assembly and the consumer assembly need to be compiled.

Difference in polymorphic behavior through interfaces

Furthermore, there may be a difference between the optional values when using them through an interface function and when using them through a concrete implementation, if the default value definition does not match.

Consider the following example:

public interface IMyInterface
{
    string DoSomething(string optional = "FromInterface");
}
public class LibA : IMyInterface
{
    public string DoSomething(string optional = "FromLibA")
    {
        return optional;
    }
}
LibA caller = new LibA();
IMyInterface interfaceCaller = new LibA();

Console.WriteLine(caller.DoSomething());
Console.WriteLine(interfaceCaller.DoSomething());

The output of the above two statements will be different. In the case of calling the concrete class (caller) the output will be “FromLibA”, and in the latter it will be “FromInterface”. This can become particularly confusing in cases you are using dependency injection through interfaces because it is not clear which default value is being used.

Alternatives

If you need to have a default parameter which can be optionally overridden by the caller, there are plenty of alternatives:

  • Use function overloading with the parameter in question and have the caller decide if they want to provide a value for that parameter by calling the appropriate overload. If the original function instead of the overloaded one is used, then the value for that parameter can be defined within the function body.

  • In case of multiple properties, create a dedicated type to hold those properties and use that in a function overload. This would allow you to easily extend the parameters in the future if needed.

  • Abstract away the default value so it is provided by another component or service. This is cleaner and decouples the actual value from the consumer.

Same-type parameter ambiguity

To add insult to injury, when you have an interface which specifies a default value for an optional parameter, you are allowed to ignore the parameter on a class that implements that interface. This can become a problem when your class implements multiple interfaces with functions which use the same number and type of parameters. In order to call the appropriate function you would need to call it through the interface.

When its OK to use them

To simplify the calling of class constructors, and other creational mechanism functions (i.e. factory) which have a long list of complex arguments, and to avoid having to create multiple convoluted overloads to cover all possible combinations. That’s strictly it. Notice that when I refer to a long list of arguments, I strictly mean that in the context of object creation, and even more so for class constructors.

If you ever find yourself at a point where you have multiple optional parameters for non-constructors or non-factory object creation functions, then your function is almost certainly doing too much, and you should refactor your code to break things down in smaller pieces (see SRP).

When you MUST use them

Almost never. There is literally no case (or at least I’ve never had a case in my years of coding) where I absolutely had to use optional arguments. As mentioned above, optional arguments is a convenience mechanism, it is not a critical language feature which you cannot live without.

As a rule of thumb

  • When coming across optional parameters in code reviews, always treat them as a code smell, until you’ve taken a closer look at the implementation and ensured that they are prudently used. This is even more important when coming across optional parameters in public functions.

  • Always prefer function overloading before optional parameters.

  • If an argument alters the flow of execution within a function, it should unequivocally not be optional.

A piece of “harsh” advice

  • Avoid using same-type optional parameters sequentially. If you see some piece of code which is doing that, refactor immediately.

  • Avoid using optional parameters in public functions altogether. Use them only for private/internal functions, and always with caution.

  • When specifying values for multiple optional arguments, make it a habit to use named arguments when calling a function.

  • Avoid using optional parameters in interface functions.

Enjoy this post?

Buy thecodewrapper a coffee

More from thecodewrapper