121

The following is an interview question. I came up with a solution, but I'm not sure why it works.


Question:

Without modifying the Sparta class, write some code that makes MakeItReturnFalse return false.

public class Sparta : Place
{
    public bool MakeItReturnFalse()
    {
        return this is Sparta;
    }
}

My solution: (SPOILER)

public class Place
{
public interface Sparta { }
}

But why does Sparta in MakeItReturnFalse() refer to {namespace}.Place.Sparta instead of {namespace}.Sparta?

LuizLoyola
  • 386
  • 3
  • 20
budi
  • 6,351
  • 10
  • 55
  • 80
  • 5
    Could you please add Spoiler Alert or something to Solution, please? I am so disapointed I do not get a chance to solve it on my own. Question is indeed amazing. – Karolis Kajenas May 26 '17 at 05:31
  • 1
    I initially included spoiler tags, however it was edited out by the community multiple times throughout the lifetime of this post. Sorry about that. – budi May 27 '17 at 21:16
  • 1
    I dislike the title of this post; it's more fit for codegolf.SE. Can we change it to something that actually describes the question? – Ky - May 30 '17 at 10:14
  • 3
    Cute puzzle. Terrible interview question, but cute puzzle. Now that you know how and why this works, you should take a shot at this harder version: https://stackoverflow.com/q/41319962/88656 – Eric Lippert Apr 20 '18 at 20:32

3 Answers3

117

But why does Sparta in MakeItReturnFalse() refer to {namespace}.Place.Sparta instead of {namespace}.Sparta?

Basically, because that's what the name lookup rules say. In the C# 5 specification, the relevant naming rules are in section 3.8 ("Namespace and type names").

The first couple of bullets - truncated and annotated - read:

  • If the namespace-or-type-name is of the form I or of the form I<A1, ..., AK> [so K = 0 in our case]:
    • If K is zero and the namespace-or-type-name appears within a generic method declaration [nope, no generic methods]
    • Otherwise, if the namespace-or-type-name appears within a type declaration, then for each instance type T (§10.3.1), starting with the instance type of that type declaration and continuing with the instance type of each enclosing class or struct declaration (if any):
      • If K is zero and the declaration of T includes a type parameter with name I, then the namespace-or-type-name refers to that type parameter. [Nope]
      • Otherwise, if the namespace-or-type-name appears within the body of the type declaration, and T or any of its base types contain a nested accessible type having name I and K type parameters, then the namespace-or-type-name refers to that type constructed with the given type arguments. [Bingo!]
  • If the previous steps were unsuccessful then, for each namespace N, starting with the namespace in which the namespace-or-type-name occurs, continuing with each enclosing namespace (if any), and ending with the global namespace, the following steps are evaluated until an entity is located:
    • If K is zero and I is the name of a namespace in N, then... [Yes, that would succeed]

So that final bullet point is what picks up the Sparta class if the first bullet doesn't find anything... but when the base class Place defines an interface Sparta, it gets found before we consider the Sparta class.

Note that if you make the nested type Place.Sparta a class rather than an interface, it still compiles and returns false - but the compiler issues a warning because it knows that an instance of Sparta will never be an instance of the class Place.Sparta. Likewise if you keep Place.Sparta an interface but make the Sparta class sealed, you'll get a warning because no Sparta instance could ever implement the interface.

Jon Skeet
  • 1,421,763
  • 867
  • 9,128
  • 9,194
  • 2
    Another random observation: Using the original `Sparta` class, `this is Place` returns `true`. However, adding `public interface Place { }` to the `Sparta` class causes `this is Place` to return `false`. Makes my head spin. – budi May 22 '17 at 21:24
  • @budi: Right, because again, the earlier bullet finds `Place` as the interface. – Jon Skeet May 22 '17 at 21:29
22

When resolving a name to its value the "closeness" of the definition is used to resolve ambiguities. Whatever definition is "closest" is the one that is chosen.

The interface Sparta is defined within a base class. The class Sparta is defined in the containing namespace. Things defined within a base class are "closer" than things defined in the same namespace.

Servy
  • 202,030
  • 26
  • 332
  • 449
  • 1
    And imagine if name lookup did *not* work this way. Then, working code that contains an inner class would become broken if anyone happened to add a top-level class with the same name. – dan04 May 22 '17 at 20:08
  • 1
    @dan04: But instead, working code that *doesn't* contain a nested class gets broken if anyone happens to add a *nested* class with the same name as a top-level class. So it's not exactly a total "win" scenario. – Jon Skeet May 22 '17 at 20:19
  • 1
    @JonSkeet I'd say adding such a nested class is a change in an area which the working code has reasonable reasons to be affected by, and watch for changes. Adding a totally unrelated top-level class is much farther removed. – Angew is no longer proud of SO May 23 '17 at 07:30
  • 2
    @JonSkeet Is that not just the Brittle Base Class problem in a slightly different guise, though? – João Mendes May 23 '17 at 09:09
  • 1
    @JoãoMendes: Yes, pretty much. – Jon Skeet May 23 '17 at 09:20
  • @dan04: Not necessarily. C++ resolves this using an *injected-class-name*. That is, the name of the current and containing classes are looked up with the same priority as members, which prevents this particular incarnation of the problem. But it would be weird for C#, which doesn't have `typedef` at all, to make such a type alias predefined. – Ben Voigt May 23 '17 at 13:56
1

Beautiful question! I'd like to add a slightly longer explanation for those who don't do C# on a daily basis... because the question is a good reminder of name resolution issues in general.

Take the original code, slightly modified in the following ways:

  • Let's print out the type names instead of comparing them as in the original expression (i.e. return this is Sparta).
  • Let's define the interface Athena in the Place superclass to illustrate interface name resolution.
  • Let's also print out the type name of this as it is bound in the Sparta class, just to make everything very clear.

The code looks like this:

public class Place {
    public interface Athena { }
}

public class Sparta : Place
{
    public void printTypeOfThis()
    {
        Console.WriteLine (this.GetType().Name);
    }

    public void printTypeOfSparta()
    {
        Console.WriteLine (typeof(Sparta));
    }

    public void printTypeOfAthena()
    {
        Console.WriteLine (typeof(Athena));
    }
}

We now create a Sparta object and call the three methods.

public static void Main(string[] args)
    {
        Sparta s = new Sparta();
        s.printTypeOfThis();
        s.printTypeOfSparta();
        s.printTypeOfAthena();
    }
}

The output we get is:

Sparta
Athena
Place+Athena

However, if we modify the Place class and define the interface Sparta:

   public class Place {
        public interface Athena { }
        public interface Sparta { } 
    }

then it is this Sparta -- the interface -- that will be available first to the name lookup mechanism and the output of our code will change to:

Sparta
Place+Sparta
Place+Athena

So we have effectively messed up with the type comparison in the MakeItReturnFalse function definition just by defining the Sparta interface in the superclass, which is found first by the name resolution.

But why does C# chose to prioritize interfaces defined in the superclass in the name resolution? @JonSkeet knows! And if you read his answer you'll get the details of the name resolution protocol in C#.

mircealungu
  • 6,831
  • 7
  • 34
  • 44