RError.com

RError.com Logo RError.com Logo

RError.com Navigation

  • 主页

Mobile menu

Close
  • 主页
  • 系统&网络
    • 热门问题
    • 最新问题
    • 标签
  • Ubuntu
    • 热门问题
    • 最新问题
    • 标签
  • 帮助
主页 / 问题 / 737194
Accepted
Андрей NOP
Андрей NOP
Asked:2020-10-29 02:41:01 +0000 UTC2020-10-29 02:41:01 +0000 UTC 2020-10-29 02:41:01 +0000 UTC

使用 IEnumerable 时打包 ValueType

  • 772

假设我们有一个数组,例如:

int[,] array = { { 1, 2, 3 }, { 4, 5, 6 } };

所有的数组都是IEnumerable(非泛型的)实现,所以在使用这个接口的时候,所有的元素都会被装箱?

这个问题是相关的,例如,在使用Linq-operationsCast<T>()或时OfType<T>():

Console.WriteLine(string.Join(" " , array.Cast<int>()));
c#
  • 2 2 个回答
  • 10 Views

2 个回答

  • Voted
  1. Best Answer
    Sergey Teplyakov
    2020-10-29T09:00:25Z2020-10-29T09:00:25Z

    是的,多维数组的情况相当可悲。这样的数组实现IEnumerable但不实现IEnumerable<T>。而这意味着任何通过“棱镜”使用多维数组IEnumerable都会导致对每个元素的封装,使用方法Enumerable.Cast<T>也不例外。

    这是一个简单的基准测试(基于BenchmarkDotNet),表明情况确实如此:

    [MemoryDiagnoser]
    public class MultidimentionalAarrayTests
    {
        private int[,] m_multiArray = {{1, 2}, {3, 4}};
        private int[] m_regularArray = {1, 2, 3, 4};
    
        [Benchmark]
        public int MultiArrayLast()
        {
            return m_multiArray.Cast<int>().Last();
        }
    
        [Benchmark]
        public int RegularArrayLast()
        {
            return m_regularArray.Last();
        }
    }
    

    结果:

                         Method |        Mean |     Error |    StdDev |  Gen 0 | Allocated |
    --------------------------- |------------:|----------:|----------:|-------:|----------:|
                 MultiArrayLast | 1,166.97 ns | 23.229 ns | 51.473 ns | 0.0401 |     132 B |
               RegularArrayLast |    51.29 ns |  1.250 ns |  3.686 ns |      - |       0 B |
    

    这里我们看到一堆分配:在第一种情况下,每个元素都被打包,迭代器 in Cast<T>,迭代器 in Last<T>。在第二种情况下,根本没有分配,因为它Last<T>检查序列是否实现IList<T>(以及一维数组实现它)并立即返回最后一个元素。

    由于多维数组没有实现 generic IEnumerable<T>,我们不能强迫它自己做,但是我们可以创建一个扩展方法来避免使用Enumerable.Cast<T>:

    public static class MultiDimentionalArrayEx
    {
        public static IEnumerable<T> AsEnumerable<T>(this T[,] array)
        {
            foreach (var e in array) yield return e;
        }
    }
    

    现在我们可以添加另一个基准来检查结果:

    [Benchmark]
    public int MultiArrayWithAsEnumerable()
    {
        return m_multiArray.AsEnumerable().Last();
    }
    

    这是最终结果:

                         Method |        Mean |      Error |     StdDev |  Gen 0 | Allocated |
    --------------------------- |------------:|-----------:|-----------:|-------:|----------:|
                 MultiArrayLast | 1,115.45 ns | 31.0145 ns | 90.9603 ns | 0.0401 |     132 B |
               RegularArrayLast |    46.11 ns |  0.1826 ns |  0.1525 ns |      - |       0 B |
     MultiArrayWithAsEnumerable |   161.74 ns |  3.2693 ns |  3.2109 ns | 0.0150 |      48 B |
    

    在这里,我们看到堆上分配了两个迭代器(一个用于扩展方法,一个用于Enumerable.Last<T>),但元素本身没有包装。

    • 18
  2. VladD
    2020-10-29T04:12:05Z2020-10-29T04:12:05Z

    当然,使用非通用IEnumerable包装时,您无法避免。

    但是编译器很聪明,在某些情况下可以不用IEnumerable. 一个重要的情况是,被枚举的对象是否具有GetEnumerator带有合适签名的公共方法。在这种情况下,它将被使用。*

    第二个重要的特殊情况(这就是我们所拥有的)是数组。编译器知道如何更有效地遍历数组,并且有时会利用这一点。

    例如,这是一个函数

    static int[,] array = ...;
    static void Test()
    {
        foreach (var val in array)
            Console.WriteLine(val);
    }
    

    编译就好像它是这样写的:

    int[,] array = Program.array;
    int upperBound = array.GetUpperBound(0);
    int upperBound2 = array.GetUpperBound(1);
    for (int i = array.GetLowerBound(0); i <= upperBound; i++)
    {
        for (int j = array.GetLowerBound(1); j <= upperBound2; j++)
        {
            Console.WriteLine(array[i, j]);
        }
    }
    

    对于这种情况Cast<T>,优化器似乎并没有尝试改进数组的代码,而是使用Cast. 该代码Cast<T>检查是否存在类型化变体IEnumerable<T>(在这种情况下将没有包装),但数组不支持它。所以它正在迭代IEnumerable,结果打包。在该语言的未来版本中,优化器可能会变得更智能(如果开发人员认为这种情况很重要)。


    (对于难以置信的,这里是 IL 代码:

    // int[,] array = Program.array;
    IL_0000: ldsfld int32[0..., 0...] Test.Program::'array'
    IL_0005: stloc.0
    
    // int upperBound = array.GetUpperBound(0);
    IL_0006: ldloc.0
    IL_0007: ldc.i4.0
    IL_0008: callvirt instance int32 [mscorlib]System.Array::GetUpperBound(int32)
    IL_000d: stloc.1
    
    // int upperBound2 = array.GetUpperBound(1);
    IL_000e: ldloc.0
    IL_000f: ldc.i4.1
    IL_0010: callvirt instance int32 [mscorlib]System.Array::GetUpperBound(int32)
    IL_0015: stloc.2
    
    // i = array.GetLowerBound(0)
    IL_0016: ldloc.0
    IL_0017: ldc.i4.0
    IL_0018: callvirt instance int32 [mscorlib]System.Array::GetLowerBound(int32)
    IL_001d: stloc.3
    
    IL_001e: br.s IL_0048 // jump to outer loop check
    // loop start (head: IL_0048)
        // j = array.GetLowerBound(1)
        IL_0020: ldloc.0
        IL_0021: ldc.i4.1
        IL_0022: callvirt instance int32 [mscorlib]System.Array::GetLowerBound(int32)
        IL_0027: stloc.s 4
    
        IL_0029: br.s IL_003f // jump to inner loop check
        // loop start (head: IL_003f)
            // array[i, j]
            IL_002b: ldloc.0
            IL_002c: ldloc.3
            IL_002d: ldloc.s 4
            IL_002f: call instance int32 int32[0..., 0...]::Get(int32, int32)
            IL_0034: call void [mscorlib]System.Console::WriteLine(int32)
    
            // j++
            IL_0039: ldloc.s 4
            IL_003b: ldc.i4.1
            IL_003c: add
            IL_003d: stloc.s 4
    
            // j <= upperBound2
            IL_003f: ldloc.s 4
            IL_0041: ldloc.2
            IL_0042: ble.s IL_002b
        // end loop
    
        // i++
        IL_0044: ldloc.3
        IL_0045: ldc.i4.1
        IL_0046: add
        IL_0047: stloc.3
    
        // i <= upperBound
        IL_0048: ldloc.3
        IL_0049: ldloc.1
        IL_004a: ble.s IL_0020
    // end loop
    
    IL_004c: ret
    

    查看!)


    *文档链接:

    • 否则,判断类型 X 是否有合适的GetEnumerator方法:

    只有在那之后

    • 否则,检查可枚举接口:

    这尤其允许在迭代时List<T>不是在接口IEnumerator<T>上而是在结构 List<T>.Enumerator上进行迭代,从而避免包装这个结构。

    • 8

