C#收益率回报表现

使用yield return语法的方法后面的底层集合保留了多少空间当我在其上执行ToList()时? 如果我创建一个具有预定义容量的列表的标准方法,它有可能重新分配并因此降低性能吗?

这两种情况:

public IEnumerable GetList1() { foreach( var item in collection ) yield return item.Property; } public IEnumerable GetList2() { List outputList = new List( collection.Count() ); foreach( var item in collection ) outputList.Add( item.Property ); return outputList; } 

yield return不会创建一个必须resize的数组,就像List所做的那样; 相反,它使用状态机创建IEnumerable

例如,让我们采用这种方法:

 public static IEnumerable Foo() { Console.WriteLine("Returning 1"); yield return 1; Console.WriteLine("Returning 2"); yield return 2; Console.WriteLine("Returning 3"); yield return 3; } 

现在让我们调用它并将该枚举赋值给变量:

 var elems = Foo(); 

Foo中的代码都没有执行过。 控制台上不会打印任何内容。 但是如果我们迭代它,就像这样:

 foreach(var elem in elems) { Console.WriteLine( "Got " + elem ); } 

foreach循环的第一次迭代中,将执行Foo方法,直到第一次yield return 。 然后,在第二次迭代中,该方法将从它停止的位置“恢复”(在yield return 1 ),并执行直到下一次yield return 。 所有后续元素都相同。
在循环结束时,控制台将如下所示:

 Returning 1 Got 1 Returning 2 Got 2 Returning 3 Got 3 

这意味着您可以编写如下方法:

 public static IEnumerable GetAnswers() { while( true ) { yield return 42; } } 

你可以调用GetAnswers方法,每次你请求一个元素时,它都会给你42; 序列永远不会结束。 您无法使用List执行此操作,因为列表必须具有有限的大小。

使用yield return语法在方法后面为底层集合保留了多少空间?

没有潜在的集合。

有一个对象,但它不是一个集合。 它将占用多少空间取决于它需要跟踪的内容。

它有可能重新分配

没有。

如果与我创建具有预定义容量的列表的标准方法相比,从而降低性能?

与创建具有预定义容量的列表相比,它几乎肯定会占用更少的内存。

我们来试试一个手动的例子。 假设我们有以下代码:

 public static IEnumerable CountToTen() { for(var i = 1; i != 11; ++i) yield return i; } 

通过这种方式,将遍历数字110包括110

现在让我们按照yield不存在的方式做到这一点。 我们做的事情如下:

 private class CountToTenEnumerator : IEnumerator { private int _current; public int Current { get { if(_current == 0) throw new InvalidOperationException(); return _current; } } object IEnumerator.Current { get { return Current; } } public bool MoveNext() { if(_current == 10) return false; _current++; return true; } public void Reset() { throw new NotSupportedException(); // We *could* just set _current back, but the object produced by // yield won't do that, so we'll match that. } public void Dispose() { } } private class CountToTenEnumerable : IEnumerable { public IEnumerator GetEnumerator() { return new CountToTenEnumerator(); } IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); } } public static IEnumerable CountToTen() { return new CountToTenEnumerable(); } 

现在,由于各种原因,这与使用yield可能从版本中获得的代码完全不同,但基本原理是相同的。 正如您所看到的,对象涉及两个分配(相同的数字就好像我们有一个集合,然后foreach做了一个foreach )和一个int的存储。 在实践中,我们可以期望yield存储比这更多的字节,但不是很多。

现在让我们来看看:

 public IEnumerable GetList1() { foreach( var item in collection ) yield return item.Property; } 

虽然这会导致使用更多内存而不仅仅是return collection ,但它不会导致更多内存; 枚举器生成的唯一真正需要跟踪的是通过在collection上调用GetEnumerator()然后包装它来生成的枚举器。

与你提到的浪费的第二种方法相比,这将大大减少内存,并且要快得多。

编辑:

你已经改变了你的问题,包括“我在其上执行ToList()时的语法”,值得考虑。

现在,我们需要增加第三种可能性:了解集合的大小。

在这里,有可能使用new List(capacity)将阻止正在构建的列表的分配。 这确实可以节省很多。

如果在其上调用ToList的对象实现了ICollection那么ToList将首先完成对T的内部数组的单个分配,然后调用ICollection.CopyTo()

这意味着您的GetList2将导致比GetList1更快的ToList()

但是,你的GetList2已经浪费了时间和内存来做ToList()无论如何都会对GetList1的结果做什么!

这里应该做的只是return new List(collection); 并完成它。

如果我们需要在GetList1GetList2实际执行某些GetList2 (例如转换元素,过滤元素,跟踪平均值等),那么GetList1将会更快更轻。 如果我们从不调用ToList()那么轻,如果我们调用ToList()稍微更轻,因为再次,更快更轻的ToList()GetList2在第一个位置变得更慢和更重而完全相同。