Después de ver el seminario web Jon Skeet Inspects ReSharper , comencé a jugar un poco con las llamadas recursivas al constructor y descubrí que el siguiente código es un código C # válido (por válido me refiero a que se compila).
class Foo
{
int a = null;
int b = AppDomain.CurrentDomain;
int c = "string to int";
int d = NonExistingMethod();
int e = Invalid<Method>Name<<Indeeed();
Foo() :this(0) { }
Foo(int v) :this() { }
}
Como probablemente todos sabemos, el compilador mueve la inicialización del campo al constructor. Entonces, si tiene un campo como int a = 42;
, lo tendrá a = 42
en todos los constructores. Pero si tiene un constructor llamando a otro constructor, tendrá el código de inicialización solo en uno llamado.
Por ejemplo, si tiene un constructor con parámetros que llaman al constructor predeterminado, solo tendrá asignación a = 42
en el constructor predeterminado.
Para ilustrar el segundo caso, el siguiente código:
class Foo
{
int a = 42;
Foo() :this(60) { }
Foo(int v) { }
}
Compila en:
internal class Foo
{
private int a;
private Foo()
{
this.ctor(60);
}
private Foo(int v)
{
this.a = 42;
base.ctor();
}
}
Entonces, el problema principal es que mi código, dado al comienzo de esta pregunta, está compilado en:
internal class Foo
{
private int a;
private int b;
private int c;
private int d;
private int e;
private Foo()
{
this.ctor(0);
}
private Foo(int v)
{
this.ctor();
}
}
Como puede ver, el compilador no puede decidir dónde colocar la inicialización del campo y, como resultado, no lo coloca en ninguna parte. También tenga en cuenta que no hay base
llamadas al constructor. Por supuesto, no se pueden crear objetos y siempre terminará con StackOverflowException
si intenta crear una instancia de Foo
.
Tengo dos preguntas:
¿Por qué el compilador permite llamadas recursivas al constructor?
¿Por qué observamos tal comportamiento del compilador para campos, inicializados dentro de dicha clase?
Algunas notas: ReSharper le advierte con Possible cyclic constructor calls
. Además, en Java, tales llamadas al constructor no compilan eventos, por lo que el compilador de Java es más restrictivo en este escenario (Jon mencionó esta información en el seminario web).
Esto hace que estas preguntas sean más interesantes, porque con todo respeto a la comunidad Java, el compilador de C # es al menos más moderno.
Esto fue compilado usando compiladores C # 4.0 y C # 5.0 y descompilado usando dotPeek .
int a = null; int b = AppDomain.CurrentDomain; int c = "string to int"; int d = NonExistingMethod(); int e = Invalid<Method>Name<<Indeeed();
Uno debería hacer una prueba: "¿En qué situación están bien estas declaraciones de campo?" (Hay una advertencia sobre los campos que no se están utilizando, pero puede deshacerse de esa advertencia leyendo cada campo dentro del cuerpo de uno de los constructores de intance (o en otro lugar).)Respuestas:
Interesante hallazgo.
Parece que en realidad solo hay dos tipos de constructores de instancias:
: this( ...)
sintaxis.: base()
es el predeterminado.(No tuve en cuenta el constructor de la instancia,
System.Object
que es un caso especial. ¡NoSystem.Object
tiene una clase base! PeroSystem.Object
tampoco tiene campos).Los inicializadores de campo de instancia que pueden estar presentes en la clase, deben copiarse al principio del cuerpo de todos los constructores de instancia del tipo 2. arriba, mientras que ningún constructor de instancia del tipo 1. necesita el código de asignación de campo.
Entonces, aparentemente, no hay necesidad de que el compilador de C # haga un análisis de los constructores de tipo 1. para ver si hay ciclos o no.
Ahora su ejemplo da una situación en la que todos los constructores de instancias son de tipo 1 .. En esa situación, no es necesario colocar el código del inicializador de campo en ningún lugar. Por tanto, parece que no se analiza muy a fondo.
Resulta que cuando todos los constructores de instancias son de tipo 1. , incluso puede derivar de una clase base que no tenga un constructor accesible. Sin embargo, la clase base no debe estar sellada. Por ejemplo, si escribe una clase con solo
private
constructores de instancia, las personas aún pueden derivar de su clase si hacen que todos los constructores de instancia en la clase derivada sean del tipo 1. anterior. Sin embargo, una nueva expresión de creación de objetos nunca terminará, por supuesto. Para crear instancias de la clase derivada, uno tendría que "hacer trampa" y usar cosas como elSystem.Runtime.Serialization.FormatterServices.GetUninitializedObject
método.Otro ejemplo: la
System.Globalization.TextInfo
clase solo tiene uninternal
constructor de instancia. Pero aún puede derivar de esta clase en un ensamblado que no seamscorlib.dll
con esta técnica.Finalmente, con respecto a la
sintaxis. De acuerdo con las reglas de C #, esto debe leerse como
porque el operador de desplazamiento a la izquierda
<<
tiene mayor precedencia que el operador menor que<
y el operador mayor que>
. Los dos últimos operadores tienen la misma precedencia y, por lo tanto, son evaluados por la regla asociativa de izquierda. Si los tipos fueranMySpecialType Invalid; int Method; int Name; int Indeed() { ... }
y si
MySpecialType
introdujo una(MySpecialType, int)
sobrecarga deoperator <
, entonces la expresiónsería legal y significativo.
En mi opinión, sería mejor si el compilador emitiera una advertencia en este escenario. Por ejemplo, podría decir
unreachable code detected
y señalar la línea y el número de columna del inicializador de campo que nunca se traduce a IL.fuente
class Example { int field = 42; internal Example() { /* some code here */ field = 100; } }
luego el IL producido por eso coloca la42
asignación en el constructor de instancia, antes que todo lo demás, exactamente como si hubiera escrito:class Example { int field; internal Example() { field = 42; /* some code here */ field = 100; } }
Creo que porque la especificación del lenguaje solo descarta la invocación directa del mismo constructor que se está definiendo.
Desde el 10.11.1:
...
Esa última oración parece impedir que la autodenominación directa produzca un error de tiempo de compilación, por ejemplo
Foo() : this() {}
es ilegal.
Sin embargo, lo admito, no veo una razón específica para permitirlo. Por supuesto, en el nivel de IL, tales construcciones están permitidas porque creo que se podrían seleccionar diferentes constructores de instancias en tiempo de ejecución, por lo que podría tener recursividad siempre que termine.
Creo que la otra razón por la que no marca ni advierte sobre esto es porque no tiene necesidad de detectar esta situación. Imagínese que persigue a través de cientos de diferentes constructores, sólo para ver si un ciclo hace existir - cuando cualquier intento de uso será rápidamente (como sabemos) estallar en tiempo de ejecución, por un caso bastante borde.
Cuando realiza la generación de código para cada constructor, todo lo que considera son
constructor-initializer
los inicializadores de campo y el cuerpo del constructor; no considera ningún otro código:Si
constructor-initializer
es un constructor de instancia para la clase en sí, no emite los inicializadores de campo; emite laconstructor-initializer
llamada y luego el cuerpo.Si
constructor-initializer
es un constructor de instancias para la clase base directa, emite los inicializadores de campo, luego laconstructor-initializer
llamada y luego el cuerpo.En ninguno de los casos necesita buscar en otra parte, por lo que no es "incapaz" de decidir dónde colocar los inicializadores de campo, solo sigue algunas reglas simples que solo consideran el constructor actual.
fuente
int e = Invalid<Method>Name<<Indeeed();
. Digo que es un error del compilador.int e = Invalid < Method > Name << Indeed();
con operadores binarios "menor que", "mayor que" y "desplazamiento a la izquierda". Eso está bien sintácticamente, pero serían algunas sobrecargas realmente locas de los operadores para hacerlo bien con una escritura fuerte.Invalid
etc que la harán válida). El error normalmente se detecta en la generación del código, pero encontró una manera de escribir código que nunca se genera. Encontraste un agujero furtivo en el compilador (una forma de escribir código que nunca será compilado), pero no uno serio ya que el código ofensivo es inalcanzable de todos modos.Tu ejemplo
class Foo { int a = 42; Foo() :this(60) { } Foo(int v) { } }
funcionará bien, en el sentido de que puede crear una instancia de ese objeto Foo sin problemas. Sin embargo, lo siguiente sería más parecido al código sobre el que está preguntando
class Foo { int a = 42; Foo() :this(60) { } Foo(int v) : this() { } }
Tanto eso como su código crearán un stackoverflow (!), Porque la recursividad nunca toca fondo. Entonces su código es ignorado porque nunca llega a ejecutarse.
En otras palabras, el compilador no puede decidir dónde colocar el código defectuoso porque puede decir que la recursividad nunca toca fondo. Creo que esto se debe a que tiene que ponerlo donde solo se llamará una vez, pero la naturaleza recursiva de los constructores lo hace imposible.
La recursividad en el sentido de que un constructor crea instancias de sí mismo dentro del cuerpo del constructor tiene sentido para mí, porque por ejemplo, eso podría usarse para instanciar árboles donde cada nodo apunta a otros nodos. Pero la recursividad a través de los preconstructores del tipo ilustrado por esta pregunta nunca puede tocar fondo, por lo que tendría sentido para mí si eso no estuviera permitido.
fuente
ambiguous method call
, no omite dicha llamada al método. Si fuera un compilador, también arrojaría un error en este escenario.Creo que esto está permitido porque aún puede (podría) capturar la Excepción y hacer algo significativo con ella.
La inicialización nunca se ejecutará, y casi con certeza lanzará una StackOverflowException. Pero esto todavía puede ser un comportamiento deseado y no siempre significaba que el proceso debería fallar.
Como se explica aquí https://stackoverflow.com/a/1599236/869482
fuente