List<T> 的每个元素都装箱了吗?

Is each element of a List<T> boxed?

装箱的 T 只能拆箱到 T。例如,这不起作用:

object o1 = 5;
double n3 = (double)o1;

List<T> 上使用 Linq 的 Cast() 方法会引发类似的异常:

List<int> _numbers = new List<int>() { 1, 2, 3, 4 };
// this is also not working
IEnumerable<double> nums = _numbers.Cast<double>();
  1. 这是否意味着 List<T> 的每个元素都被装箱了?
  2. 是否可以在不创建列表副本的情况下转换 List<> 的每个元素?
    (请不要 post 使用 Linq 的 Select() 方法进行任何回答,因为它正在创建集合的副本)

我们这里只讨论值类型。引用类型遵循不同的规则。

  1. 这是否意味着 List<T> 的每个元素都被装箱了?

没有。 Java 做到了,在运行时擦除所有类型 T 并将所有类型重定向到单个 List<Object> 实现(称为 type erasure). In .NET, for value types, every List<ValueType1>, List<ValueType2>, List<ValueType3> receives a distinct implementation at runtime (see here)。

  1. 是否可以在不创建列表副本的情况下转换 List<> 的每个元素?

当您将值类型转换为其他内容时,您正在复制它。通常在堆栈上,但副本存在。您不能简单地将值类型作为不同的值类型进行访问。

For example:

public static void M1(long n) {
    M2((int)n);
}

public static void M2(int n) {
}

在 IL asm 中被翻译成

IL_0000: ldarg.0
IL_0001: conv.i4
IL_0002: call void C::M2(int32)
IL_0007: ret

其中 conv.i4:

If the conversion is successful, the resulting value is pushed onto the stack.

请注意,即使 退出 List<int> 的一个元素也会导致创建一个副本:您没有访问 List<int> 中的元素:

public void M1(List<int> a) {
    int v = a[0];
    v = 6; // this won't modify a[0]
}

使用数组(以及较新的 C#),您可以直接访问数组的元素:

public void M1(int[] a) {
    ref int r = ref a[0];
    r = 6; // this will modify a[0]
}

这里的区别在于List<T>的索引器(当你做一个var foo = list[5]时调用的东西是一个方法,而索引器数组的是 IL 指令(数组定义在 .NET 虚拟机的最低级别,并且有特殊的 OP 来处理它们,List<T> 构建在数组的“顶部”)。即使这样也可以在 SharpLab.

上很容易看到