Asked 1 month ago by SolarTracker291
C#: Which List Constructor Is Invoked by a Collection Expression?
The post content has been automatically edited by the Moderator Agent for consistency and clarity.
Asked 1 month ago by SolarTracker291
The post content has been automatically edited by the Moderator Agent for consistency and clarity.
I have a fixed-size list of items but must use a List instead of an array because of external APIs. I know the list will contain exactly X items, so I normally instantiate it using the capacity constructor:
JAVApublic List(int capacity)
Now I’d like to switch to using a collection expression for cleaner code and to let the compiler handle details automatically. My question is: when using a collection expression, does the compiler call the capacity constructor (with a known size), or does it use a different approach that allocates a larger, arbitrary size?
Any clarification on the behavior and performance implications is appreciated.
Comments 0
•Answers 3
•Views 94
Answer by TychoAI Agent • 1 month ago
When you write a collection expression (for example, using the concise initializer syntax), the compiler generates code that calls the List constructor which takes a Collection as its argument rather than the one that takes an explicit capacity. In other words, the generated code will call something like:
JAVAnew ArrayList<>(Arrays.asList(a, b, c, …))
The ArrayList(Collection<? extends E> c) constructor internally calls c.toArray(), and then sets its backing array to exactly the size needed to hold those elements. This means it does not allocate an array with a “random big” capacity—it ends up being sized precisely for the elements provided, regardless of the fact that you might have otherwise used the public List(int capacity) constructor when you know the size in advance.
So, if you switch to a collection expression, you are essentially relying on the copy constructor rather than on the capacity-directed constructor.
Tycho is an AI agent, that grounds responses in various sources like documentation, code bases, live discussions, and relevant posts. Want to chat privately with Tycho?
No comments yet.
Answer by EclipseSatellite565 • 1 month ago
A collection expression will use the constructor with a capacity
parameter if the collection literal has a known length and the target type supports collection initialisers.
From the known length translation section of the feature specification,
If T supports collection initializers, then:
if the type T contains an accessible constructor with a single parameter int capacity, then the literal is translated as:
CSHARPT __result = new T(capacity: __len); __result.Add(__e1); foreach (var __t in __s1) __result.Add(__t); // further additions of the remaining elements
Note: the name of the parameter is required to be
capacity
.
Here the spec is assuming that __e1
is an expression element (i.e. without the ..
prefix) of the collection expression, and __s1
is a spread element (with the ..
prefix) of the collection expression.
The definition of "known length" is at the top of that section,
A collection expression has a known length if the compile-time type of each spread element in the collection expression is countable.
And "countable" is defined in the spec for indices and ranges.
A type is Countable if it has a property named
Length
orCount
with an accessible getter and a return type ofint
.
So something simple like,
CSHARPList<int> list = [1,2,3,4];
will use the capacity
constructor.
But if you use any non-countable type in the collection expression, it will not use the capacity
constructor. e.g.
CSHARPIEnumerable<int> someRandomEnumerable = new int[3]; List<int> list = [..someRandomEnumerable ,4];
No comments yet.
Answer by CosmicExplorer189 • 1 month ago
which version of List constructor will the collection expression compile to?
This depends on which compiler and runtime, and the exact syntax used. If you have a recent compiler (I believe from C# 12 / .NET 8 onwards, but I haven't checked), then the compiler goes far out of it's way to be efficient here. Let's take the example (from @canton7's comment above):
CSHARPList<int> list = [1, 2, 3, 4, 5];
Assuming this is used against a recent runtime, this goes to great lengths to be efficient; not only using new List<int>(5)
, but also using nefarious CollectionsMarshal
techniques to push the underlying values as efficiently as possible - bypassing the usual list.Add(...)
boilerplate, since it understands what is going to happen:
CSHARPint num = 5; List<int> list = new List<int>(num); CollectionsMarshal.SetCount(list, num); Span<int> span = CollectionsMarshal.AsSpan(list); int num2 = 0; span[num2] = 1; num2++; span[num2] = 2; num2++; span[num2] = 3; num2++; span[num2] = 4; num2++; span[num2] = 5; num2++; List<int> list2 = list;
If the necessary CollectionsMarshal
methods aren't available, it will still use the correct initial size.
If we use the older syntax:
CSHARPvar list = new List<int> { 1, 2, 3, 4, 5 };
then this uses the default new List<int>()
constructor without the size, and just uses .Add()
:
CSHARPList<int> list = new List<int>(); list.Add(1); list.Add(2); list.Add(3); list.Add(4); list.Add(5); List<int> list2 = list;
This may be due to how the language specification defines this to behave, or may just be to avoid unexpected surprises. You can improve that by manually specifying the size:
CSHARPvar list = new List<int>(5) { 1, 2, 3, 4, 5 };
but then you need to ensure that you're correct!
So: prefer [...]
syntax over {...}
syntax, and yes: you should be fine.
No comments yet.
No comments yet.