csharplang/meetings/2019/LDM-2019-04-01.md
2019-04-03 23:15:29 -07:00

5 KiB

C# Language Design Notes for April 1st, 2019

Agenda

  1. Pattern-based Index/Range translation

  2. Default interface implementations: Is object.MemberwiseClone() accessible in an interface?

Discussion

Index and Range patterns

The current design for Index has a high implementation overhead for types that currently have int indexers and all of the implementations sum to delegation to the int indexer. Also, full support can only be added by type authors because there are no "extension indexers" in the language. There's also a small overhead for using the Index type, as opposed to doing a "direct" translation into the int indexer.

Range is similar to the previous.

Proposal: contextual conversions for Index and Range members

In short, there would be a new contextual language-defined conversion from Index to int for any instance member where the receiver is "indexable".

One significant limitation is the conversion could be more applicable than expected, meaning that a member could be called with an Index that takes an int, but the member does not intend to be called with an Index, e.g. a member that takes a length, not a index, could be called with a System.Index type due to the containing type being "indexable".

The "contextual" conversion also seems very complicated, especially given that C# is already very complicated.

*Q: Why only support extensions only up to the type already being "indexable"? Could we add support for an extension Count() or similar?

A: Adding general collection functionality is broader than just a different form of indexing. That seems like an instance of a more general problem in C#. There are other proposals, like roles, that provide the general purpose extensibility needed for the above.

Q: What if Count is implemented slowly or improperly?

A: This is a violation of the framework design guidelines. Count should be cheap and O(1). We're willing to double down on this guideline.

Range

Note: the Slice method is preferred to the Range indexer if the Slice is present.

This does violate the general principle we have that exact type matches are always preferred over anything else and it makes adding a Slice method a breaking library change, since it will be preferred over the Index indexer. It would also allow a library to add an extension Slice to override the type's implementation, which is generally not desired.

The general argument of recognizing Slice seems useful and allows people to add extension support for slicing, which we like.

Note: multi-dimensional arrays are not supported for either

Alternative Proposal

We like the general approach of the prior proposal, but think adding contextual conversions is a step too far. Contextual conversions would be both difficult for the implementation and for the user to understand.

For Index, if a type is "indexable" (has a Count or Length, and an indexer that takes an int) then a synthetic indexer will be added to the type that takes an Index and implements a translation to the indexer.

For Range, we do the same thing, but for "rangeable types". A type is "rangeable" if it has a Count or a Length, and a Slice(int,int) method.

Q: Is the 'indexable' pattern based on the constructed type or the original definition?

Unknown, revisit later.

Q: Do we want to support Slice(Range) or Slice(int)?

Probably not -- not worth the complexity.

Note: for the counter-proposal, the length should only be evaluated once, and after the arguments are evaluated.

Warning or error for writing your own Range indexer?

Conclusion

We think the first proposal went too far, but think the alternative is workable. Let's try to flesh out more of the details of that proposal and then consider it again for inclusion in C# 8.

Revisit object.MemberwiseClone()

There are two things we'd like to consider: potential uses for the feature and consistency with the rest of the language rules.

Example of use case:

interface IPoint
{
    public int X { get; protected set;}
    public int Y { get; protected set;}

    public IPoint WithX(int x)
    {
        var tmp = MemberwiseClone();
        tmp.X = x;
        return tmp;
    }
}

One argument also levelled against the feature is that it may be unexpected that an interface could call MemberwiseClone(). A counter argument is that it may be unexpected if a base class calls MemberwiseClone, but that is the current behavior in classes.

On the other hand, many people may have an impression that interfaces and class are essentially different type hierarchies and they meet at the implementation. There is also another protected member, the destructor, which is on object and is questionable to provide to the interface itself.

Conclusion

The only thing we strongly agree upon is that many of the members of object should not have been there in the first place. Beyond that, we aren't particularly swayed by either the advantages of having the access or the dangers of misuse. We'll slightly prefer our earlier decision: protected members of object are not accessible in interfaces.