Внутренняя работа C # Virtual и Override - PullRequest
9 голосов
/ 15 апреля 2009

Тема о том, как работает виртуальный механизм C + и механизм переопределения, обсуждалась до смерти программистами ... но после получаса в Google я не могу найти ответ на следующий вопрос (см. Ниже):

Используя простой код:

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!!!"?
  }
}

Я думаю, что не могу добраться до методов базового класса или промежуточного производного класса, просто используя самый производный экземпляр (без изменения сигнатур методов 3 классов). Но я хотел бы подтвердить и укрепить мое понимание ...

Спасибо.

Ответы [ 6 ]

14 голосов
/ 15 апреля 2009

C # не может этого сделать, но на самом деле возможно в IL, используя call вместо callvirt. Таким образом, вы можете обойти ограничения C #, используя Reflection.Emit в сочетании с DynamicMethod.

Вот очень простой пример, иллюстрирующий, как это работает. Если вы действительно намерены использовать это, оберните его внутри хорошей функции, стремясь заставить ее работать с различными типами делегатов.

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));
}

Печать:

callvirt, in C#: No.
call, in IL: NO!!!
7 голосов
/ 15 апреля 2009

Без внесения изменений в ваш образец и дисконтирования отражений, нет никакого способа. Целью виртуальной системы является принудительный вызов производного, несмотря ни на что, и CLR хорошо справляется со своей задачей.

Однако есть несколько способов обойти это.

Вариант 1: Вы можете добавить следующий метод в ThirdClass

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

Это приведет к вызову SecondClass.SayNo

Вариант 2. Основная проблема заключается в том, что вы хотите вызывать виртуальный метод не виртуально. C # предоставляет только один способ сделать это через модификатор base. Это делает невозможным вызов метода в вашем собственном классе не виртуальным способом. Вы можете исправить это, добавив второй метод и проксируя.

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

public void SayNoHelper() {
  Console.WriteLine("No");
}
2 голосов
/ 15 апреля 2009

Использование base в C # работает только для непосредственной базы. Вы не можете получить доступ к элементу base-base.

Похоже, кто-то другой избил меня до отказа, ответив, что это можно сделать в IL.

Тем не менее, я думаю, что способ, которым я сделал code gen, имеет некоторые преимущества, поэтому я все равно опубликую его.

То, что я сделал по-другому, - это использование деревьев выражений, которые позволяют использовать компилятор C # для разрешения перегрузки и подстановки общих аргументов.

Эта штука сложная, и вам не нужно копировать ее самостоятельно, если вы можете ей помочь. В вашем случае код будет работать так:

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

Возможно, вы захотите сохранить делегат в статическом поле, предназначенном только для чтения, так что вам придется скомпилировать его только один раз.

Вам необходимо указать 3 общих аргумента:

  1. Тип владельца - это класс, из которого вы бы вызвали код, если бы не использовали «CreateNonVirtualCall».

  2. Базовый класс - это класс, с которого вы хотите сделать не виртуальный вызов

  3. Тип делегата. Это должно представлять сигнатуру вызываемого метода с дополнительным параметром для аргумента this. Это можно устранить, но это требует больше работы в методе code gen.

Метод принимает один аргумент - лямбда, представляющую вызов. Это должен быть звонок, и только звонок. Если вы хотите расширить код поколения, вы можете поддерживать более сложные вещи.

Для простоты лямбда-тело ограничено только возможностью доступа к лямбда-параметрам и может только передавать их непосредственно в функцию. Вы можете снять это ограничение, если вы расширите код gen в теле метода для поддержки всех типов выражений. Это займет некоторую работу, хотя. С возвращающимся делегатом вы можете делать все, что захотите, поэтому ограничение не слишком велико.

Важно отметить, что этот код не идеален. Он может использовать гораздо больше проверок и не работает с параметрами «ref» или «out» из-за ограничений дерева выражений.

Я проверил его в примерах с помощью методов void, методов, возвращающих значения, и универсальных методов, и это сработало. Однако я уверен, что вы можете найти некоторые крайние случаи, которые не работают.

В любом случае, вот код IL Gen:

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;
}
2 голосов
/ 15 апреля 2009

Конечно ...

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

«Виртуальный» означает , что реализация, которая будет выполняться, основана на АКТУАЛЬНОМ типе базового объекта, а не на типе переменной, в которую он вставлен ... Так что если фактический объект Третий класс, это реализация, которую вы получите, независимо от того, к чему вы ее приведете. Если вам нужно поведение, которое вы описали выше, не делайте методы виртуальными ...

Если вам интересно "какой смысл?" это для «полиморфизма»; так что вы можете объявить коллекцию или параметр метода как некоторый базовый тип и включить / передать ему набор производных типов, и все же, когда в коде, даже если каждый объект назначен переменной ref, объявленной как базовый тип, для каждого фактическая реализация, которая будет выполняться для любого вызова виртуального метода, будет той реализацией, определенной в определении класса для фактического типа каждого объекта ...

1 голос
/ 15 апреля 2009

Вы не можете получить базовые методы переопределения. Независимо от того, как вы приведете объект, всегда используется последнее переопределение в экземпляре.

0 голосов
/ 15 апреля 2009

Если оно опирается на поле, вы можете вытащить поле с помощью отражения.

Даже если вы извлечете methodinfo, используя отражение от typeof (BaseClass), вы все равно будете выполнять переопределенный метод

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...