2010-03-15 16 views
12

Considere el siguiente código:C# - cierres sobre campos de clase dentro de un inicializador?

using System; 

namespace ConsoleApplication2 
{ 
    class Program 
    { 
     static void Main(string[] args) 
     { 
      var square = new Square(4); 
      Console.WriteLine(square.Calculate()); 
     } 
    } 

    class MathOp 
    {   
     protected MathOp(Func<int> calc) { _calc = calc; } 
     public int Calculate() { return _calc(); } 
     private Func<int> _calc; 
    } 

    class Square : MathOp 
    { 
     public Square(int operand) 
      : base(() => _operand * _operand) // runtime exception 
     { 
      _operand = operand; 
     } 

     private int _operand; 
    } 
} 

(! Ignoran el diseño de la clase, no estoy realmente escribir una calculadora de este código representa meramente una repro mínimo para un problema mucho más grande que tomó un tiempo para reducir)

yo esperaría que sea:

  • de impresión "16", O
  • lanzar un error de tiempo de compilación si no se permite el cierre sobre un campo miembro en este escenario

En su lugar obtengo una excepción sin sentido lanzada en la línea indicada. En el 3.0 CLR es NullReferenceException; en el Silverlight CLR es el infame La operación podría desestabilizar el tiempo de ejecución.

+0

No compila para mí .... "Se requiere una referencia de objeto para el campo, el método o la propiedad no estáticos 'ConsoleApplication2 .Square._operand'". ¿Es este tu código exacto? –

+0

Sí, es una copia/pega, y compila para mí. –

+0

Tenga en cuenta que estoy en VS2008 - como señaló Aaron, el equipo compilador de 2010 puede haber clasificado esto como un error (es decir, estuvo de acuerdo conmigo :)) –

Respuesta

11

No va a dar lugar a un error en tiempo de compilación porque es un cierre válido.

El problema es que this aún no se ha inicializado en el momento en que se creó el cierre. Su constructor no se ha ejecutado aún cuando se proporciona ese argumento. Entonces, el resultante NullReferenceException es bastante lógico. ¡Es this que es null!

Te lo demostraré. Vamos a reescribir el código de esta manera:

class Program 
{ 
    static void Main(string[] args) 
    { 
     var test = new DerivedTest(); 
     object o = test.Func(); 
     Console.WriteLine(o == null); 
     Console.ReadLine(); 
    } 
} 

class BaseTest 
{ 
    public BaseTest(Func<object> func) 
    { 
     this.Func = func; 
    } 

    public Func<object> Func { get; private set; } 
} 

class DerivedTest : BaseTest 
{ 
    public DerivedTest() : base(() => this) 
    { 
    } 
} 

¿Adivina qué se imprime? Sí, es true, el cierre devuelve null porque this no se inicializa cuando se ejecuta.

Editar

tenía curiosidad acerca de la declaración de Thomas, pensando que tal vez se había cambiado el comportamiento en una posterior VS liberación. De hecho, encontré un Microsoft Connect issue sobre esto mismo. Fue cerrado como "no arreglará". Impar.

Como dice Microsoft en su respuesta, normalmente no es válido utilizar la referencia this dentro de la lista de argumentos de una llamada al constructor base; la referencia simplemente no existe en ese momento y obtendrá un error en tiempo de compilación si intenta usarlo "desnudo". Entonces, podría decirse si produce un error de compilación para el caso de cierre, pero la referencia this está oculta del compilador, que (al menos en VS 2008) debería saber mirar dentro del cierre para evitar que las personas hagan esto. No es así, por lo que terminas con este comportamiento.

+0

¿Lo has intentado? Obtengo un error de compilación ... –

+0

@Thomas Levesque: Sí, lo hice, compilé y recibí el mismo error de tiempo de ejecución. Es curioso que hayas obtenido un error de compilación; Estoy en VS 2008, ¿estás en VS 2010? ¿Tal vez clasificaron esto como un error y actualizaron el compilador para detectar esto? – Aaronaught

+0

+1 Buena explicación. Lo sospechaba, pero no podía estar seguro de que la falta de/this/pointer en mi ventana de observación no fuera solo una rareza VS (me parece que se confunde con demasiada facilidad). –

0

¿Ha intentado usar () => operand * operand en su lugar? El problema es que no hay certeza de que la operación se establecerá cuando llame a la base. Sí, está intentando crear un cierre en su método, y no hay garantía del orden de las cosas aquí.

Dado que no está configurando _operand en absoluto, yo recomendaría simplemente usar () => operand * operand en su lugar.

+1

Funcionará, pero tiene un significado muy diferente ... –

+0

Basta decir que esto frustra el propósito. En mi código "real" tengo varios MathOps muy complejos. Algunos de los pasos son comunes para todos los MathOps, así que los coloco en la clase base. En una operación en particular, la primera parte del cálculo es invariante: quería optimizarlo almacenando en caché el resultado intermedio en un campo miembro, luego dejando que el resto del cálculo (que varía según los parámetros para calcular) continúe como siempre. . –

+1

@Richard Berg: ¿Quizás podría solucionar el problema usando un método de inicialización protegido en su lugar? Estoy seguro de que ya has pensado en eso, pero no hace daño mencionar ... – Aaronaught

2

¿Qué tal esto:

using System; 
using System.Linq.Expressions; 

namespace ConsoleApplication2 
{ 
    class Program 
    { 
     static void Main(string[] args) 
     { 
      var square = new Square(4); 
      Console.WriteLine(square.Calculate()); 
     } 
    } 

    class MathOp 
    { 
     protected MathOp(Expression<Func<int>> calc) { _calc = calc.Compile(); } 
     public int Calculate() { return _calc(); } 
     private Func<int> _calc; 
    } 

    class Square : MathOp 
    { 
     public Square(int operand) 
      : base(() => _operand * _operand) 
     { 
      _operand = operand; 
     } 

     private int _operand; 
    } 
} 
+0

forma interesante de diferir la resolución. Todavía no funciona en 2010 sin embargo. – Jimmy

+0

Inteligente. +1 para la solución más rápida (no es necesario refactorizar). –

14

Fue un error del compilador que se ha fijado. El código nunca debería haber sido legal en primer lugar, y si lo vamos a permitir, deberíamos haber generado al menos un código válido. Mi error. Perdon por el inconveniente.

Cuestiones relacionadas