16

I don't understand why the following behaves the way it does at all. I don't even know if it's caused by hiding or something else.

class A<T>
{

    public class B : A<int>
    {
        public void b()
        {
            Console.WriteLine(typeof(T).ToString());
        }
        public class C : B
        {
            public void c()
            {
                Console.WriteLine(typeof(T).ToString());
            }
        }
        public class D : A<T>.B
        {
            public void d()
            {
                Console.WriteLine(typeof(T).ToString());
            }
        }
    }
}
class Program
{
    static void Main(string[] args)
    {
        A<string>.B.C c = new A<string>.B.C();
        A<string>.B.D d = new A<string>.B.D();
        c.c();
        c.b();
        d.d();
        d.b();
    }
}

The questions are:

  1. Why does c.c() produce System.String while c.b() produces System.Int32?

  2. Why does d.d() and d.b() both produce System.String and not behave in exactly the same way as the class C?

puretppc
  • 3,096
  • 7
  • 34
  • 63
Petr Hudeček
  • 1,422
  • 18
  • 29
  • 15
    Seems like this code is deliberately obfuscated. The solution is to not write code like this in the first place. – Servy Jan 16 '14 at 19:34
  • 1
    @Servy: Presumably, it's an exercise, not intended for practical consumption, but merely learning. – Robert Harvey Jan 16 '14 at 19:36
  • @RobertHarvey SO is designed for *practical* programming problems however, so if that's the case, it would make it offtopic, no? – Servy Jan 16 '14 at 19:37
  • @Servy Someone may end-up *generating* such code for whatever *practical* reasons. – Ondrej Tucny Jan 16 '14 at 19:38
  • 2
    I'd be surprised if anyone would have a legitimate reason to use this mess of nested classes in any practical code (generated or not). – Tim S. Jan 16 '14 at 19:39
  • @Servy: You can't require every Stack Overflow question to be tied to some specific project the asker is currently working on. The requirement is that the question have *practical considerations*. – Robert Harvey Jan 16 '14 at 19:42
  • Unless that legitimate reason is to teach a student how code works, in which case this question is wrong for a different reason. If this is simply obfuscated to hide business logic (which it may be too simple for) then whatever. – Magus Jan 16 '14 at 19:43
  • 9
    Practical or not, I'm currently stumped :( Personally I think this kind of question is great for growing people's ability to read the specification, which is useful in itself. – Jon Skeet Jan 16 '14 at 19:43
  • 2
    http://blogs.msdn.com/b/ericlippert/archive/2007/07/27/an-inheritance-puzzle-part-one.aspx – SLaks Jan 16 '14 at 19:44
  • @SLaks: That's what I thought of immediately when I saw this, but this result doesn't follow from what I saw there, because both C and D are B, and in B, T is Int32. – Magus Jan 16 '14 at 19:47
  • 5
    Jon Skeet stumped?? Question: upvoted. – TypeIA Jan 16 '14 at 19:49
  • @Servy, yes, I understand it's obfuscated and I tried but I couldn't reduce its complexity without removing the question. If I removed some of the things (an inheritance here or there), the answer could have stayed the same but for different reasons. – Petr Hudeček Jan 16 '14 at 19:50
  • This guy is wizard for causing @JonSkeet to be stumped. – deloreyk Jan 16 '14 at 19:51
  • @PetrHudeček That's my *point*. If you simplify the question to code that someone would legitimately write, you wouldn't have a problem to begin with. It's only observable from code that really shouldn't be used. It's a confusing concept because it's a situation one the language designers didn't really expect people to be in to begin with, and so didn't optimize for. – Servy Jan 16 '14 at 19:51
  • Confusing and complicated is an understatement for this question! – Sriram Sakthivel Jan 16 '14 at 20:04

3 Answers3

11

This is a variation of a puzzle that I posted on my blog many years ago:

http://blogs.msdn.com/b/ericlippert/archive/2007/07/27/an-inheritance-puzzle-part-one.aspx

and Cyrus posted on his blog before that:

http://blogs.msdn.com/b/cyrusn/archive/2005/08/01/446431.aspx

See the discussion there for details.

Briefly: what does B mean in class C : B ? Check the container, class B. Does it contain any type called B? No. Then check the container's base class. The container's base class is A<int>. Does it contain anything called B? Yes. So this means class C : A<int>.B.

Now we say that c is A<string>.B.C. We call method A<string>.B.C.c() What is T throughout A<string>? Obviously string. So c.c() prints String for T.

