StackOverflowException初始化大型列表

Gaz*_*Gaz 8 .net c#

我正在做以下事情:

public static class DataHelper
{
  public static List<Seller> Sellers = new List<Seller> {
    {new Seller {Name = "Foo", Location = new LatLng {Lat = 12, Lng = 2}, Address = new Address {Line1 = "blah", City = "cokesville"}, Country = "UK"},
    //plus 3500 more Sellers
  };
}
Run Code Online (Sandbox Code Playgroud)

当我从MVC网站内部访问DataHelper.Sellers时,我得到一个StackOverflowException.当我使用Visual Studio进行调试时,堆栈只有六个帧,并且通常没有堆栈溢出的明显迹象(即没有重复的方法调用).

应用程序调用可以像这样简单,以引发异常:

public ActionResult GetSellers()
{
  var sellers = DataHelper.Sellers;
  return Content("done");
}
Run Code Online (Sandbox Code Playgroud)

额外信息:

  • 当我在单元测试中运行相同的代码时,它很好
  • 如果我删除一半的卖家(上半部分或下半部分),它在网络应用程序中没问题,所以对任何特定的卖家来说都不是问题
  • 我已经尝试将卖家更改为房产并在第一次通话时初始化列表 - 没有帮助
  • 我也试过添加一半到一个列表然后一半到另一个并结合2 - 再次没有帮助

我对这个问题的正确答案印象深刻!

And*_*tan 7

这是一个事实,即,有效,您的在线列表初始化为堆栈过大造成的-看到这个非常 相关的问题上在MSDN论坛上,其中的场景就在眼前,在相同的.

.Net中的方法具有堆栈深度和大小.A StackOverflowException不仅是由堆栈上的调用次数引起的,而是由每个方法在堆栈中分配内存的总大小引起的.在这种情况下,您的方法太大了 - 这是由局部变量的数量引起的.

举例来说,请考虑以下代码:

    public class Foo
    {
            public int Bar { get; set;}
    }
    public Foo[] GetInts()
    {
        return new Foo[] { new Foo() { Bar = 1 }, new Foo() { Bar = 2 }, 
          new Foo() { Bar = 3 }, new Foo() { Bar = 4 }, new Foo() { Bar = 5 } };
    }
Run Code Online (Sandbox Code Playgroud)

现在看一下编译时该方法的引入IL(这也是一个发布版本):

.maxstack 4
.locals init (
    [0] class SomeExample/Foo '<>g__initLocal0',
    [1] class SomeExample/Foo '<>g__initLocal1',
    [2] class SomeExample/Foo '<>g__initLocal2',
    [3] class SomeExample/Foo '<>g__initLocal3',
    [4] class SomeExample/Foo '<>g__initLocal4',
    [5] class SomeExample/Foo[] CS$0$0000
)
Run Code Online (Sandbox Code Playgroud)

注意 - 之前的实际位/,即SomeExample取决于定义方法和嵌套类的命名空间和类 - 我必须编辑几次以从我正在编写的一些进行中代码中删除类型名称工作!

为什么所有当地人?由于执行内联初始化的方式.每个对象都是新建的并存储在"隐藏"的本地中,这是必需的,以便可以对每个Foo对象的内联初始化执行属性分配(需要对象实例来生成属性集Bar). 这也证明了内联初始化只是一些C#语法糖.

在你的情况下,正是这些本地人导致堆栈爆炸(至少有几千个仅用于顶级对象 - 但你也有嵌套的初始化器).

C#编译器也可以预先加载每个所需的引用数量到堆栈(为每个属性赋值弹出每个引用)但是然后滥用堆栈,其中本地的使用将表现得更好.

它也可以使用单个本地,因为每个只是写入,然后通过数组索引存储在列表中,本地永远不再需要.这可能是C#团队需要考虑的问题 - 如果他偶然发现这个问题,Eric Lippert可能对此有一些想法.

现在,这个考试也给我们解决此使用当地人为您潜在的路线非常庞大的方法:使用迭代器:

public Foo[] GetInts()
{
    return GetIntsHelper().ToArray();
}

private IEnumerable<Foo> GetIntsHelper()
{
    yield return new Foo() { Bar = 1 };
    yield return new Foo() { Bar = 2 };
    yield return new Foo() { Bar = 3 };
    yield return new Foo() { Bar = 4 };
    yield return new Foo() { Bar = 5 };
}
Run Code Online (Sandbox Code Playgroud)

现在,IL GetInts()现在只是.maxstack 8处于领先地位,而且没有本地人.查看迭代器函数,GetIntsHelper()我们有:

.maxstack 2
.locals init (
    [0] class SomeExample/'<GetIntsHelper>d__5'
)
Run Code Online (Sandbox Code Playgroud)

所以现在我们已经停止在这些方法中使用所有这些本地人......

但 ......

查看SomeExample/'<GetIntsHelper>d__5'由编译器自动生成的类- 我们看到本地人仍然存在 - 他们刚刚被提升到该类的字段:

.field public class SomeExample/Foo '<>g__initLocal0'
.field public class SomeExample/Foo '<>g__initLocal1'
.field public class SomeExample/Foo '<>g__initLocal2'
.field public class SomeExample/Foo '<>g__initLocal3'
.field public class SomeExample/Foo '<>g__initLocal4'
Run Code Online (Sandbox Code Playgroud)

所以问题是 - 如果应用于您的场景,该对象的创建是否也会破坏堆栈?可能不是,因为在内存中它应该像尝试初始化大型数组 - 其中百万元素数组是完全可以接受的(假设在实践中有足够的内存).

所以 - 您可以通过转移到使用每个元素的IEnumerable方法来简单地修复代码yield.

但最佳实践表明,如果您必须对此静态定义 - 请考虑将数据添加到磁盘上的嵌入式资源或文件(XML和Linq to XML可能是一个不错的选择),然后根据需要从那里加载它.

更好 - 把它粘在一个数据库:)