meetings/2020/LDM-2020-06-15.md
modreq for init accessors
Initializing readonly fields in same type
init methods
Equality dispatch
Confirming some previous design decisions
IEnumerable.Current
modreq for init accessorsWe've confirmed that the modreq design for init accessors:
- The modreq type `IsExternalInit` will be present in .NET 5.0, and will be recognized if
defined in source
- The feature will only be fully supported in .NET 5.0
- Usage of the property (including the getter) will not be possible on older compilers, but
if the compiler is upgraded (even if an older framework is being used), the getter will be
usable
readonly fields in same typeWe previously removed init fields from the design proposal because we didn't think it was
necessary for the records feature and because we didn't want to allow fields which were
declared readonly before records to suddenly be settable externally in C# 9, contrary to
the author's intent.
One extension would be to allow readonly fields to be set in an object initializer only inside
the type. In this case you could still use object initializers to set readonly fields in
static factories, but because they would be a part of your type you would always know the intent
of the readonly modifier. For instance,
class C
{
public readonly string? ReadonlyField;
public static C Create()
=> new C() { ReadonlyField = null; };
}
On the other hand, we may not need a new feature for many of these scenarios. An init-only
property with a private init accessor behaves similarly.
class C
{
public string? ReadonlyProp { get; private init; }
public static C Create()
=> new C() { ReadonlyProp = null; };
}
Conclusion
We still think readonly fields are interesting, but we're not sure of the scenarios yet.
Let's keep this on the table, but leave it for a later design meeting.
init methodsWe previously considered having init methods which could modify readonly members just
like init accessors. This could enable scenarios like "immutable collection initializers",
where members can be added via an init Add method.
The problem is that the vast majority of immutable collections in the framework today would be
unable to adopt this pattern. Collection initializers are hardcoded to use the name Add today
and almost all the immutable collections already have Add methods that are meant for a different
purpose -- they return a copy of the collection with the added item.
If we naively extend init to collection initializers most immutable collections wouldn't be able
to adopt them because they couldn't make their Add methods init-only, and no other method name
is allowed in a collection initializer. In addition, some types, like ImmutableArrays, would be
forced to implement init-only collection initializers very inefficiently, by declaring a new array
and copying each item every time Add is called.
Conclusion
We're still very interested in the feature, but we need to determine how we can add it in a way that provides a path forward for our existing collections.
We have an equality implementation that we think is functional, but we're not sure it's the most efficient implementation. Consider the following chain of types:
class R1
{
public override bool Equals(object other)
=> Equals(other as R1);
public virtual bool Equals(R1 other)
=> !(other is null) &&
this.EqualityContract == other.EqualityContract
/* && compare fields */;
}
class R2 : R1
{
public override bool Equals(object other)
=> Equals(other as R2);
public override bool Equals(R1 other)
=> Equals(other as R2);
public virtual bool Equals(R2 other)
=> base.Equals((R1)other)
/* && compare fields */;
}
class R3 : R2
{
public override bool Equals(object other)
=> Equals(other as R3);
public override bool Equals(R1 other)
=> Equals(other as R3);
public override bool Equals(R2 other)
=> Equals(other as R3);
public virtual bool Equals(R2 other)
=> base.Equals((R1)other)
/* && compare fields */;
}
The benefit of the above strategy is that each virtual call goes directly to the appropriate implementation method for the runtime type. The drawback is that we're effectively generating a quadratic number of methods and overrides based on the number of derived records.
One alternative is that we could not override the Equals methods of anything
except our base. This would cause more virtual calls to reach the implementation,
but reduce the number of overrides.
Conclusion
We need to do a deep dive on this issue and explore all the scenarios. We'll come back once we've outlined all the options and come up with a recommendation.
Proposals for copy constructors
- Do not include initializers (including for user-written copy constructors)
- Require delegation to a base copy constructor or `object` constructor
- If the implementation is synthesized, this behavior is synthesized
Proposal for Deconstruct:
- Doesn't delegate to a base Deconstruct method
- Synthesized body is equivalent to a sequence of assignments of member
accesses. If any of these assignments would be an error, an error is produced.
It's also proposed that any members which are either dispatched to in a derived record or expected to be overridden in a derived record will produce an error for synthesized implementations if the required base member is not found. This includes if the base member was not present in the immediate base, but was inherited instead. For some situations this may mean that the user can write a substituted implementation for that synthesized member, but for the copy constructor this effectively forbids record inheritance, since the valid base member must be present even in a user-defined implementation.
Conclusion
All of the above decisions are upheld.
Currently in the framework IEnumerable.Current (the non-generic interface) is annotated to
return object?. This produces a lot of warnings in legacy code that foreach over the result
with types like string, which is non-nullable. We have two proposals to resolve this:
- Un-annotate `IEnumerable.Current`. This will keep the member nullable-oblivious and no warnings
will be generated, even if the property is called directly
- Special-case the compiler behavior for `foreach` on `IEnumerable` to suppress nullable warnings
when calling `IEnumerable.Current`
Conclusion
Overall, we prefer un-annotation. Since this interface is essentially legacy, we feel that providing nullable analysis is potentially harmful and rarely beneficial.