tags:

views:

122

answers:

4

Hello, I have this code snippet and I would like to know why is the output as written in comment below:

interface I {   
    void m1();
    void m2();      
    void m3();      
}

class A : I {
    public void m1() { Console.WriteLine("A.m1()"); }
        public virtual void m2() { Console.WriteLine("A.m2()"); }
        public virtual void m3() { Console.WriteLine("A.m3()"); }
}

class C : A, I {
    public new void m1() { Console.WriteLine("C.m1()"); }
    public override void m2() { Console.WriteLine("C.m2()"); }
    public new void m3() { Console.WriteLine("C.m3()"); }
}

------
C c = new C();

((I) ((A) c)).m1();  //"C.m1()"
((I) ((A) c)).m2();  //"C.m2()"
((I) ((A) c)).m3();  //"C.m3()"

One original guess of what the output shoud be was:

A.m1(); C.m2(); A.m3();

Thank you for help!

+1  A: 

All these type-casts are redundant and I'm pretty sure they get optimized away by the compiler.

Here's what I mean. (A) c is redundant since C is A, so we're left just with (I)c, which is redundant as well, since C implements I. Thus, we have just an instance of C class, for which the compiler applies normal resolution rules.

EDIT

Turns out, I was completely wrong. This document describes what happens:

Interface mapping for a class or struct C locates an implementation for each member of each interface specified in the base class list of C. The implementation of a particular interface member I.M, where I is the interface in which the member M is declared, is determined by examining each class or struct S, starting with C and repeating for each successive base class of C, until a match is located:

  • If S contains a declaration of an explicit interface member implementation that matches I and M, then this member is the implementation of I.M.
  • Otherwise, if S contains a declaration of a non-static public member that matches M, then this member is the implementation of I.M.
Anton Gogolev
Sergey wrote a counterexample - (A)c doesn't behave the same way as c in calling methods.
MartyIX
+4  A: 

Change class C to this:

class C : A {
    public new void m1() { Console.WriteLine("C.m1()"); }
    public override void m2() { Console.WriteLine("C.m2()"); }
    public new void m3() { Console.WriteLine("C.m3()"); }
}

Explanation from Jeffrey Richter:

The C# compiler requires that a method that implements an interface be marked as public. The CLR requires that interface methods be marked as virtual. If you do not explicitly mark the method as virtual in your source code, the compiler marks the method as virtual and sealed; this prevents a derived class from overriding the interface method. If you explicitly mark the method as virtual, the compiler marks the method as virtual (and leaves it unsealed); this allows a derived class to override the interface method.

If an interface method is sealed, a derived class cannot override the method. However, a derived class can re-inherit the same interface and can provide its own implementation for the interface's methods. When calling an interface's method on an object, the implementation associated with the object's type is called.

Because C class also implements I interface, than when we calling interface methods on C object, we will call implementation accosicated with the object's type is called (i.e class C methods) but not methods derived from C's base class (not A class methods).

Sergey Teplyakov
I tried that and the output is what I wrote as the guess above. But I would like to know the rule which is used in my code snippet. It's a question for an exam so that if the example is dumb is irrelevant.
MartyIX
A: 

Casting does not matter as C already contains A and I.

for ((I) ((A) c)).m1() and ((I) ((A) c)).m3():
- The "new" keyword hides the A.m1 implementation (in fact without the new keyword you will get a warning). So C.M1 and C.M3 are chosen.

For ((I) ((A) c)).m2():
- The override takes care that the C.M2 implementation is chosen.

Mvt
+1  A: 

I think you're misunderstanding what the cast operator does for reference conversions.

Suppose you have a reference to an instance of C on the stack. That's a certain set of bits. You cast the thing on the stack to A. Do the bits change? No. Nothing changes. It's the same reference to the same object. Now you cast it to I. Do the bits change this time? No. Same bits. Same reference. Same object.

An implicit reference conversion via a cast like this simply tells the compiler to use different rules when figuring out at compile time what method to call.

So the casts to "A" are completely irrelevant and ignored by the compiler. All the compiler knows or cares about is that you have an expression of type I, and you're calling a method on it. The compiler generates a call that says "at runtime, look at the object reference that is on the stack and invoke whatever is in the "I.m1" slot of the object.

The way to figure this stuff out is to think about the slots. Every interface and class defines a certain number of "slots". At runtime, every instance of a class has those slots filled in with references to methods. The compiler generates code that says "invoke whatever is in slot 3 of that object", and that's what the runtime does -- looks in the slot, calls what's there.

In your example there are all kinds of slots. The interface requires three slots, the base class provides more, and the "new" methods of the derived class provide two more. When an instance of the derived class is constructed, all of those slots are filled in, and, understandably, the slots associated with I are filled in with the matching members of the derived class.

Does that make sense?

Eric Lippert
It does. But I can't agree on the point that casting to "A" is irrelevant - if I call:((A) c).m1(); //"A.m1()"((A) c).m2(); //"C.m2()"((A) c).m3(); //"A.m3()" - that is I call the methods without casting to "I" the result is different. Thank you for your comment.
MartyIX
@MartyIX: My point was that when you have RECEIVER . METHODGROUP ( ARGUMENTS ), the compiler determines what method slot to call *solely* on the basis of the type of the receiver, the expressions in the arguments, and the name of the method group. You can put as many casts as you like in the receiver, but for the purposes of determining the slot that is called, only the one that actually determines the type of the expression is relevant.
Eric Lippert