Now we call A<string>.B.C.b() but there is no such method in A<string>.B.C directly. Where does it get this method? From its base class. What's it's base class? A<int>.B. So we call A<int>.B.b(). What is T throughout A<int>? Obviously int.

Now we come to A<string>.B.D.d(). The base class is irrelevant. T is string throughout A<string>.

And finally A<string>.B.D.b(). There is no such method on A<string>.B.D directly so it must get it from its base type. T is string throughout A<string>, so the base type is A<string>.B. Therefore this calls A<string>.B.b().

If that doesn't make sense to you, spell everything out. Let's substitute String for T:

class A_string
{
    public class B : A_int
    {
        public void b()
        {
            Console.WriteLine(typeof(string).ToString());
        }
        public class C : A_int.B // Note!
        {
            public void c()
            {
                Console.WriteLine(typeof(string).ToString());
            }
        }
        public class D : A_string.B
        {
            public void d()
            {
                Console.WriteLine(typeof(string).ToString());
            }
        }
    }
}

OK, that's one of the types. Now let's do the same for int:

class A_int
{
    public class B : A_int
    {
        public void b()
        {
            Console.WriteLine(typeof(int).ToString());
        }
        public class C : A_int.B // Note!
        {
            public void c()
            {
                Console.WriteLine(typeof(int).ToString());
            }
        }
        public class D : A_int.B
        {
            public void d()
            {
                Console.WriteLine(typeof(int).ToString());
            }
        }
    }
}

Now given those types it should be clear what A_string.B.C.c(), A_string.B.C.b(), etc, all print out.

Eric Lippert
  • 612,321
  • 166
  • 1,175
  • 2,033
  • And -- this is also because we first look up methods in base class before we go to outer class, right? – Petr Hudeček Jan 16 '14 at 20:09
  • 1
    @PetrHudeček: We look up *names* in base classes before outer classes. – Eric Lippert Jan 16 '14 at 20:11
  • @PetrHudeček: Not methods, but base types. – SLaks Jan 16 '14 at 20:11
  • @SLaks: What do you mean? I meant... when, in the Main method, we are looking for c.b() where c is as A.B.C, but we cannot find the method b in the class itself, then we may either look in its base class (which is A.B) or its outer class (which is A.B). Looking in base class is preferred by the compiler, I understood. – Petr Hudeček Jan 16 '14 at 20:18
  • 1
    @PetrHudeček: Wrong. Instance members are never looked up from outer classes; after all, you don't actually have an instance of the outer class to call it on. The question here is about _type_ lookup in the base declaration; does `B` mean `A.B` (inherited from the base type's outer class as `A.B.B`) or `A.B` (inherited fromn the outermost class) – SLaks Jan 16 '14 at 20:41
3

A<string>.B.C inherits A<int>.B, because B in the base class declaration comes from the inside parent scope first. (to clarify, its parent scope is A<T>.B, which contains a type named B referring to A<int>.B, inherited from its base class A<int>)

Calling b() comes from its base class, in which T (from the parent scope) is int.

D explicitly inherits A<T>.B, using T from the outermost scope (A<T>), so its T always comes from A<> in its typename.

SLaks
  • 800,742
  • 167
  • 1,811
  • 1,896
  • The most recent edit explains it. It does make me wonder if the other `T` is somehow accessible though. – Magus Jan 16 '14 at 19:50
  • @Magus: Within the base class methods in `B`, there _is no_ other type; generic type parameters are not polymorphic. – SLaks Jan 16 '14 at 19:52
  • `c.GetType()` is `A\`1+B+C[System.String]`; its `BaseType` is `A\`1+B[System.Int32]`. – SLaks Jan 16 '14 at 19:53
  • It's D that causes me confusion. It must be an A.B, right? But the int is not accessible? – Magus Jan 16 '14 at 19:54
  • @MagusL No. `B` is not generic. Type parameters are not inherited by derived class. `T` inside `B` refers to `T` from `A` from the parent scope. The first one was different because it was actually `A.B`. – SLaks Jan 16 '14 at 19:58
  • @Magus: No, there is no such thing as `A.B` because `B` isn't generic to begin with. – Eric Lippert Jan 16 '14 at 19:59
3

This is a very slightly more complex example of a puzzle Eric Lippert describes in this blog article, which is then explained in this article.

The short summary (I highly suggest just reading the article) is that in this case there is an ambiguity in the line C : B. A decision needs to be made as to what B actually means in this context; whether it's A<T>.B or A<int>.B. Essentially, the compiler chooses the latter, given a particular criteria of "betterness" described in the spec.

Servy
  • 193,745
  • 23
  • 295
  • 406