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

问题描述

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

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

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

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<> 的每个元素?
    (请不要使用 Linq 的 Select() 方法发布任何答案,因为它正在创建集合的副本)

解决方法

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

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

没有。 Java 会这样做,在运行时擦除所有类型 T,并将所有类型重定向到单个 List<Object> 实现(称为 type erasure)。在 .NET 中,对于值类型,每个 List<ValueType1>List<ValueType2>List<ValueType3> 在运行时都会收到一个不同的实现(请参阅 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

如果转换成功,则将结果值压入堆栈。

请注意,即使 取出 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> 是构建在数组的“顶部”)。即使在 {{ 上也可以很容易地看到这一点3}}。