10

Lets say I have a library, version 1.0.0, with the following contents:

public class Class1
{
    public virtual void Test()
    {
        Console.WriteLine( "Library:Class1 - Test" );
        Console.WriteLine( "" );
    }
}
public class Class2 : Class1
{
}

and I reference this library in a console application with the following contents:

class Program
{
    static void Main( string[] args )
    {
        var c3 = new Class3();
        c3.Test();
        Console.ReadKey();
    }
}
public class Class3 : ClassLibrary1.Class2
{
    public override void Test()
    {
        Console.WriteLine("Console:Class3 - Test");
        base.Test();
    }
}

Running the program will output the following:

Console:Class3 - Test
Library:Class1 - Test

If I build a new version of the library, version 2.0.0, looking like this:

public class Class1
{
    public virtual void Test()
    {
        Console.WriteLine( "Library:Class1 - Test V2" );
        Console.WriteLine( "" );
    }
}

public class Class2 : Class1
{
    public override void Test()
    {
        Console.WriteLine("Library:Class2 - Test V2");
        base.Test();
    }
}

and copy this version to the bin folder containing my console program and run it, the results are:

Console:Class3 - Test
Library:Class1 - Test V2

I.e, the Class2.Test method is never executed, the base.Test call in Class3.Test seems to be bound to Class1.Test since Class2.Test didn't exist when the console program was compiled. This was very surprising to me and could be a big problem in situations where you deploy new versions of a library without recompiling applications.

Does anyone else have experience with this?

Are there any good solutions?

This makes it tempting to add empty overrides that just calls base in case I need to add some code at that level in the future...

Edit:

It seems to be established that the call is bound to the first existing base method at compile time. I wonder why. If I build my console program with a reference to version 2 of my library (which should mean that the call is compiled to invoke Class2.Test ) and then replace the dll in the bin folder with version 1 the result is, as expected:

Console:Class3 - Test
Library:Class1 - Test

So there is no runtime error when Class2.Test doesn't exist. Why couldn't the base call have been compiled to invoke Class2.Test in the first place?

It would be interesting to get a comment from Eric Lippert or someone else who works with the compiler...

Berg
  • 350
  • 1
  • 2
  • 8
  • 1
    @Berg, you can drop Eric Lippert a line at his blog (http://blogs.msdn.com/ericlippert/). Just click 'Email' and ask him to take a look. He's recommended that people do this recently (http://stackoverflow.com/questions/2508945/can-anyone-explain-this-strange-behaviour). – Jeff Sternal Mar 24 '10 at 17:03

2 Answers2

12

This was the subject of my blog on March 29th:

http://blogs.msdn.com/ericlippert/archive/2010/03/29/putting-a-base-in-the-middle.aspx

Turns out that C# 1.0 did it your way, and that this decision causes some interesting crashes and performance problems. We switched it to do it the new way in C# 2.0.

I learned a lot from this question. See the blog for details.

Eric Lippert
  • 612,321
  • 166
  • 1,175
  • 2,033
  • @Eric: can you catalog this answer here: (http://stackoverflow.com/questions/1456785/a-definite-guide-to-api-breaking-changes-in-net)? Or I can do that, if you don't mind. – Pavel Minaev Mar 29 '10 at 16:19
4

When I build the executable with the first version of the library (I named mine "Thing"), and disassemble it I get:

L_000d: call instance void [Thing]Thing.Class1::Test()

Rebuilding it with the new DLL referenced:

L_000d: call instance void [Thing]Thing.Class2::Test()

So that confirms the decision to which method is referred to is made compile-time.

C.Evenhuis
  • 24,516
  • 2
  • 54
  • 68
  • Interesting, I wonder why the base call is bound like that, it makes it very tricky to update a library without demanding that all programs referencing it be recompiled. See also my edit above about what happens if we reference version 2 at compile time and replace it with version 1 at run time. – Berg Mar 24 '10 at 11:37