197 lines
6.9 KiB
Markdown
197 lines
6.9 KiB
Markdown
|
|
# C# Language Design Meeting for June 15, 2020
|
|
|
|
## Agenda
|
|
|
|
1. `modreq` for init accessors
|
|
|
|
1. Initializing `readonly` fields in same type
|
|
|
|
1. `init` methods
|
|
|
|
1. Equality dispatch
|
|
|
|
1. Confirming some previous design decisions
|
|
|
|
1. `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,
|
|
|
|
```C#
|
|
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.
|
|
|
|
```C#
|
|
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:
|
|
|
|
```C#
|
|
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.
|