5

Recently, I wanted to add an optional parameter to an extension method. The original method looked like this:

public static class Extensions {
    public static bool Foo(this IFoo target) {
        target.DoIt(true);
    }
}

This is obviously a simplified version but let's go from that.

What I did is:

public static class Extensions {
    public static bool Foo(this IFoo target, bool flag = true) {
        target.DoIt(flag);
    }
}

All I did is introduce an optional parameter with a default value, right? What I was expecting was the compiler to generate the overloaded method, without the flag. This partially happened. Any code that I recompiled was able to compile and execute without any problems, even without the parameter like this:

...
IFoo foo = new FooBar();
foo.Foo();
...

However, any code that was built against the previous version of Foo() did not work, throwing the following exception:

Unhandled Exception: System.MissingMethodException: Method not found: 'Boolean Models.Class1.Foo()'.
at DefaultParamsTests.Program.Main(String[] args)

This is obviously a problem for us as we do have a public API that our customers do leverage and this would be a breaking change.

The solution is to explicitly create an overload:

public static class Extensions {
    public static bool Foo(this IFoo target) {
        target.DoIt(true);
    }

    public static bool Foo(this IFoo target, bool ) {
        target.DoIt(true);
    }
}

However, Resharper does suggest that I could introduce an optional parameter for method foo.

resharper introduce optional parameter

If I do follow the refactoring it basically does what I showed above. However, that won't work for existing code.

resharper refactored

I have looked at the generated IL using both Reflector and dotPeek. Neither is showing the generation of the overload.

What am I missing?

Eric Liprandi
  • 5,324
  • 2
  • 49
  • 68
  • What causes you to believe that the compiler would generate two method overloads for a single declaration? – gknicker Jan 28 '15 at 04:08
  • 1
    I think you have answered your question already. Can you just add the method and ignore Resharper's suggestion to introduce the optional parameter? – maxwellb Jan 28 '15 at 04:11
  • @gknicker, obviously, an overload is created somewhere. Otherwise, default parameters would not work. As pointed out by the two people who answered the question, I was assuming it was generated in the wrong place. The default value is injected by the compiler at the call-site instead of the declaration site. – Eric Liprandi Jan 28 '15 at 04:16
  • @maxwellb, yes, that is how I ended up "fixing" it. I was just curious. You and Andrew did a great job answering it. I even ended up confirming this with dotPeek. And yes, the default value is injected at the call site. – Eric Liprandi Jan 28 '15 at 04:18
  • Optional parameters direct compiler to insert appropriate defaults at the call site. Recommend you avoid optional parameters in a public API. See also http://stackoverflow.com/questions/2674417/conflicting-overloaded-methods-with-optional-parameters – gknicker Jan 28 '15 at 04:19
  • 1
    First, what a well written question. Very clear, and with lots of good code snippets. SO needs more of these nowadays. I should mention, I turn off a lot of resharper's suggestions. Some harm readability, and some only apply 1/2 the time. This is one I am pretty sure I turned off – Andrew Jan 28 '15 at 04:21

3 Answers3

5

In the case of default parameters, the actual call site is re-written to use the default value. This means an overload isn't generated at all, your calling code is modified!

public void fun(int x = 3) { }

// in another file
fun();   // compiler re-writes to fun(3);
fun(7);  // fun(7) as expected

// actual code generated by the c# compiler
fun(3);
fun(7);

If you use optional parameters in C# you need to re-compile all callers when the function changes. For this reason it is strongly discouraged to put these kind of methods on a public interface (e.g. called by other's code). Using them inside your own linked projects is fine, since they should all compile at the same time

Andrew
  • 8,322
  • 2
  • 47
  • 70
  • 1
    The other gotcha is that you cannot reliably "update the default value". Any code compiled when the default value was "foo" will continue calling the method with "foo". Updating the default value to "bar" will only get called as such for new code built with that definition. – maxwellb Jan 28 '15 at 04:09
  • 2
    @maxwellb that is the same issue. The compiler re-wrote the call for you, so you have to re-compile. If it' internal code, only used inside your projects (and has proper dependencies) then they are a nice time-saver in some cases. I always use overloads except for `internal` classes, just in case someone abuses what I wrote :D – Andrew Jan 28 '15 at 04:12
  • 1
    exactly. I meant to add to your response with another example of why optional parameters have "gotchas" and should be used sparingly. – maxwellb Jan 28 '15 at 04:13
  • 1
    Trying to think of a better way to write the example... I'd like to make the answer clearer – Andrew Jan 28 '15 at 04:15
4

Optional parameters in methods in C# are resolved at compile-time at the call site. Your calling code that omits the parameter expands to include the parameter, calling the two-parameter version. This is the opposite of what you are presuming happens, with creating an overload.

maxwellb
  • 13,366
  • 2
  • 25
  • 35
1

The key here is that the compiler changes the caller of the method to include the default value.

Take a look at the NEW code in dotPeek.

NOTE: You should delete the pdb files when using dotPeek on your own files!

Here is an extensions class with two methods (decompiled with dotPeek). One with a default and one without

public static class Extensions
{
    public static bool Foo(this IFoo target)
    {
        return target.DoIt(true);
    }

    public static bool Foo(this IFoo2 target, bool doit = false)
    {
        return target.DoIt2(doit);
    }
}

Everything looks fine there. Now look at the object calling the extension method (decompiled with dotPeek).

public class Bar
{
    public Bar(IFoo foo)
    {
        Extensions.Foo(foo);
    }

    public Bar(IFoo2 foo)
    {
        Extensions.Foo(foo, false);
    }
}

Notice that the caller to the IFoo2 extension method actually contains the value false to the method Foo of class Extensions. So your method Extensons.Foo now has two parameters in it. The first method the object and the second being the "default" parameter. So any code that was created with the first version will now fail because there is no method with only one parameter.

Disable resharper around the methods and you won't see it pester you anymore :)

Shawn Kendrot
  • 12,425
  • 1
  • 25
  • 41