相关问题

Sidebar

Stats

  • 问题 10021
  • Answers 30001
  • 最佳答案 8000
  • 用户 6900
  • 常问
  • 回答
  • Marko Smith

    Python 3.6 - 安装 MySQL (Windows)

    • 1 个回答
  • Marko Smith

    C++ 编写程序“计算单个岛屿”。填充一个二维数组 12x12 0 和 1

    • 2 个回答
  • Marko Smith

    返回指针的函数

    • 1 个回答
  • Marko Smith

    我使用 django 管理面板添加图像,但它没有显示

    • 1 个回答
  • Marko Smith

    这些条目是什么意思,它们的完整等效项是什么样的

    • 2 个回答
  • Marko Smith

    浏览器仍然缓存文件数据

    • 1 个回答
  • Marko Smith

    在 Excel VBA 中激活工作表的问题

    • 3 个回答
  • Marko Smith

    为什么内置类型中包含复数而小数不包含?

    • 2 个回答
  • Marko Smith

    获得唯一途径

    • 3 个回答
  • Marko Smith

    告诉我一个像幻灯片一样创建滚动的库

    • 1 个回答
  • Martin Hope
    Air 究竟是什么标识了网站访问者? 2020-11-03 15:49:20 +0000 UTC
  • Martin Hope
    Алексей Шиманский 如何以及通过什么方式来查找 Javascript 代码中的错误? 2020-08-03 00:21:37 +0000 UTC
  • Martin Hope
    Qwertiy 号码显示 9223372036854775807 2020-07-11 18:16:49 +0000 UTC
  • Martin Hope
    user216109 如何为黑客设下陷阱,或充分击退攻击? 2020-05-10 02:22:52 +0000 UTC
  • Martin Hope
    Qwertiy 并变成3个无穷大 2020-11-06 07:15:57 +0000 UTC
  • Martin Hope
    koks_rs 什么是样板代码? 2020-10-27 15:43:19 +0000 UTC
  • Martin Hope
    user207618 Codegolf——组合选择算法的实现 2020-10-23 18:46:29 +0000 UTC
  • Martin Hope
    Sirop4ik 向 git 提交发布的正确方法是什么? 2020-10-05 00:02:00 +0000 UTC
  • Martin Hope
    faoxis 为什么在这么多示例中函数都称为 foo? 2020-08-15 04:42:49 +0000 UTC
  • Martin Hope
    Pavel Mayorov 如何从事件或回调函数中返回值?或者至少等他们完成。 2020-08-11 16:49:28 +0000 UTC

热门标签

javascript python java php c# c++ html android jquery mysql

Explore

  • 主页
  • 问题
    • 热门问题
    • 最新问题
  • 标签
  • 帮助

Footer

RError.com

关于我们

  • 关于我们
  • 联系我们

Legal Stuff

  • Privacy Policy

帮助

© 2023 RError.com All Rights Reserve   沪ICP备12040472号-5