Odious ambiguous overloads, part two

Odious ambiguous overloads, part two

Rate This
  • Comments 9

There were a number of ideas in the comments for what we should do about the unfortunate situation I described yesterday. We considered all of these options and more.  I thought I might talk a bit about the pros and cons of each.

I suggested do nothing.  We shipped with this behaviour in C# 2.0, the world didn't come to an end, we can keep on shipping with it. The pro side is that this is the cheapest and easiest thing to do.  The con side is that as a reader pointed out, this is a gross "gotcha" just waiting for someone to stumble upon it.

DrPizza suggested that we come up with a syntax that disambiguates between the two ambiguous cases.  On the pro side, the problem would be clearly solved.  On the con side, this might be a breaking change (because suddenly one of the methods that used to bind one way might start binding the other way, depending on what the choice of syntax was.)

Also on the con side, adding new language syntax is something that we do not do lightly.  There is considerable work involved in changing the specification, and this is kind of an edge case.

Ayende Rahien suggested that we add an attribute that hints to the compiler which is the right binding. This has the nice property of not introducing new language syntax, but is kind of ugly.

These are both good ideas but unfortunately, as it turns out, neither of these work for technical reasons which I will go into in detail below.

Chris D suggested that the constructed interface should be treated as an interface with only one method. That's a really nice idea, but unfortunately it doesn't work with the CLR definition of generics.  An interface with n methods has n vtable slots that need filling, period, and n is invariant under construction. We'd have to change both the semantics of constructed interfaces and their implementation in the CLR, which means changing both the C# spec and the CLR spec, and that's way too much work for this edge case.

Kyle Lahnakoski suggested that it simply be an error.  There are several ways this could be an error.

It could be an error to have an unconstructed generic type which could possibly be constructed to produce a duplicated signature.  This was how generics originally worked in C#, but we decided that we could allow these cases and we can't really go back on that decision now – that would be a huge breaking change. There are many serializable generic objects which use a pattern of having a method that takes a type parameter to overload a method that takes a serialization object parameter, and all of those would suddenly be errors.

It could be an error to construct a generic type so as to actually produce a duplicate signature, but again, we decided to allow that in C# 2.0 and it would be a large breaking change to go back now.

It could be an error to have any explicit implementation of an interface method which matches ambiguously.  This would be a smaller breaking change, but still breaking.

What do any of these breaking changes buy the user though?  Not more functionality, but less.  Basically we'd be saying "you could do this before, and now you can't, too bad." That's a hard sell.

Carlos suggested unification – that is, have an explicit implementation map to every method that it matches, not just the first one.  This is a breaking change, but it seems like the most natural thing to do.  Since an implicit implementation maps to every interface slot that it matches, why shouldn't an explicit implementation do the same? This means adding new language to the spec, but no new syntax.

This is what we decided to do, and I tried to implement it, only to discover that, uh, actually that stuff I told you yesterday about the implicit implementation matching the first one in source code order is a bit of a mistruth. That's what I thought when I first looked at the problem, but upon closer examination, something deeper was wrong.

In fact what is going on here is that when the CLR loads the class for the first time, it does a verification check to ensure that the class does in fact implement every method of every interface that it says it does.  It is the CLR, not C#, which matches the explicit implementation to the first matching slot in the constructed interface type! It just so happens that since metadata for the interface is emitted in source-code order, that the CLR appears to do the match in source code order.  It actually does it in a CLR-implementation-defined order that just happens to be source code order.

To understand why we can't fix this, you need to understand how explicit implementations are represented in metadata.  In the CLR metadata for a class there is a table called the MethodImpl table which says "method blah of class foo explicitly implements method bar of interface IFred".  The "method bar of interface IFred" portion of that mapping may be either a token to a method of the unconstructed interface (a "MethodDef") , or it may be a token representing a method on the constructed interface (a "MemberRef").

We can't use a MethodDef because there is a signature mismatch between the class's implementation and the method on the unconstructed interface.  One takes an int, the other takes a type parameter, and the CLR flags that as illegal. So that's out.

But we can't use the latter because the MemberRef identifies the constructed interface method by – you guessed it – its signature!  And now we're in the same hideous bind all over again.  We have no way to tell the CLR "no, really, use the generic-substituted version of the method", because it can't tell the two signatures apart either.

