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