csharplang/meetings/2020/LDM-2020-06-15.md
2020-06-18 16:23:32 -07:00

6.9 KiB

C# Language Design Meeting for June 15, 2020

Agenda

  1. modreq for init accessors

  2. Initializing readonly fields in same type

  3. init methods

  4. Equality dispatch

  5. Confirming some previous design decisions

  6. 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.