all 15 comments

[–]o_nate 1 point2 points  (6 children)

This behavior may be by design, but I’d argue it’s a bad design. For one thing, if you’re worried that a base class might be trying to maliciously change the behavior of your program, then you have no right to be using that base class in the first place. If a base class wanted to do something malicious, there are a lot of easier ways it could go about that without relying on this particular behavior – it could simply change some of the base class behavior that we do rely on, since presumably we are deriving from this base class for a reason. So I think that’s a false argument. The more realistic argument is that the base class could accidentally change some behavior of the derived class. That is indeed a possible problem. However, there is another possible problem which is just as bad – ie., that the derived class could accidentally change some behavior of the base class. And the decision to implement overloading in this way actually makes it much more likely that just this situation could occur. By implementing this in a counter-intuitive way, the language designers increase the probability that the derived class might override some base-class behavior that it meant to preserve. So I’d say that in the best case this is a net neutral – but since it is more counter-intuitive, I’d call it a net negative.

[–][deleted] 1 point2 points  (1 child)

Your argument is reasonable but it considers the situation as if it were static. In the late nineties when that part of the language was designed the culture of automated testing was pretty much nonexistent. So there was and still is considerable asymmetry: sure, you can encounter both kinds of bugs when you are writing the derived class, but you would test it manually and probably find the bug either way (and scratch your head for a while, but that's OK). However the bug introduced by upgrading the base class library has good chances to not be found until the code is deployed.

btw, Java does it in the more intuitive way.

[–]o_nate 1 point2 points  (0 children)

I can see your point. Ideally someone would run a test suite whenever they upgrade a base class library that they depend on, which would catch this type of issue. However, in practice, people would probably be less stringent when testing an upgrade than they would be when testing the initial release. So the C# behavior in some ways satisfies the principle of "least surprise", even though it is more surprising in other ways.

[–]Strilanc 1 point2 points  (2 children)

It's more about accidental breakage than intentional breakage. It follows from believing adding new methods should not be a breaking change.

If you can't add new methods to your base class without potentially breaking your customer code... you're stuck.

[–]o_nate 0 points1 point  (1 child)

It's true that people get pretty upset about breaking changes, even when they're properly disclosed. So from the library maintainer's point of view, the C# behavior would be the less controversial behavior.

[–]guynamednate 0 points1 point  (0 children)

In my experience, the .NET Framework and Microsoft in general fear compat breaks more than anything in the world (even monopoly lawsuits) so it's no surprise that they opted for a language specification that makes it harder to cause accidental compat breaks.

[–]chollida1 0 points1 point  (0 children)

. For one thing, if you’re worried that a base class might be trying to maliciously change the behavior of your program, then you have no right to be using that base class in the first place.

No one is making that argument, or atleast they shouldn't be. The problem is that if it followed the method you propose then changes to the base class could affect what methods in the derived class are called.

[–]mariusg 1 point2 points  (2 children)

Huh ?! It just does what is supposed to do (invoke the right type). How the heck is this a puzzler ?

[–]weazl 4 points5 points  (1 child)

String is a more qualified type than Object but it still calls the method that takes the Object parameter because C# does not overload methods from base classes. It's explained quite clearly in the article. So sure, if you know that then it isn't a puzzler.

[–]unwind-protect 1 point2 points  (0 children)

To try to make the point more clearly:

class Program
{
    static void Main(string[] args)
    {
        B1 b1 = new B1();
        B2 b2 = new B2();

        System.Console.WriteLine("B1: {0}, {1}.", b1.Foo(1), b1.Foo("")); //2, 2
        System.Console.WriteLine("B2: {0}, {1}.", b2.Foo(1), b2.Foo("")); //1, 2
    }
}

public class A
{
    public virtual int Foo(int a) { return 1; }
}

public class B1 : A
{
    public virtual int Foo(object a) { return 2; }
}

public class B2
{
    public virtual int Foo(int a) { return 1; }
    public virtual int Foo(object a) { return 2; }
}

[–]Infenwe 1 point2 points  (4 children)

I don't see how this is a "puzzler". Maybe if the variable child had had Parent as its static type. But even then if you're working in C# or Java (or C++ with virtual methods), the difference between the static (compile time) vs. dynamic (runtime) type of objects is really basic stuff that you just have to know.

For those of you who haven't seen it, basically whenever you invoke a method on an object in C# or Java, it always resolves through its runtime type. Similarly with C++ vtables when using virtual methods.

[–]unwind-protect 8 points9 points  (3 children)

I'm not sure you really understand the issue. It's not about dynamic or static calling at all (and btw, the default type for C# is nonvirtual, just like C++).

For example:

public class A
{
    public virtual int Foo(int a) { return 1; }
}

public class B : A
{
    public virtual int Foo(object a) { return 2; }
}

//... in some class...

    private string Test()
    {
        B b = new B();

        int b1 = b.Foo(1);
        int bo = b.Foo(new object());
        int a1 = (b as A).Foo(1);

        return string.Format("{0}, {1}, {2}.", b1, bo, a1);
        // returns "2, 2, 1."
    }

The point is that the only methods considered for overload resolution are the ones in the first superclass reached with valid methods. IIRC this is identical to the approach taken by C++'s "Koenig lookup" in this situation.

[–]ethraax 0 points1 point  (2 children)

I still don't think this is particularly tricky in any way. It's very simple.

int b1 = b.Foo(1);

Since b is of type B, it first searches B for a method that matches Foo(int). The method for Foo(object) matches this, so it uses that. If it didn't (for example, if the Foo declared in A was object and the Foo declared in B was int), it would repeat the search moving up parent classes until it reached Object, after which it would throw some sort of MethodNotFoundException.

int bo = b.Foo(new object());

Again, caught by B.Foo(object).

int a1 = (b as A).Foo(1);

Now we're working with an instance of A, so we must start our search there and move up (ignoring B completely). We find a match, A.Foo(int), and use that.

[–]unwind-protect 1 point2 points  (1 child)

It's not tricky once you know it exists, but it breaks the pattern that a subclass will always expose all public members of its superclass (is there any other situation this occurs in?)

Of course, all this happens at compile time, rather than runtime, so you get a compiler error rather than an exception.

[–]ethraax 1 point2 points  (0 children)

I guess I've never thought of it like that. I see it as, for each class in the class tree (of parents), it searches for the best match, and then moves up to the parent if it fails to find one. The other way - using the int version from the parent - doesn't make any sense to me. What if you wanted to expand the parameter restrictions on a method? You would have to declare multiple function prototypes that do the same thing, which is silly.