2009-04-15 15 views
9

El tema de cómo funciona el mecanismo virtual e invalidación de C# internamente ha sido discutido hasta la muerte entre los programadores ... pero después de media hora en google, no puedo encontrar la respuesta a la siguiente pregunta (ver a continuación):Funcionamientos internos de C# Virtual e Override

el uso de un código simple:

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

pienso que no pueda llegar a los métodos de clase base o la clase derivada intermedia simplemente utilizando la instancia más derivada (sin modificando las firmas de método de las 3 clases). Pero me gustaría confirmar y consolidar mi comprensión ...

Gracias.

+0

¿tenía intención de una de sus clases para heredar de BasClass (digamos de segunda clase)? –

+0

No del todo; no hay más clases para agregar o cambiar ... – henry000

Respuesta

1

No puede acceder a los métodos básicos de una anulación. No importa cómo proyecte el objeto, siempre se usa la última anulación en la instancia.

+0

Esto no es del todo cierto. Siempre puede usar "base" como en la respuesta de Jareds para llamar a un método anulado en la clase base, en este caso SecondClass. – Pete

+1

No, no puede si está fuera del objeto y eso es lo que le preguntó el autor de la pregunta. Por supuesto, puede sortearlo utilizando la llamada IL en lugar de callvirt, pero C# es especial porque nunca emite llamadas, excepto por métodos estáticos. – grover

7

Sin modificación en su muestra y reflejo de descuento, no hay forma. La intención del sistema virtual es hacer que las llamadas sean más derivadas sin importar nada y el CLR es bueno en su trabajo.

Sin embargo, hay un par de formas en que puede evitar esto.

Opción 1: Se podría añadir el siguiente método para ThirdClass

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

Esto obligaría a la invocación de SecondClass.SayNo

Opción 2: El principal problema aquí es que desea invocar un Portal método no virtualmente. C# solo proporciona una forma de hacerlo a través del modificador de base. Esto hace que sea imposible llamar a un método dentro de su propia clase de una manera no virtual. Puede solucionar esto descomponiéndolo en un segundo método y proxying.

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

public void SayNoHelper() { 
    Console.WriteLine("No"); 
} 
+0

Además, si tenía clase pública ThirdClass: BaseClass { base.SayNo(); } ¡Eso volvería NO! – Pete

2

Claro ...

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

"virtual" significa que la aplicación que se ejecutará se basa en el tipo real del objeto subyacente, no el tipo de la variable es rellenas ... Entonces, si el objeto real es un ThirdClass, esa es la implementación que obtendrás, sin importar a qué lo arrojes. Si desea el comportamiento que describe arriba, no haga que los métodos sean virtuales ...

Si se está preguntando "¿cuál es el punto?" es para 'polimorfismo'; para que pueda declarar una colección, o un parámetro de método, como un tipo base, e incluir/pasar una mezcla de tipos derivados, y aún cuando, dentro del código, aunque cada objeto se asigne a una variable de referencia declarada como tipo de base, para cada uno, la implementación real que se ejecutará para cualquier llamada de método virtual será esa implementación definida en la definición de clase para la tonalidad real de cada objeto ...

0

Si está respaldado con un campo que podría saca el campo usando la reflexión.

Incluso si se tira de la MethodInfo utilizando la reflexión de typeof (BaseClass) que todavía va a terminar la ejecución de su método reemplazado

14

C# no puede hacer esto, pero es realmente posible en la IL usando call en lugar de callvirt. Por lo tanto, puede trabajar alrededor de la limitación de C# utilizando Reflection.Emit en combinación con un DynamicMethod.

Aquí hay un ejemplo muy simple para ilustrar cómo funciona esto. Si realmente desea usar esto, envuélvalo dentro de una función agradable para que funcione con diferentes tipos de delegados.

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

Lienzo:

callvirt, in C#: No. 
call, in IL: NO!!! 
+1

Solo he leído las primeras páginas de CLR a través de C#, pero respuestas como esta me dan ganas de tomar el día libre y terminarlo. – overslacked

+0

@overslacked, yo también. así que quiero tener suficiente tiempo para terminar el libro: CLR a través de C#. – Attilah

2

Usando base en C# sólo funciona para la base de inmediato. No puede acceder a un miembro base-base.

Parece que alguien más me golpeó hasta el golpe con la respuesta de que es posible hacerlo en IL.

Sin embargo, creo que la forma en que hice el código gen tiene algunas ventajas, así que lo publicaré de todos modos.

Lo que hice de manera diferente es usar árboles de expresiones, que le permiten usar el compilador de C# para realizar la resolución de sobrecarga y la sustitución de argumentos genéricos.

Eso es complicado, y no quiere tener que replicarlo usted mismo si puede evitarlo. En su caso, el código sería el siguiente:

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

Es probable que desee almacenar el delegado en un campo estático de sólo lectura, por lo que sólo tiene que compilar una vez.

es necesario especificar 3 argumentos genéricos:

  1. El tipo de propietario - Esta es la clase que usted habría invocado el código de si no estuviera usando "CreateNonVirtualCall".

  2. La clase base - Esta es la clase que desea realizar la llamada no virtual a partir de

  3. Un tipo de delegado. Esto debería representar la firma del método que se está llamando con un parámetro adicional para el argumento "this". Es posible eliminar esto, pero requiere más trabajo en el método de código genético.

El método tiene un único argumento, una lambda que representa la llamada. Tiene que ser una llamada y solo una llamada. Si desea extender el gen del código, puede admitir cosas más complejas.

Por simplicidad, el cuerpo lambda está restringido a solo poder acceder a los parámetros lambda, y solo puede pasarlos directamente a la función. Puede eliminar esta restricción si amplía el código gen en el cuerpo del método para admitir todos los tipos de expresiones. Eso tomaría un poco de trabajo sin embargo. Puedes hacer lo que quieras con el delegado que vuelve, por lo que la restricción no es demasiado importante.

Es importante tener en cuenta que este código no es perfecto. Podría usar mucha más validación, y no funciona con los parámetros "ref" o "out" debido a las limitaciones del árbol de expresiones.

Lo probé en casos de muestra con métodos nulos, métodos de devolución de valores y métodos genéricos, y funcionó. Sin embargo, estoy seguro de que puede encontrar algunos casos extremos que no funcionan.

En cualquier caso, aquí está el código Gen IL:

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; 
} 
+1

Genial pieza de trabajo. Definitivamente vale la pena publicarlo! –

+0

Gracias. (argh! mínima longitud de comentario) –