To fix this problem by either unifying, or adding a syntax/attribute to disambiguate, we need some way to represent that unification/disambiguation in metadata. But the CLR simply lacks the concept of "method token for constructed method identified by something more unique than signature". 

Though we can certainly suggest that this is a weakness in the CLR metadata design, waiting around for that to be addressed to fix this edge-case problem seems like a non-starter. Given that we can't fix it in the current CLR the way we'd like, and making it an error seems like an egregious overreaction that buys the user nothing, but at the same time doing nothing is leaving a gotcha in the code waiting to happen, I took the only course of action left:

Warning: Explicit interface implementation 'I1<int>.M(int)' matches more than one interface member. Which interface member is actually chosen is implementation-dependent. Consider using a non-explicit implementation instead." 

Unfortunately, sometimes that's the best you can do.

  • I'm intrigued - did you decide what to do by yourself, or solicit guidance from your ECMA buddies? C# is a standard, you know. Is it fair to just do your own thing then run it through the process afterwards?
  • Er, you really mean that you added a warning, and pushed a request to be able to tell them apart to the CLR metadata folks, because sometimes all you can do is add a warning, and push it to the guys who can really fix it, right?

    Or did you just add a warning, and mark the bug FIXED when it isn't really?
  • Sure, we'll have a talk with the CLR folks and see if a future version of the CLR can get this feature added.

    And no, of course I didn't decide this by myself!  I wrote a detailed analysis of the bug and sent it off to Anders and Mads and Don Box and a whole pile of other people who are part of the standardization process, and took guidance from them.  And the guidance was that if possible, the standard and implementation should be emended to read that unification is dejure, and if not, we'll issue a warning and stay broken until it is practical to fix the problem.  There's absolutely no point in changing the standard if it's _impossible_ for the implementation to comply!  It's better to just leave it implementation-defined behaviour in this regrettable scenario.
  • > It could be an error to have any explicit implementation of an interface method which matches ambiguously.  > This would be a smaller breaking change, but still breaking.

    I would imagine that the number of explicite implementaions like I1<int> would be rare, if not non-existent.

    What do any of these breaking changes buy the user though?  Not more functionality, but less.  Basically we'd be saying "you could do th> It could be an error to have any explicit implementation of an interface method which
    > matches ambiguously.  This would be a smaller breaking change, but still breaking.

    I would imagine that the number of explicit ambiguous implementations (like I1<int>) would be rare, if not non-existent.  Is there much in the C# libraries that breaks with this “functionality” is denied?

    > What do any of these breaking changes buy the user though?  Not more functionality,
    > but less.  Basically we'd be saying "you could do this before, and now you can't, too bad."
    > That's a hard sell.

    Functionality and clarity are a balancing act.  Personally, I would categorize this “functionality” as a “gotcha”.  It would be crazy-hard to debug such a thing, especially if the implementations are in parent classes, and especially if you are not aware that such craziness could happen.  At least the warning you propose indicates a possible random behavior.
    is before, and now you can't, too bad." That's a hard sell.
  • One way to disambiguate the interface methods would be to add a modopt to the generic type parameters in the method signature of methods that potentailly clash.

    I don't think it is a realistic solution, because it would complicate (and break) cross language and cross version scenarios, but it is something worth mentioning.
  • While it would be a hideous hack, you could always add an attribute that controls the order in which the metadata is emitted by the compiler.

    I believe there are some source-order dependencies in C# with regards to field initializers.  If you have source-code rules about which order members of a class should appear (alphabetical, for instance) this could be a minor problem.

    An attribute that takes an integer parameter could fix these problems.

    But like I said, it'd be a hideous hack.

    The current behavior needs to be documented and maybe even added to the spec so that other CLR implementations can be compatible.

  • We on the C# team hate making breaking changes. As my colleague Neal called out in his article on the

  • User : The typeof(T) operator in C# essentially means “compiler, generate some code that gives me an

  • Пользователь: Оператор typeof(T) в C#, по существу, означает «компилятор, сгенерируй некий код, который

Page 1 of 1 (9 items)