Udostępnij za pośrednictwem


An Inheritance Puzzle, Part One

Once more I have returned from my ancestral homeland, after some weeks of sun, rain, storms, wind, calm, friends and family. I could certainly use another few weeks, but it is good to be back too.

Well, enough chit-chat; back to programming language design. Here's an interesting combination of subclassing with nesting. Before trying it, what do you think this program should output?

public class A<T> {
public class B : A<int> {
public void M() {
System.Console.WriteLine(typeof(T).ToString());
}
public class C : B { }
}
}
class MainClass {
static void Main() {
A<string>.B.C c = new A<string>.B.C();
c.M();
}
}

Should this say that T is int, string or something else? Or should this program not compile in the first place?

It turned out that the actual result is not what I was expecting at least. I learn something new about this language every day.

Can you predict the behaviour of the code? Can you justify it according to the specification? (The specification is really quite difficult to understand on this point, but in fact it does all make sense.)

(The answer to the puzzle is here.)

Comments

  • Anonymous
    July 27, 2007
    It did what I expected. This may spoil it for everyone else, but it's because you're deriving B from the specific A<int>, not the generic A<T>.  So instantiating A with a type T of string doesn't matter to class B.

  • Anonymous
    July 27, 2007
    Inner class B always has type int no matter what type the outer class A is declared with. Since method M() is part of class B, it will use the meaning of T that B was declared with and ignore the outer T.  Thus the printed type will be int or System.Int32 as was printed when I ran the program.

  • Anonymous
    July 27, 2007
    First off I'll admit that if I didn't know this was a trick question I'd assume "string" without a second thought. Knowing that it IS a trick question, though, and thinking about the question a little harder... A<string>.B inherits from A<int> A<string>.B.C inherits from "B" The question is, WHICH "B" does C inherit from? Is it A<T>.B that's in scope because it's the containing class of C (which would lead to output of string), or is it A<int>.B that's in scope because A<int> is the base class of the containing class? I can't readily justify why A<int>.B inherited from the base class of the container should have higher precedence in scope than A<T>.B which is the container, though. So I'll completely punt on actually answering the question, but give justifications for each of the possible outcomes: string: because A<T>.B.C inherits from A<T>.B and T is string. failure to compile: because a class can't inherit from it's containing class (I forget if that's actually a rule in C#?) so A<T>.B.C can't inherit from A<T>.B int: this is tricky to justify so the best I can come up with is a combination of the prior two - the compiler knows that A<T>.B.C can't inherit from A<T>.B so it discards that possible meaning of "B" and proceeds to the only other meaning in scope, A<int>.B - and makes A<string>.B.C inherit from that. I still think string is the most likely option. But I'm probably missing something.

  • Anonymous
    July 27, 2007
    jimo and Doug: If that's the explanation, then what happens if you put this in Main instead: A<string>.B b = new A<string>.B(); b.M();

  • Anonymous
    July 27, 2007
    Stuart: You are right, the question hinges solely upon what "B" means in "class C : B", and in this case it means A<int>.B.   It is perfectly legal for a class to inherit from its outer class, and in fact this is a very useful way to write the following pattern: public abstract class Frobber { private Frobber() {} public abstract void Frob(); public static Frobber MakeFrobber(int size) {  if (size > 100) return new BigFrobber(size); else return new SmallFrobber(); } private class BigFrobber : Frobber { ... } private class SmallFrobber : Frobber { ... } } This lets you build a factory which hands out frobbers such that you can change what actual object you hand out in future versions without breaking backwards compatibility.  No third party can extend Frobber because its only constructor is private, no third party can get at the concrete classes because they are private, etc.

  • Anonymous
    July 27, 2007
    I'll say it prints "string". The fact that B derives from A<int> means that B itself is not a template, so the "<int>" part has no effect on the generated code. The T in B::M() comes from the instantiation of A, which is A<string>. I would expect the code to print "int" only if B derived from A<T>. C is just an attempt at misdirection. :)

  • Anonymous
    July 27, 2007
    I don't know what C# does, but I know what I would want it to do.  I would want it to print "string" IMHO, it doesn't make sense to me at all that the inherited T (being int) from A<int> in the class declaration for B should mask out the T from generic outer class A.  The generic parameter T should act sort of like a static member, so the int T inherited from A<int> shouldn't be "visible" at all from the scope of B's declaration without specifying something like A.T. Just my opinion. If I were doing this in C++, I'd create a typedef member in A for T such as... class A<T> { typedef T templ_T; // other A members } ...then only explicitly reference either A::templ_T or B::templ_T, never just T, to avoid any ambiguities.... I don't know if there is any similar option in C#.  It seems to me that to have generics without having some way of creating a type alias could create a lot of hassles.

  • Anonymous
    July 27, 2007
    Well, the answer depends on your compiler. MSVC says int, Mono says String.

  • Anonymous
    July 27, 2007
    Makes sense to me. For our purposes, A<string> is a namespace, not a class. Actually, A<string>.B is also a namespace. C is the class, and it inherits from B. B in turn inherits from A<int>.

  • Anonymous
    July 27, 2007
    > C is the class, and it inherits from B. B in turn inherits from A<int>. Right, but in other languages with generics, such as C++, the template parameters don't inherit like other members.  Which makes sense, because even in C#, you can't access them like other members.  In C++, template parameters only "inherit" when you don't specify it for the inner class (i.e. class B : A<T>, instead of class B : A<int>).  But then what you are really doing is not inheriting, but rather declaring a new template parameter T for B and assigning it the same value as the outer T from A. The way I look at it, B shouldn't have a template parameter T received from A<int>, because A<int> is completely specialized for int.  It's not generic anymore.  If it was still generic, you'd have to specify the value for both A and B (e.g. A<string>.B<int>.C, and in that case, the output of int makes perfect sense).  And if B is derived instead from A<T>, then B gets a new T which happens to have the same value as the outer one.

  • Anonymous
    July 27, 2007
    Here's a shorter version of the same problem: public class A<T> { public class B : A<int> { public void L() { System.Console.WriteLine(typeof(T).ToString());  } } } class MainClass { static void Main() { new A<bool>.B.B.B.B().L(); new A<bool>.B().L(); System.Console.ReadKey(); } }

  • Anonymous
    July 27, 2007
    And you don't need generics to demonstrate the issue either: namespace Root { class A{  class X{  class B{  class X{  class C : A{   X a = new Root.A.X();//this is OK   X b = new Root.A.X.B.X(); //this is not OK   X c = new Root.A.X.B.X.C.X(); //this is OK }}}}} } So the point is - inside the innermost class C, how is syntactic token X resolved? is it Root.A.X, or Root.A.X.B.X? When referring to X, you first look at whether such a name exists INSIDE the current scope.  So inside C, does C.X exist?   Turns out, it does!  Since C inherits from A, and A contains an "X", C contains and "X". declarations scoped inside a superclass have precedence above syntactically surrounding scopes. This is acutally exactly what causes the original oddness:  When in Eric Lipperts code, inside class B, a new class C is declared, it is subclassed from B - but from WHICH B?  You might think that inside class B clearly "B" means the type of that class itself, but as the non-generic example illustrates, there's another "B", namely that in scope due to inheritance.  The inherited "B" takes precedence over the "B" in surrounding scopes, just like in the non-generic case.

  • Anonymous
    July 27, 2007
    And of course the non-generic example works in Mono. So I guess I'll file another bug then (it seems every post of you reveals at least one mono bug ;) ). Also, though it's clear why this B is chosen, it's still a question why inheritance over scoping is the design choice. So consider this the example given above concerning the Frobber. Let's say the Frobber contains an inner class called Irquil, because each Frobber has it's own Irquil, which however isn't relevant to any other classes, so it's an inner class of Frobber. However, as it appears, an Irquil is also something which has nothing to do with Frobbers at all, so it also exists somewhere else. Now if you want to create an Irquil in BigFrobber, do you want it to be the inner class or the unrelated class? However, why you would create a stack of inner classes inside Frobber, and have the other Irquil somewhere in there is still a bit unclear to me... Really, who invented this whole inner class business. It makes me wanna cry. Also, it's robbing me of my sleep, so i think I call it quits now ;)

  • Anonymous
    July 27, 2007
    Well, I'll wait to hear from Mr. Lippert the explanation of all this. However, I"ll point out that the "inheritance" of the generic parameter T seems to be a problem to me.  It leads to an implicitly declared generic parameter (T in B) masking an explicitly declared one (T in A).  And since the generic parameter cannot be referenced in a manner like "real" members (i.e. "dot" syntax: A.T), and C# seems to have no way to create a type alias member (like C++ typedef), this seems to me to be a big usability problem for the language itself.  As far as I can see, we have an implicit masking which cannot be gotten around without changing the class structure in a fairly significant way.

  • Anonymous
    July 27, 2007
    First off, Mr. Lippert is my father, dude. Second, I do not know what you mean by "the inheritance of the generic parameter T".   T isn't inherited.  B doesn't have a T.  Only A<T> has a T. Hopefully my explanation on Monday will clear it all up.

  • Anonymous
    July 27, 2007
    Sorry, I prefer to err on the side of politeness... As far as B inheriting T....  I was trying to infer the inner workings here, to explain why in the world the example would yield int instead of string.  Some sort of weird inheritance of T was the only way I could figure that this would happen. Obviously there's some assumption I'm making that is wrong.  Either I don't understand the scoping rules, or I've got the order in which things happen all mixed up. I still think it should output string.  =P Sigh....  I'll wait for Monday.

  • Anonymous
    July 27, 2007
    Took me a few minutes to grasp this one.  The <T> is clearly referring to the containing class.  Which means that b.M() shows a string. But the container class for c is b - which is A<Int>. So yes, that makes sense - just took a moment to get there.

  • Anonymous
    July 28, 2007
    Hi, public class A<M> { public class X<T> {    public class B : A<int> {        public new void M() {            System.Console.WriteLine(typeof(T).ToString());            System.Console.WriteLine(typeof(M).ToString());        }        public class C : B { }    } } ... A<string>.X<double>.B.C c = new A<string>.X<double>.B.C();        c.M(); Prints string and double. And why do I need the new keyword in M here? Your postings are way shorter than Chris Brumme's when he was still posting, but I am finding myself equally confused after reading them (I love that). So I guess you can consider yourself much more efficient:-)

  • Anonymous
    July 28, 2007
    Steve: Excellent example!  Why is it that this DOESN'T reproduce the odd behaviour? The germane difference between the two is that in your example, A<T> does not contain anything called B. When I post what is really happening on Monday it'll be clear why this does what it does. You need the "new" because your method M shadows the in-scope type variable M from A<M>.  We try to make it either illegal to shadow like this, or make you put the keyword "new" on one of them as a big waving red flag that says "there is a name ambiguity involving this method, so keep that in mind when you are reading this code".

  • Anonymous
    September 06, 2007
    Today, the answer to Friday's puzzle . It prints "Int32". But why? Some readers hypothesized that M would

  • Anonymous
    September 27, 2007
    Hi Eric, I have a confusion, you mentioned that ** the question hinges solely upon what "B" means in "class C : B", and in this case it means A<int>.B. ** So, if you consider the following code A<string>.B b = new A<string>.B();           b.M(); Why in this case b.M() prints String not INT32 , here also "B" should be considered as A<int>.B ... what's your comment ....

  • Anonymous
    September 27, 2007
    Hi Eric, If we modify the code like this .. public class A<T>    {        public void N()        {            Console.WriteLine(typeof(T).ToString());        }        public class B : A<int>        {            public void M()            {                Console.WriteLine(typeof(T).ToString());            }            public class C : B            {            }        }    }    static void Main()        {            A<string>.B b = new A<string>.B();                      b.M();            b.N(); Here, b.M() shows String but b.N() shows Int32.  I am confused how could it happen.....

  • Anonymous
    August 02, 2008
    如果我们的代码中同时出现泛型、继承、嵌套类这三种语言元素,那么在根据名称解析类型的时候可能就会有歧义了。本文中的问题及其结论是非常有意思的,其分析过程也非常的绕,大家一起来讨论下吧:)

  • Anonymous
    August 03, 2008
    如果我们的代码中同时出现泛型、继承、嵌套类这三种语言元素,那么在根据名称解析类型的时候可能就会有歧义了。本文中的问题及其结论是非常有意思的,其分析过程也非常的绕,大家一起来讨论下吧:)