Interesting find.
It appears that there are really only two kinds of instance constructors:
- An instance constructor which chains another instance constructor of the same type, with the
: this( ...)
syntax. - 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.