假设我们有一个数组,例如:
int[,] array = { { 1, 2, 3 }, { 4, 5, 6 } };
所有的数组都是IEnumerable
(非泛型的)实现,所以在使用这个接口的时候,所有的元素都会被装箱?
这个问题是相关的,例如,在使用Linq
-operationsCast<T>()
或时OfType<T>()
:
Console.WriteLine(string.Join(" " , array.Cast<int>()));
假设我们有一个数组,例如:
int[,] array = { { 1, 2, 3 }, { 4, 5, 6 } };
所有的数组都是IEnumerable
(非泛型的)实现,所以在使用这个接口的时候,所有的元素都会被装箱?
这个问题是相关的,例如,在使用Linq
-operationsCast<T>()
或时OfType<T>()
:
Console.WriteLine(string.Join(" " , array.Cast<int>()));
是的,多维数组的情况相当可悲。这样的数组实现
IEnumerable
但不实现IEnumerable<T>
。而这意味着任何通过“棱镜”使用多维数组IEnumerable
都会导致对每个元素的封装,使用方法Enumerable.Cast<T>
也不例外。这是一个简单的基准测试(基于BenchmarkDotNet),表明情况确实如此:
结果:
这里我们看到一堆分配:在第一种情况下,每个元素都被打包,迭代器 in
Cast<T>
,迭代器 inLast<T>
。在第二种情况下,根本没有分配,因为它Last<T>
检查序列是否实现IList<T>
(以及一维数组实现它)并立即返回最后一个元素。由于多维数组没有实现 generic
IEnumerable<T>
,我们不能强迫它自己做,但是我们可以创建一个扩展方法来避免使用Enumerable.Cast<T>
:现在我们可以添加另一个基准来检查结果:
这是最终结果:
在这里,我们看到堆上分配了两个迭代器(一个用于扩展方法,一个用于
Enumerable.Last<T>
),但元素本身没有包装。当然,使用非通用
IEnumerable
包装时,您无法避免。但是编译器很聪明,在某些情况下可以不用
IEnumerable
. 一个重要的情况是,被枚举的对象是否具有GetEnumerator
带有合适签名的公共方法。在这种情况下,它将被使用。*第二个重要的特殊情况(这就是我们所拥有的)是数组。编译器知道如何更有效地遍历数组,并且有时会利用这一点。
例如,这是一个函数
编译就好像它是这样写的:
对于这种情况
Cast<T>
,优化器似乎并没有尝试改进数组的代码,而是使用Cast
. 该代码Cast<T>
检查是否存在类型化变体IEnumerable<T>
(在这种情况下将没有包装),但数组不支持它。所以它正在迭代IEnumerable
,结果打包。在该语言的未来版本中,优化器可能会变得更智能(如果开发人员认为这种情况很重要)。(对于难以置信的,这里是 IL 代码:
查看!)
*文档链接:
只有在那之后
这尤其允许在迭代时
List<T>
不是在接口IEnumerator<T>
上而是在结构List<T>.Enumerator
上进行迭代,从而避免包装这个结构。