dotnet / csharplang

The official repo for the design of the C# programming language
11.56k stars 1.03k forks source link

[Proposal]: Extend indexers to object initializers #3882

Open Youssef1313 opened 4 years ago

Youssef1313 commented 4 years ago

Indexers in collection initializers (Inspired from https://github.com/dotnet/roslyn/issues/47632)

Summary

An extension to the existing collection initializers syntax.

Motivation

Indexers provide a more readable syntax to access elements based on their position from the end ( [^1]: last element, [^2]: element before last, etc).

However, indexers doesn't work with collection initializers currently. For example the following is allowed:

using System.Collections.Generic;

static void CloneAndChangeFirst(List<string> source)
{
    List<string> items = new List<string>(source)
    {
        [0] = "FirstIsChanged"
    };

}

But the following is not:

using System.Collections.Generic;

static void CloneAndChangeLast(List<string> source)
{
    List<string> items = new List<string>(source)
    {
        [^1] = "LastIsChanged"
    };

}

Detailed design

The same way the current indexers are designed based on System.Index.

Drawbacks

Currently, there is no way to set the last element in a collection initializer, even without using indexers.

The following code fails with CS0165:

using System.Collections.Generic;

static void CloneAndChangeLast(List<string> source)
{
    List<string> items = new List<string>(source)
    {
        [items.Count - 1] = "LastIsChanged" //     error CS0165: Use of unassigned local variable 'items'
    };

}

I don't know the reason for the current limitation (possibly false positive?). But I believe this should be worked on first.

Alternatives

Unresolved questions

Design meetings

HaloFour commented 4 years ago

C# does currently support index initializers using System.Index if the type has a settable indexer that accepts System.Index. What C# doesn't currently do is to translate the System.Index into an int when the type doesn't have such an indexer. So what you're proposing would be that:

var items = new List<string>(source)
{
    [^1] = "LastIsChanged"
};

be compiled into:

var $temp = new List<string>(source);
var index = $temp.Count - 1;
$temp[index] = "LastIsChanged";
var list = $temp;
Youssef1313 commented 4 years ago

So what you're proposing would be that:

var items = new List<string>(source)
{
    [^1] = "LastIsChanged"
};

be compiled into:

var $temp = new List<string>(source);
var index = $temp.Count - 1;
$temp[index] = "LastIsChanged";
var list = $temp;

This is exactly what I'm proposing. But I'm not getting the first part of your comment (probably due to lack of understanding of how things works together internally).

HaloFour commented 4 years ago

Here's an example, shows that an indexer initializer will work if there is a compatible this property:

SharpLab

The problem is that List<T> doesn't actually have an indexer method that accepts System.Index as a parameter, so C# fakes it, but it doesn't do this with initializers today.

Youssef1313 commented 4 years ago

@HaloFour Got it!

But was there a reason that the runtime didn't define this indexer instead of the compiler being faking it? The proposal only makes sense if there is a reason for the runtime not to define this indexer for certain types including List<T>.

theunrepentantgeek commented 4 years ago

I believe it comes down to backward compatibility.

Modifying List<T> to treat negative indexes as indexing from the end would have broken any code relying on an exception to terminate a loop.

So they introduced an index type.

But modifying List<T> to add another indexer would also break back compatibility, both with overload choice and potentially reflection.

If you don't care about breaking existing code, many changes are possible - but you break trust with your developer community.

Youssef1313 commented 4 years ago

Then I think it just makes sense to "fake" things to work in initializers as well.

huoyaoyuan commented 4 years ago

Currently, collection initializer works a bit different from dictionary and array ones. It doesn't calculate the desired length for the collection. It just calls Add repeatly. Length related properties of the collection are essentially untouched. (For example, HashSet doesn't guarantee to produce the same length of added items) I'd love this feature, but it should be not easy to design for the corner cases. Bringing this syntax to array should be easier.

Youssef1313 commented 4 years ago

@huoyaoyuan HashSet doesn't in anyway allow indexing. So that case is unrelated.

for example, the following isn't allowed. So, this proposal doesn't apply for it.

using System.Collections.Generic;

var set = new HashSet<int>()
{
    [0] = 5
};

But, the following is allowed, so this proposal applies to it:

using System.Collections.Generic;

var set = new List<int>()
{
    [0] = 5 // Ignoring the fact that this will throw - i.e, assume some collection was passed to the ctor of List<T>.
};
RikkiGibson commented 4 years ago

I think we need to clarify whether this is just a hole in the "pattern index/range" implementation, or whether the current behavior is by-design.

Also, the title should probably be changed to mention "object initializers", not "collection initializers".