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; }
通过这种方式,将遍历数字1
到10
包括1
和10
。
现在让我们按照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
。
这意味着您的GetList2
将导致比GetList1
更快的ToList()
。
但是,你的GetList2
已经浪费了时间和内存来做ToList()
无论如何都会对GetList1
的结果做什么!
这里应该做的只是return new List
并完成它。
如果我们需要在GetList1
或GetList2
实际执行某些GetList2
(例如转换元素,过滤元素,跟踪平均值等),那么GetList1
将会更快更轻。 如果我们从不调用ToList()
那么轻,如果我们调用ToList()
稍微更轻,因为再次,更快更轻的ToList()
被GetList2
在第一个位置变得更慢和更重而完全相同。