34

I am aware that adding an optional parameter in a library method is a breaking change,

void Foo(int x)             // OLD
void Foo(int x, int y = 5)  // NEW

because in the compiled code the new version is seen as Foo(int, int). Every call of Foo(0) (source code) is translated to Foo(0, 5) (compiled code) by the compiler. Thus, an old client, using a compiled call of Foo(0) would not find a suitable method.


What about the other direction?

void Foo(int x, int y = 5) { ... }    // OLD

void Foo(int x)        { Foo(x, 5); } // NEW
void Foo(int x, int y) { ... }        // NEW

Foo(0) (source code) would still compile, and Foo(0, 5) (compiled code) would still find a suitable overload, so, theoretically, this should work.

Does it work in practice, i.e., is this scenario "officially supported" by the .NET runtime and the C#/VB compilers? Or are calls to methods with optional parameters somehow "marked", causing them to fail when the optional parameters are replaced by overloads?


EDIT: To clarify, I'm asking about binary compatibility: Is it possible to replace library.dll (old) with library.dll (new) without recompiling projectUsingLibrary.exe?

Community
  • 1
  • 1
Heinzi
  • 167,459
  • 57
  • 363
  • 519
  • Maybe if the client has to be recompiled (which I would assume for removing overloads), this could be considered a breaking change. If you could simple xcopy the new library without the client being recompiled, this could be considered a non-breaking change. – Uwe Keim Oct 01 '12 at 09:37
  • 7
    I would *expect* it would be fine; but: have you tested it? should only take a few minutes with 2 projects – Marc Gravell Oct 01 '12 at 09:37
  • 1
    @UweKeim: That's exactly my question: Does the client need to be recompiled or not? – Heinzi Oct 01 '12 at 09:39
  • 4
    @MarcGravell: Even if it works in tests, I'd be curious to know if it works because *it's specified like this and supposed to work* or if it just works "by accident" (because the particular CLR I'm using decides to be "generous"). That's what I meant by "officially supported". – Heinzi Oct 01 '12 at 09:41
  • @Heinzi Oops, sorry, I mis-read your question. Maybe changing the version number would be somehow involved, too? – Uwe Keim Oct 01 '12 at 09:42
  • What if the library is used in a project compiled in an older version of .NET (for example 2.0)? – Guillaume Oct 01 '12 at 09:45
  • It's fine if `Foo(x, 5)` is equivalent to `Foo(x)` for all `x`. – CodesInChaos Oct 01 '12 at 09:47
  • @Guillaume: If the language supports optional parameters (e.g. VB.NET), I don't see why the .NET version should make a difference. – Heinzi Oct 01 '12 at 09:48
  • Out of interest, why would you want to do this? – Jodrell Oct 01 '12 at 10:00
  • 3
    If you are not using strong named assemblies with versioning this should be fine, cause the method Foo(int,int) will be found during runtime. So there is no need to recompile your exe – Jehof Oct 01 '12 at 10:04
  • @Jodrell: We originally had only `Foo(int)` [Version A]. Then, not knowing that this would break compatibility, we changed it to `Foo(int, optional int)` [Version B]. I'm now wondering whether I can fix this mess by creating a Version C: `Foo(int); Foo(int, int)`, which is binary compatible with clients compiled against Versions A and B. – Heinzi Oct 01 '12 at 10:20

2 Answers2

12

I thought that was a good question, so here goes my take.

Using a quick client that does this:

        c1.Foo(1);
        c1.Foo(1, 2);

When using optional parameter the client IL looks like:

    IL_0000: nop
IL_0001: newobj instance void [ClassLibrary1]ClassLibrary1.Class1::.ctor()
IL_0006: stloc.0
IL_0007: ldloc.0
IL_0008: ldc.i4.1
IL_0009: ldc.i4.5
IL_000a: callvirt instance void [ClassLibrary1]ClassLibrary1.Class1::Foo(int32, int32)
IL_000f: nop
IL_0010: ldloc.0
IL_0011: ldc.i4.1
IL_0012: ldc.i4.2
IL_0013: callvirt instance void [ClassLibrary1]ClassLibrary1.Class1::Foo(int32, int32)
IL_0018: nop
IL_0019: ret

and when using overloads it looks like:

    IL_0000: nop
IL_0001: newobj instance void [ClassLibrary2]ClassLibrary2.Class2::.ctor()
IL_0006: stloc.0
IL_0007: ldloc.0
IL_0008: ldc.i4.1
IL_0009: callvirt instance void [ClassLibrary2]ClassLibrary2.Class2::Foo(int32)
IL_000e: nop
IL_000f: ldloc.0
IL_0010: ldc.i4.1
IL_0011: ldc.i4.2
IL_0012: callvirt instance void [ClassLibrary2]ClassLibrary2.Class2::Foo(int32, int32)
IL_0017: nop
IL_0018: ret

So, if you changed the implementation from optional to overloads, but left the client as it was originally, it would be effectively adding the default parameter for you, and always calling the the function that has two arguments, which may or may not be the desired behaviour.

Justin Harvey
  • 14,446
  • 2
  • 27
  • 30
  • Thanks, that's very helpful because it shows that on IL level the call to `Foo(int, opt int)` and `Foo(int, int)` are the same. About calling the "wrong" function: That's not an issue, I've clarified this in my question (Foo(int) just calls Foo(int, 5)). – Heinzi Oct 01 '12 at 10:30
4

I am not sure if my method of testing was the best but here is what I discovered starting with: (apologies for class and namespace names)

namespace ClassLibrary1
{
    public class Class1
    {
        private int x;
        private int y;

        public void Foo(int x)
        {
            Foo(x, 0);
        }
        public void Foo(int x, int y = 5)
        {
            this.x = x;
            this.y = y;
        }
    }
}

I built this and added the dll to a console app in a different solution and referenced the dll by browsing to it:

using ClassLibrary1;

  namespace ConsoleApplication1  
  {
        class Program
        {
            static void Main(string[] args)
            {
                var c = new Class1();

                c.Foo(1);
                c.Foo(2, 3);
                c.Foo(3, 5);
            }
        }
    }

I then changed the method signatures of the class library to:

namespace ClassLibrary1
{
    public class Class1
    {
        private int x;
        private int y;

        public void Foo(int x)
        {
            Foo(x, 0);
        }
        public void Foo(int x, int y)
        {
            this.x = x;
            this.y = y;
        }
    }
}

I then compiled the class library and copied the dll into the console apps folder and ran the console app; there were no problems changing the signature, but as I said I am not sure if my testing method is sufficient.

So to answer your question, you can change the library in the way you have specified without being required to recompile your executable.

Mr. Mr.
  • 4,257
  • 3
  • 27
  • 42
  • I know this is old, but I do not know how the compiler could have compiled the first version of your app. You had two version of your Foo method which differed only by optional parameters, which I thought was not allowed. Based on your sample, the results would be different based on which version of Foo that was decided to be executed when a single parameter was passed, which should not be allowed. – Shaggie Nov 20 '21 at 06:58