Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why does a recursive constructor call make invalid C# code compile?

Tags:

c#

After watching webinar Jon Skeet Inspects ReSharper, I've started to play a little with recursive constructor calls and found, that the following code is valid C# code (by valid I mean it compiles).

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()   { } } 

As we all probably know, field initialization is moved into constructor by the compiler. So if you have a field like int a = 42;, you will have a = 42 in all constructors. But if you have constructor calling another constructor, you will have initialization code only in called one.

For example if you have constructor with parameters calling default constructor, you will have assignment a = 42 only in the default constructor.

To illustrate second case, next code:

class Foo {     int a = 42;      Foo() :this(60)  { }     Foo(int v)       { } } 

Compiles into:

internal class Foo {     private int a;      private Foo()     {         this.ctor(60);     }      private Foo(int v)     {         this.a = 42;         base.ctor();     } } 

So the main issue, is that my code, given at the start of this question, is compiled into:

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

As you can see, the compiler can't decide where to put field initialization and, as result, doesn't put it anywhere. Also note, there are no base constructor calls. Of course, no objects can be created, and you will always end up with StackOverflowException if you will try to create an instance of Foo.

I have two questions:

Why does compiler allow recursive constructor calls at all?

Why we observe such behavior of the compiler for fields, initialized within such class?


Some notes: ReSharper warns you with Possible cyclic constructor calls. Moreover, in Java such constructor calls won't event compile, so the Java compiler is more restrictive in this scenario (Jon mentioned this information at the webinar).

This makes these questions more interesting, because with all respect to Java community, the C# compiler is at least more modern.

This was compiled using C# 4.0 and C# 5.0 compilers and decompiled using dotPeek.

like image 867
Ilya Ivanov Avatar asked May 20 '13 08:05

Ilya Ivanov


People also ask

Can you call a constructor recursively?

If a constructor calls itself, then the error message “recursive constructor invocation” occurs. The following program is not allowed by the compiler because inside the constructor we tried to call the same constructor.

Does JVM recurse constructor?

In Java, constructors cannot be recursive.

Can a constructor call itself?

BC30298: Constructor '<name>' cannot call itself A constructor is permitted to call another constructor to perform its functionality in addition to its own. But it is meaningless for a constructor to call itself, and in fact it would result in infinite recursion if permitted.

What is recursive invocation?

A recursive call is one where procedure A calls itself or calls procedure B which then calls procedure A again. Each recursive call causes a new invocation of the procedure to be placed on the call stack.


1 Answers

Interesting find.

It appears that there are really only two kinds of instance constructors:

  1. An instance constructor which chains another instance constructor of the same type, with the : this( ...) syntax.
  2. An instance constructor which chains an instance constructor of the base class. This includes instance constructors where no chainig is specified, since : base() is the default.

(I disregarded the instance constructor of System.Object which is a special case. System.Object has no base class! But System.Object has no fields either.)

The instance field initializers that might be present in the class, need to be copied into the beginning of the body of all instance constructors of type 2. above, whereas no instance constructors of type 1. need the field assignment code.

So apparently there's no need for the C# compiler to do an analysis of the constructors of type 1. to see if there are cycles or not.

Now your example gives a situation where all instance constructors are of type 1.. In that situation the field initaializer code does not need to be put anywhere. So it is not analyzed very deeply, it seems.

It turns out that when all instance constructors are of type 1., you can even derive from a base class that has no accessible constructor. The base class must be non-sealed, though. For example if you write a class with only private instance constructors, people can still derive from your class if they make all instance constructors in the derived class be of type 1. above. However, an new object creation expression will never finish, of course. To create instances of the derived class, one would have to "cheat" and use stuff like the System.Runtime.Serialization.FormatterServices.GetUninitializedObject method.

Another example: The System.Globalization.TextInfo class has only an internal instance constructor. But you can still derive from this class in an assembly other than mscorlib.dll with this technique.

Finally, regarding the

Invalid<Method>Name<<Indeeed() 

syntax. According to the C# rules, this is to be read as

(Invalid < Method) > (Name << Indeeed()) 

because the left-shift operator << has higher precedence than both the less-than operator < and the greater-than operator >. The latter two operarors have the same precedence, and are therefore evaluated by the left-associative rule. If the types were

MySpecialType Invalid; int Method; int Name; int Indeed() { ... } 

and if the MySpecialType introduced an (MySpecialType, int) overload of the operator <, then the expression

Invalid < Method > Name << Indeeed() 

would be legal and meaningful.


In my opinion, it would be better if the compiler issued a warning in this scenario. For example, it could say unreachable code detected and point to the line and column number of the field initializer that is never translated into IL.

like image 181
Jeppe Stig Nielsen Avatar answered Oct 05 '22 11:10

Jeppe Stig Nielsen