We will speak here only of value types. Reference types follow different rules.
- Does that mean that each element of a
List<T>
is boxed ?
No. Java does it, erases all the types T
at runtime and redirect all the types to a single List<Object>
implementation (it is called type erasure). In .NET, for value types, every List<ValueType1>
, List<ValueType2>
, List<ValueType3>
receives a distinct implementation at runtime (see here).
- Is it possible to cast each element of the
List<>
without creating a copy of the list ?
When you cast a value type to something to else you are making a copy of it. Often on the stack, but the copy exists. You can't simply access a value type as a different value type.
For example:
public static void M1(long n) {
M2((int)n);
}
public static void M2(int n) {
}
is translated in IL asm to
IL_0000: ldarg.0
IL_0001: conv.i4
IL_0002: call void C::M2(int32)
IL_0007: ret
Where conv.i4
:
If the conversion is successful, the resulting value is pushed onto the stack.
Note that even getting out an element of List<int>
causes a copy to be made: you aren't accessing the element INSIDE the List<int>
:
public void M1(List<int> a) {
int v = a[0];
v = 6; // this won't modify a[0]
}
With arrays (and with newer C#) you can access directly the element of an array:
public void M1(int[] a) {
ref int r = ref a[0];
r = 6; // this will modify a[0]
}
The difference here is that the indexer of List<T>
(the things that is called when you make a var foo = list[5]
is a method, while the indexer of an array is an IL instruction (array are defined at the lowest level in the .NET virtual machine, and the have special OPs to handle them, List<T>
are built "on top" of arrays). Even this can be easily seen on SharpLab.