Today I’ve been reviewing the ECMA-334 C# specification, and in particular the section about class instance constructors.
I was struck by this piece in a clause about default constructors:
If a class contains no instance constructor declarations, a default instance constructor is automatically provided. That default constructor simply invokes the parameterless constructor of the direct base class.
I believe this to be incorrect, and indeed it is, as shown here (in C# 6 code for brevity, despite this being the C# 5 spec that I’m reviewing; that’s irrelevant in this case):
using System; class Base { public int Foo { get; } public Base(int foo = 5) { Foo = foo; } } class Derived : Base { } class Test { static void Main() { var d = new Derived(); Console.WriteLine(d.Foo); // Prints 5 } }
Here the default constructor in Derived
clearly doesn’t execute a parameterless constructor in Base
because there is no parameterless constructor in Base
. Instead, it executes the parameterized constructor, providing the default argument value.
So, I considered whether we could reword the standard to something like:
If a class contains no instance constructor declarations, a default instance constructor is automatically provided. That default constructor simply invokes a constructor of the direct base class as if the default constructor contained a constructor initializer of
base()
.
But is that always the case? It turns out it’s not – at least not in Roslyn. There are more interesting optional parameters we can use than just int foo = 5
. Let’s have a look:
using System; using System.Runtime.CompilerServices; class Base { public string Origin { get; } public Base([CallerMemberName] string name = "Unspecified", [CallerFilePath] string source = "Unspecified", [CallerLineNumber] int line = -1) { Origin = $"{name} - {source}:{line}"; } } class Derived1 : Base {} class Derived2 : Base { public Derived2() {} } class Derived3 : Base { public Derived3() : base() {} } class Test { static void Main() { Console.WriteLine(new Derived1().Origin); Console.WriteLine(new Derived2().Origin); Console.WriteLine(new Derived3().Origin); } }
The result is:
Unspecified - Unspecified:-1 Unspecified - Unspecified:-1 .ctor - c:\Users\Jon\Test\Test.cs:23
When base()
is explicitly specified, that source location is treated as the “caller” for caller member info attributes. When it’s implicit (including when there’s a default constructor), no source location is made available to the Base
constructor.
This is somewhat compiler-specific – and I can imagine different results where the default constructor could specify a name but not source file or line number, and the declared constructor with an implicit call could specify the name and source file but no line number.
I would never suggest using this little tidbit of Roslyn implementation trivia, but it’s fun nonetheless…
Jon – very interesting bit of Trivia. One minor typo: in the first code sample you have: “Foo = 5;” for the assignment that should be “Foo = foo;”. Of course, with that typo corrected, the behavior is still as described in your article.
LikeLiked by 1 person
Doh! Fixed, thanks :)
LikeLike
Is it the C# spec that is incorrect, or the C# implementation that is incorrect? By definition, the spec says how the language should work. If the language doesn’t do what the spec says, isn’t it an implementation problem?
LikeLike
Well, it’s a spec problem if the spec doesn’t say what the language designers intended it to say, which is what I believe the case is here :)
LikeLike
“Is it the C# spec that is incorrect, or the C# implementation that is incorrect? By definition, the spec says how the language should work. If the language doesn’t do what the spec says, isn’t it an implementation problem?”
So if a spec had wording that implied that, say, every arithmetic operation must throw an exception, then the spec isn’t incorrect?
In this case, the spec says that the default constructor of the derived class “simply invokes the parameterless constructor of the direct base class” but, as Jon noted (did you even read it?) , there is no such constructor in the base class. How is the implementation incorrect when the spec is incoherent?
LikeLike
Yesssss!!!! Jon is back!
LikeLiked by 2 people
For comparison, in
mcs
the implicitly provided constructors behave identical to the explicitly provided constructor:LikeLiked by 1 person
Hm, wouldn’t it be a good idea to specify all this behavior instead of leaving it unspecified? Better to make a decision and force all compilers to the same rules.
LikeLike
The specification says “If a class contains no instance constructor declarations…”, but you HAVE defined an instance constructor. If the specification said “If a class contains no parameterless instance constructor declaration” then it would be wrong.
LikeLike
Sorry, I read that wrong. I see it now, but it still seems “odd”. Your base declares an instance constructor with an optional parameter. Are you sure the problem isn’t with how instance constructors with an optional parameter is implemented? It wouldn’t make any sense to have both an single optional parameter AND a parameterless constructor at the same time.
LikeLike
I don’t think there’s any problem in the implementation – I think it’s just a matter of the spec needing to be updated in the light of optional parameters.
LikeLike
Ugh, nevermind, it’s the second part that is wrong.
LikeLike
I’m surprised this works. Won’t there be nasty problems if the default value of the base constructor parameter is changed and it resides in a different assembly (meaning the assembly with the derived class is not re-compiled and simply uses the newer base class version)?
While I believe that this is a problem for any use of default parameters, it appears to be even more severe here as the base class ctor call is virtually invisible.
LikeLike
No more than if there were an explicitly-specified constructor, with
base()
either being implicit or explicit. Yes, it’s a concern – but far from unique to this situation. Basically, changing the default argument of anything is a breaking change.LikeLike
As per the wording of the spec as Base provides a constructor, a default constructor will not be created for it, subclasses would need to call the constructor provided i.e. Base(int foo = 5). As such, I don’t think it’s incongruous for Derived to implicitly call Base(), would you have the compiler issue an error and force the developer to explicitly declare a constructor for the Derived class?
As Jonathan Pryor pointed out the behavior in the 2nd example seems to differ between implementations of Rosalyn. I think the 2nd example is illustrating a bug in the Microsoft .NET implementation of Rosalyn.
LikeLike
No – but that’s what the spec says at the moment. That’s why I’m working to change the spec.
I think it would be reasonable for it to always provide the member name. For the filename and line number… that’s harder to say, IMO. Where there’s an explicitly declared constructor, I think it makes sense to specify the filename, as we know where the member is declared – but including a line number suggests that there is a constructor initializer in the file (on that line), when there might not be. I think it would be sane to have:
base()
call? Name, filename, no line numberbase()
call? Name, filename and line numberLikeLike
ECMA-334(4th) uses the term “parameterless” 19 times. I bet there are fun bugs or, at the very least, unexpected behavior lurking behind a lot of them when combined with optional arguments. Perhaps the solution is to define “parameterless” as part of overload resolution, i.e. “can resolve an overload with zero actual arguments”?
In fact, optional arguments probably show up in the new spec as part of overload resolution anyway.
LikeLike
I had no idea you could assign a value to a “read only” property …
LikeLike
Only from a constructor – just like assigning to a read-only field (which is what it’s doing behind the scenes).
LikeLike
is this something new in the latest version of C# or something ?
LikeLike
I think until read-only fields exists :)
LikeLike
I’m afraid I don’t understand your point here…
LikeLike
It’s new to C# 6, yes.
LikeLike
Enlightened. :)
LikeLike