- "Depending on how you interpret it, the results can be whatever you want"
Today we looked at one of the pieces of community feedback for primary constructors, around name shadowing from base types. A user might not expect the output that this program will give:
public class Base
{
protected readonly string a = "base";
}
public class Derived(string a = "derived") : Base
{
public void M()
{
Console.WriteLine(a);
}
}
This code will print base
when Derived.M()
is called, because the base field shadows the primary constructor parameter. While this could be confusing
for this particular case, we don't think this is common case for this type of code; instead, the common case is something more like the following:
public class Base(string a)
{
protected readonly string a = a;
}
public class Derived(string a = "derived") : Base(a)
{
public void M()
{
Console.WriteLine(a);
}
}
In this example, a
is passed through to Base
via its constructor, and the protected field is referenced by the derived type ensuring that the value
is not double-stored. There are a few possible options we can look for improving the first scenario:
- Warn whenever a base member shadows a primary constructor parameter at the use site. This would produce a warning in both of the above code samples.
- A variation on 1, produce a warning whenever a base member shadows a primary constructor parameter and that parameter wasn't passed to the base type.
There are 2 subvariants of this:
- Ensure that names match when doing this. IE, passing
a
to a parameter namedb
would not count for the purposes of suppressing the warning. - Do no validation on the name of the parameter.
- Ensure that names match when doing this. IE, passing
- Change the shadowing order: make the primary ctor parameter shadow the base member, so accessing the base member would require
base.
qualification.
For 1, we're concerned about the impact to the second code example: that seems the far more common case in the wild, and impacting it negatively isn't
great. For 3, we're unsure whether the effective behavior being different than records will be confusing: Derived
would not create a new member named
a
, it would use the existing one from the base. This leaves us with option 2.
For 2, we thought a bit about the subvariants. We're a bit concerned by field naming conventions differing from parameters: what guarantees are there
that a constructor parameter named a
will actually assign to a field named a
? There are none, of course, only long-standing conventions. Some
users prefer to name their fields with _
prefixes (following the C# code-style guidelines), but many keep them exactly the same. We ultimately think
that there's enough signal of intent in passing the primary ctor parameter, no matter what the name of the base ctor parameter is, and we can suppress
the warning for this case.
Option 2.2: Produce a warning on usage when a base member shadows a primary constructor parameter if that primary constructor parameter was not passed to the base type via its constructor.