views:

1553

answers:

6

Hi,

The topic of how C# virtual and override mechanism works internally has been discussed to death amongst the programmers... but after half an hour on google, I cannot find an answer to the following question (see below):

Using a simple code:

public class BaseClass
{
  public virtual SayNo() { return "NO!!!"; }
}

public class SecondClass: BaseClass
{
  public override SayNo() { return "No."; }
}

public class ThirdClass: SecondClass
{
  public override SayNo() { return "No..."; }
}

class Program
{
  static void Main()
  {
     ThirdClass thirdclass = new ThirdClass();
     string a = thirdclass.SayNo(); // this would return "No..."

     // Question: 
     // Is there a way, not using the "new" keyword and/or the "hide"
     // mechansim (i.e. not modifying the 3 classes above), can we somehow return
     // a string from the SecondClass or even the BaseClass only using the 
     // variable "third"?

     // I know the lines below won't get me to "NO!!!"
     BaseClass bc = (BaseClass)thirdclass;
     string b = bc.SayNo(); // this gives me "No..." but how to I get to "NO!!!"?
  }
}

I think I can't get to the methods of base class or the intermediate derived class simply using the most derived instance (without modifying the method signatures of the 3 classes). But I would like to confirm and cement my understanding...

Thanks.

+1  A: 

You can't get to the base methods of an override. No matter how you cast the object, the last override in the instance is always used.

__grover
This isn't entirely true. You can always use "base" as in Jareds answer to call an overridden method in the base class, in this case SecondClass.
Pete
No you can't if you're outside of the object and that's what the question author has asked about. Of course you can get around it using IL call instead of callvirt, but C# is special that it never emits call except for static methods.
__grover
+5  A: 

Without modification to your sample and discounting reflection, no there is no way. The intent of the virtual system is to enforce calling the derived most no matter what and the CLR is good at its job.

There are a couple of ways you can work around this though.

Option 1: You could add the following method to ThirdClass

public void SayNoBase() {
  base.SayNo();
}

This would force the invocation of SecondClass.SayNo

Option 2: The main problem here is that you want to invoke a virtual method non-virtually. C# only provides one way of doing this via the base modifier. This makes it impossible to call a method within your own class in a non-virtual fashion. You can fix this by factoring it out into a second method and proxying.

public overrides void SayNo() {
  SayNoHelper();
}

public void SayNoHelper() {
  Console.WriteLine("No");
}
JaredPar
Also, if you had public class ThirdClass: BaseClass{ base.SayNo();}That would return NO!!!
Pete
+1  A: 

Sure...

   BaseClass bc = new BaseClass();
   string b = bc.SayNo();

"Virtual" means that the implementation which will be executed is based on the ACTUAL type of the underlying object, not the type of the variable it is stuffed in... So if the actual object is a ThirdClass, that's the implementation you will get, no matter what you cast it to. If you want the behavior you describe above, don't make the methods virtual...

If you're wondering "what's the point?" it's for 'polymorphism'; so that you can declare a collection, or a method parameter, as some base type, and include/ pass it a mix of derived types, and yet when, within the code, even though each object is assigned to a ref variable declared as the base type, for each one, the actual implementation which will be executed for any virtual method call will be that implementation defined in the class definition for the ACTUAL tyoe of each object...

Charles Bretana
A: 

If its backed with a field you could pull out the field using reflection.

Even if you pull off the methodinfo using reflection from typeof(BaseClass) you will still end up executing your overridden method

Sam Saffron
+8  A: 

C# can't do this but it is actually possible in IL using call instead of callvirt. You can thus work around C#'s limitation by using Reflection.Emit in combination with a DynamicMethod.

Here's a very simple example to illustrate how this works. If you really intend to use this, wrap it inside a nice function strive to make it work with different delegate types.

delegate string SayNoDelegate(BaseClass instance);

static void Main() {
    BaseClass target = new SecondClass();

    var method_args = new Type[] { typeof(BaseClass) };
    var pull = new DynamicMethod("pull", typeof(string), method_args);
    var method = typeof(BaseClass).GetMethod("SayNo", new Type[] {});
    var ilgen = pull.GetILGenerator();
    ilgen.Emit(OpCodes.Ldarg_0);
    ilgen.EmitCall(OpCodes.Call, method, null);
    ilgen.Emit(OpCodes.Ret);

    var call = (SayNoDelegate)pull.CreateDelegate(typeof(SayNoDelegate));
    Console.WriteLine("callvirt, in C#: {0}", target.SayNo());
    Console.WriteLine("call, in IL: {0}", call(target));
}

