Confusing errors for a confusing feature, part two

Last time I gave you the challenge to find a case where the same simple name means two different things, without introducing a new local/parameter/range variable into scope, that produces an error. It seems like it ought to be impossible; if nothing new has been introduced to a local scope then how can name resolution choose two different things? The relevant section of the C# specification (7.6.2.1 Invariant meaning in blocks) only gives the example I gave last time, of a local having the same name as a field.

The key to solving the riddle is a little-known rule about resolving a name from a set of possible class members:

if the member is invoked, all non-invocable members are removed from the set

And now we can see how to do it:

class C
{
  static void x11() {}
  class N
  {
    static int x11;
    static void M12()
    {
      x11();
      int x13 = x11;
    }
  }

The first usage of simple name x11 is invoked, so non-invocable member C.N.x11 is removed from the set of things to consider. But the second usage is not invoked and therefore the field is chosen. Now, you might say that hey, nothing terrible has happened here, but still, it is profoundly weird that x11 means two different things in the same block. It seems like this could be really, really confusing were the code not as straightforward and short as this. So this is illegal. And once again, we have the worst possible error message:

error CS0135: 'x11' conflicts with the declaration 'C.N.x11'

I exaggerate; I suppose it could be worse. It could say, as we saw CS0136 does last time:

error CS0135: 'x11' conflicts with something, 
but I'm not telling you what!

So I suppose we can be thankful for small mercies.

The challenge was also accidentally made somewhat more difficult because Visual Studio 14 CTP 3 does not correctly enforce the rule we’re discussing here. I’m not sure why; the version of Roslyn on codeplex does, and I would have thought CTP 3 would be using it. There has been some talk of modifying this rule; I’ll investigate!

Next time: a brief interlude for this week’s Ask The Bug Guys, and then we’ll have the thrilling conclusion of this series.

10 thoughts on “Confusing errors for a confusing feature, part two

  1. Why are you allowed to have static properties/functions with the same name in a class and in a nested class? That sounds like the shadowing issue that the local declaration space stuff was meant to prevent.

    Say you have a class with a nested class and both have a static A() method. Someone comes along and decides that the nested A() method isn’t useful so they remove it. Now instead of the calls to A() in the nested class being errors so that they can be removed they instead change to calls to the A() method of the class containing the nested class. Suddenly the program could be doing very different things but the person thinks they’ve successfully removed that method..

    • That reminds me of a feature I’ve sometimes wanted [and think would have resulted in a cleaner framework had it existed in .NET 1.0]: an standard convention for declaring that a certain base-class member [typically either a `protected` one, or one which is only supported by select subclasses] should not be accessible through the derived class. Very few classes, for example, should expose `MemberwiseClone` to derived classes, since in many cases there is no possible way via which that method could produce a legitimate derived-class instance which was detached from the original without using Reflection to access private base-class members (and a derived class which uses Reflection to access private base-class members won’t need `MemberwiseClone`).

      Note that a `protected` member only represents a contract between a class and its *immediate* descendants, and even the existence of a `public` member only binds the base class in circumstances where the parent promises that the member will be usable. Blocking access to a protected member does not violate the Liskov Substitution Principle (which only relates to access by things outside the instance). Further, even public members may legitimately be blocked in cases where the parent contract would forbid client code from trying to use those members on instances of the derived class [e.g. if an abstract collection type supports an `IsFixedSize` property and an `Add` method, a fixed-sized derivative might legitimately hide public access to the `Add` method].

      • class Base { special void M() { } }
        class Super : Base { }

        Base t = new Super();
        t.M() // now what? compiler or runtime error?

        • If the member is public, it will be accessible when the reference is cast to the base type. A more realistic example would be [assume MutableList and ImmutableList both inherit from ReadableList]

          MutableList thing1a = new MutableList(…);
          ImmutableList thing1a = new ImmutableList(…);
          ReadableList thing1b = thing1a;
          ReadableList thing2b = thing2a;

          thing1a.IsFixedSize yields false
          thing1a.Add(item) works

          thing1b.IsFixedSize yields false
          thing1a.Add(item) works

          thing2a.IsFixedSize yields true
          thing2a.Add(item) refuses compilation

          thing2b.IsFixedSize yields true
          thing2b.Add(item) throws exception

          Some readable lists support adding items; others do not. Code may only legitimately add an item to a list if IsFixedSize returns false. An attempt to add an item to an immutable list can’t possibly work; I would suggest that in cases where the compiler can detect it, it would be better for it to fail at compile-time than at run-time.

      • I’m not really sure how you think this feature can even be attainable. You are basically trampling type safety in the most common polymorphism scenarios (see Patrick Huizinga’s reply).

        What could be (up to a point) useful is some mechanism to explicilty disallow method hiding. I’m not sure if this is what you are referring to. Some special keyword that could enforce non private class members to be unhidable and obviously unoverridable.

        • A base type or interface contract inherently requires that implementations *exist* for all members, but not necessarily that *useful* implementations exist. All implementations of IList<int> are required to support an IsFixedSize property, and all implementations whose IsFixedSize property returns false should usefully support Add(), but it is perfectly legitimate for a type to have an IsFixedSize property which returns True and an Add method which throws a NotSupportedException. The compiler has to let code with a reference of type IList<int> call the Add() method, even though that reference might identify something like that doesn’t support it (e.g. ReadOnlyCollection<int>), because the compiler can’t know if the code has called IsFixedSize upon the object in question. On the other hand, if code has a reference of type ReadOnlyCollection<int>, the fact that any object identified thereby must be a ReadOnlyCollection<int> implies that the Add() method can *never* work, and there’s no reason it should be allowed to compile.

          Explicit interface implementation allows the above situation to be handled with interface members, but the same situation can arise just as well with inherited class members and there is presently no particularly clean way of handling it. While it would be possible for the derived class to define an empty dummy class with the same name as each unsupported member, the resulting compilation error if code tried to use that member would be decidedly unintuitive. Perhaps an Obsolete-tagged implementation would be the best approach, but I would think it would be cleaner to not have the member show up at all (as would be the case with explicit interface implementation).

  2. Pingback: Confusing errors for a confusing feature, part one | Fabulous adventures in coding

  3. Pingback: The Morning Brew - Chris Alcock » The Morning Brew #1705

  4. Pingback: Dew Drop – September 30, 2014 (#1866) | Morning Dew

  5. Pingback: Confusing errors for a confusing feature, part three | Fabulous adventures in coding

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s