[Discussion] Collection initializer support for fixed size collections #3763
Replies: 13 comments 5 replies
-
An alternative way of realising this goal would be to add This approach may better mirror the existing
|
Beta Was this translation helpful? Give feedback.
-
|
Beta Was this translation helpful? Give feedback.
-
That's a very good point. I confess that I missed that restriction because I've only ever used @svick Do you happen to know whether there may be another way of passing a stack allocated list of managed references for this special case? Bearing in mind that the size is known at compile time because the collection initializer is hard coded, and that the limitation is what the CLR can be made to do, not necessarily the confines of existing language features. I think something like the following should work on a conceptual level, demonstrating that a solution is possible with sufficient compiler magic. interface ICollectionInit<T> {
int Length { get; }
T this[int i] { get; }
}
struct CustomArray<T>
{
public init void CollectionInit<TInit>(TInit items) where TInit : ICollectionInit<T> => throw new NotImplementedException();
} Because the initializer sizes are known at compile time, the compiler can generate hidden structs of specific lengths as needed. new CustomArray<string>() { "hello", "world" }; could become something like new CustomArray<string>().CollectionInit(new HiddenMangledCollectionInitializerStructLength2<string>("hello", "world"));
// ...
struct HiddenMangledCollectionInitializerStructLength2<T> : ICollectionInit<T>
{
private readonly T Item0;
private readonly T Item1;
public HiddenMangledCollectionInitializerStructLength2(T item0, T item1) => (this.Item0, this.Item1) = (item0, item1);
public int Length => 2;
public T this[int i] {
get {
switch (i) {
case 0: return this.Item0;
case 1: return this.Item1;
default: throw new IndexOutOfRangeException();
}
}
}
} I think that this demonstrates that it's a solvable problem, even if this isn't a very good solution. This is only a haphazard composition of existing ideas. |
Beta Was this translation helpful? Give feedback.
-
We could also approach the problem from a completely different angle. struct MyCustomArray<T>
{
private T[] buffer;
// Some special construct visible only to collection initializers
public init SetSize(int size) {
this.buffer = new T[size];
}
// Some special construct visible only to collection initializers
// SetSize is guaranteed to have been called first
public init SetValue(int index, T value) {
this.buffer[index] = value;
}
} Where new MyCustomArray<string>() { "hello", "world" }; is transformed into var temp = new MyCustomArray<string>();
temp.SetSize(2);
temp.SetValue(0, "hello");
temp.SetValue(1, "world"); This is much more like the existing incremental build functionality of |
Beta Was this translation helpful? Give feedback.
-
I love this idea, there are loads of examples where The upside of changing the behaviour en masse would be potentially large performance benefits when utilizing fundamental classes such as Lists, and particularly in Concurrent collections. Where code does break (which is practically unlikely), it would be trivial to work around from either end (manually call Once used with records, having an init keyword applied, and your suggestion to add a compile-time check to prevent calling outside of initialisation seems very useful. |
Beta Was this translation helpful? Give feedback.
-
I don't believe that any kind of breaking change is on the table. It simply won't be considered. I agree that if this were a breaking change in somebody's codebase that would probably indicate that it's bad code, but breaking is breaking and they won't do it. It should be possible to make If we introduced a completely new init construct - as is already proposed with final initializers - then it could be added to the existing collections and get the same benefit. It would be okay for that to take precedence. |
Beta Was this translation helpful? Give feedback.
-
I don't disagree that it's unlikely they'll do it, though it is worth noting that breaking changes are introduced in every major release of the compiler. Though the majority are 'bug fixes', that's not always the case. For example, the auto-calling of a Considering one of the core drives for .NET 5 is performance, it may be something they consider if benchmarking shows a big improvement in ASP.NET performance (the favourite love child).
Yeah, this would be a fair compromise, but it's a shame not to 'fix' the collection initialiser who's initial implementation always felt a bit rushed. An alternative may be to allow opt-in behaviour of // Adds the elements of the given collection to the end of this list. If
// required, the capacity of the list is increased to twice the previous
// capacity or the new size, whichever is larger.
//
[CollectionInitializer]
public void AddRange(IEnumerable<T> collection)
=> InsertRange(_size, collection); Giving preference to |
Beta Was this translation helpful? Give feedback.
-
An analyser could recommend adding the Attribute. |
Beta Was this translation helpful? Give feedback.
-
What would you be passing into these I think that even if the |
Beta Was this translation helpful? Give feedback.
-
It's possible the compiler can optimise away a lot of the IEnumerable overhead during collection initialisation, including loop unrolling, preventing the need for boxing or array allocation, there's even a few proposals out there for that. But I don't dispute your point that adding a new construct would be simpler. |
Beta Was this translation helpful? Give feedback.
-
That value parameterised types functionality is not in the language, and I'm not aware of any plans to add it in the immediate future. That would be a major feature itself, so I doubt that it would be quickly added just to support this. It's quite similar to what I proposed above about having the compiler spit out hidden types. If the |
Beta Was this translation helpful? Give feedback.
-
I've changed the title from [Proposal] to [Discussion] as this is about an aim rather than a tight proposal for how to achieve that aim. I'm not sure whether this was a sensible change. I can change it back if people think that's preferable. |
Beta Was this translation helpful? Give feedback.
-
I see it's only about array, not what the title is suggested. I've hoped it was a wider discussion. |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
#3707 (Remaining design work in and around records) talks about adding more initialization functionality. There's mention of expanding functionality around collection initializers.
The intent there is to add initializer support for immutable collections that don't want to expose an
Add
method after they've been created. However, I don't believe that an init-onlyAdd
method is a good solution, because the incremental building model is poorly suited to immutable collections that need a fixed size allocation.As a design goal, it should be possible to support
new MyCustomFixedLengthArray<int>() {1, 2, 3}
. It's not currently possible to do this efficiently, as the constructor does not know how much memory to allocate, because the initializer repeatedly invokes theAdd
method after construction.A workaround using the
Add
method is passing the length to the constructor likenew MyCustomFixedLengthArray<int>(3) {1, 2, 3}
. This is clumsy as information is repeated unnecessarily and it's easy to accidentally have the lengths become out of sync when maintaining the code. The arguments in favour of allowing the omission are the same as the ones for allowing the explicit length omission innew int[] {1, 2, 3}
.I propose that some construct be added to allow collection initialization to pass in all of the items at once, likeinit(ReadOnlySpan<T> items)
.new MyCustomFixedLengthArray<int>() {1, 2, 3}
would be transformed into something likenew MyCustomFixedLengthArray<int>().init(stackalloc[] {1, 2, 3})
.init
would then be able to read theLength
property and allocate memory accordingly.@svick pointed out below that this use of
stackalloc
creates a problem for reference types. Two alternative approaches to achieving this goal are discussed in comments. One option is some compiler magic to achieve something similar tostackalloc
for this case, the other is transforming the initializer into a chain of calls like.SetSize(2); .SetValue(0, "hello"); .SetValue(1, "world");
#3707 talks about potentially adding factory tagging.
There would need to be some kind of restriction on collection initializers with factories, as the collection initializer would have to be prevented from being called multiple times.
A custom array implementation using this approach may look something a little bit like this.
There's a significant real use case for this. The Unity game engine is pushing their experimental new DOTS (Data Oriented Technology Stack) tech at the moment, and they have a restriction on having no managed types. This means they have custom "native collection" types which are structs with manually managed memory, like
NativeArray<T>
. Documentation. They don't support collection initializers at the moment, but could if this change were made. This is becoming a core API in a fairly major C# product, so many people would benefit from this change.The proposal around exact syntax here is vague. I'm focusing on the functionality and leaving the specifics as part of the whole collection of work to be done around #3707, which should all be designed in tandem.
Beta Was this translation helpful? Give feedback.
All reactions