Prints:

callvirt, in C#: No.
call, in IL: NO!!!
Konrad Rudolph
I've only read the first few pages of CLR via C#, but answers like this make me want to take the day off and finish it!
overslacked
+3  A: 

Using base in C# only works for the immediate base. You can't access a base-base member.

It looks someone else beat me to the punch with the answer about it being possible to do in IL.

However, I think the way I did the code gen has some advantages, so I'll post it anyways.

The thing I did differently is to use expression trees, which enable you to use the C# compiler to do overload resolution and generic argument substitution.

That stuff is complicated, and you don't want to have to replicate it your self if you can help it. In your case, the code would work like this:

var del = 
    CreateNonVirtualCall<Program, BaseClass, Action<ThirdClass>>
    (
        x=>x.SayNo()
    );

You would probably want to store the delegate in a readonly static field, so that you only have to compile it once.

You need to specify 3 generic arguments:

  1. The owner type - This is the class that you would have invoked the code from if you were not using "CreateNonVirtualCall".

  2. The base class - This is the class you want to make the non virtual call from

  3. A delegate type. This should represent the signature of the method being called with an extra parameter for the "this" argument. It's possible to eliminate this, but it requires more work in the code gen method.

The method takes a single argument, a lambda representing the call. It has to be a call, and only a call. If you want to extend the code gen you can support more complex stuff.

For simplicicty, the lambda body is restricted to only being able to access lambda parameters, and can only pass them in directly to the function. You can remove this restriction if you extend the code gen in the method body to support all expression types. That would take some work though. You can do anything you want with the delegate that comes back, so the restriction isn't too big of a deal.

It's important to note that this code is not perfect. It could use a lot more validation, and it doesn't work with "ref" or "out" parameters because of expression tree limitations.

I did test it in sample cases with void methods, methods returning values, and generic methods, and it worked. I'm sure, however, you can find some edge cases that don't work.

In any case, here's the IL Gen Code:

public static TDelegate CreateNonVirtCall<TOwner, TBase, TDelegate>(Expression<TDelegate> call) where TDelegate : class
{
    if (! typeof(Delegate).IsAssignableFrom(typeof(TDelegate)))
    {
        throw new InvalidOperationException("TDelegate must be a delegate type.");
    }

    var body = call.Body as MethodCallExpression;

    if (body.NodeType != ExpressionType.Call || body == null)
    {
        throw new ArgumentException("Expected a call expression", "call");
    }

    foreach (var arg in body.Arguments)
    {
        if (arg.NodeType != ExpressionType.Parameter)
        {
            //to support non lambda parameter arguments, you need to add support for compiling all expression types.
            throw new ArgumentException("Expected a constant or parameter argument", "call");
        }
    }

    if (body.Object != null && body.Object.NodeType != ExpressionType.Parameter)
    {
        //to support a non constant base, you have to implement support for compiling all expression types.
        throw new ArgumentException("Expected a constant base expression", "call");
    }

    var paramMap = new Dictionary<string, int>();
    int index = 0;

    foreach (var item in call.Parameters)
    {
        paramMap.Add(item.Name, index++);
    }

    Type[] parameterTypes;


    parameterTypes = call.Parameters.Select(p => p.Type).ToArray();

    var m = 
        new DynamicMethod
        (
            "$something_unique", 
            body.Type, 
            parameterTypes,
            typeof(TOwner)
        );

    var builder = m.GetILGenerator();
    var callTarget = body.Method;

    if (body.Object != null)
    {
        var paramIndex = paramMap[((ParameterExpression)body.Object).Name];
        builder.Emit(OpCodes.Ldarg, paramIndex);
    }

    foreach (var item in body.Arguments)
    {
        var param = (ParameterExpression)item;

        builder.Emit(OpCodes.Ldarg, paramMap[param.Name]);
    }

    builder.EmitCall(OpCodes.Call, FindBaseMethod(typeof(TBase), callTarget), null);

    if (body.Type != typeof(void))
    {
        builder.Emit(OpCodes.Ret);
    }

    var obj = (object) m.CreateDelegate(typeof (TDelegate));
    return obj as TDelegate;
}
Scott Wisniewski
Cool piece of work. Definitely worth posting it!
Konrad Rudolph
Thanks. (argh! minimum comment length)
Scott Wisniewski