6.9 KiB
C# Language Design Meeting for June 15, 2020
Agenda
-
modreq
for init accessors -
Initializing
readonly
fields in same type -
init
methods -
Equality dispatch
-
Confirming some previous design decisions
-
IEnumerable.Current
Discussion
modreq
for init accessors
We'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
Initializing readonly
fields in same type
We 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
methods
We 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.
Equality dispatch
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.
Affirming some previous decisions
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.
Non-generic IEnumerable
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.