Mau*_*tro 2 c# memory yield return
使用yield return语法的方法后面的底层集合保留了多少空间当我在其上执行ToList()时?如果与我创建具有预定义容量的列表的标准方法相比,它有可能重新分配并因此降低性能?
这两种情况:
public IEnumerable<T> GetList1()
{
foreach( var item in collection )
yield return item.Property;
}
public IEnumerable<T> GetList2()
{
List<T> outputList = new List<T>( collection.Count() );
foreach( var item in collection )
outputList.Add( item.Property );
return outputList;
}
Run Code Online (Sandbox Code Playgroud)
Sol*_*lli 10
yield return不会创建一个必须调整大小的数组,就像List那样; 相反,它创建了IEnumerable一个状态机.
例如,让我们采用这种方法:
public static IEnumerable<int> Foo()
{
Console.WriteLine("Returning 1");
yield return 1;
Console.WriteLine("Returning 2");
yield return 2;
Console.WriteLine("Returning 3");
yield return 3;
}
Run Code Online (Sandbox Code Playgroud)
现在让我们调用它并将可枚举赋值给变量:
var elems = Foo();
Run Code Online (Sandbox Code Playgroud)
Foo尚未执行任何代码.控制台上不会打印任何内容.但是如果我们迭代它,就像这样:
foreach(var elem in elems)
{
Console.WriteLine( "Got " + elem );
}
Run Code Online (Sandbox Code Playgroud)
在foreach循环的第一次迭代中,该Foo方法将执行直到第一次yield return.然后,在第二次迭代中,该方法将从其停止的位置(紧接在其后yield return 1)"恢复" ,并执行直到下一次yield return.所有后续元素都相同.
在循环结束时,控制台将如下所示:
Returning 1
Got 1
Returning 2
Got 2
Returning 3
Got 3
Run Code Online (Sandbox Code Playgroud)
这意味着您可以编写如下方法:
public static IEnumerable<int> GetAnswers()
{
while( true )
{
yield return 42;
}
}
Run Code Online (Sandbox Code Playgroud)
你可以调用这个GetAnswers方法,每次你请求一个元素时,它都会给你42; 序列永远不会结束.你不能用a做List,因为列表必须有一个有限的大小.
为使用 yield return 语法的方法后面的基础集合保留了多少空间?
没有底层集合。
有一个对象,但它不是一个集合。它将占用多少空间取决于它需要跟踪什么。
它有机会重新分配
不。
因此,如果与我创建具有预定义容量的列表的标准方法相比,性能会降低吗?
与创建具有预定义容量的列表相比,它几乎肯定会占用更少的内存。
让我们尝试一个手动示例。假设我们有以下代码:
public static IEnumerable<int> CountToTen()
{
for(var i = 1; i != 11; ++i)
yield return i;
}
Run Code Online (Sandbox Code Playgroud)
To foreachthrough this 将遍历数字1以10包含在内。
现在让我们按照如果yield不存在的方式来做这件事。我们会做这样的事情:
private class CountToTenEnumerator : IEnumerator<int>
{
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<int>
{
public IEnumerator<int> GetEnumerator()
{
return new CountToTenEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
public static IEnumerable<int> CountToTen()
{
return new CountToTenEnumerable();
}
Run Code Online (Sandbox Code Playgroud)
现在,由于各种原因,这与您可能从使用 的版本获得的代码大不相同yield,但基本原理是相同的。正如你所看到的,有两个对象分配(与我们有一个集合然后foreach在它上面做了一个相同的数字)和单个 int 的存储。在实践中,我们可以期望yield存储比这更多的字节,但不会太多。
编辑:yield实际上做了一个技巧,即GetEnumerator()在获得对象的同一线程上的第一次调用返回同一个对象,为这两种情况做双重服务。由于这涵盖了超过 99% 的用例,因此yield实际上只进行一次分配而不是两次分配。
现在让我们看看:
public IEnumerable<T> GetList1()
{
foreach( var item in collection )
yield return item.Property;
}
Run Code Online (Sandbox Code Playgroud)
虽然这会导致使用更多的内存return collection,但不会导致更多;枚举产生的唯一真正需要跟踪的是通过调用产生的枚举GetEnumerator()上collection,然后包裹这一点。
与您提到的浪费的第二种方法相比,这将大大减少内存,并且运行速度要快得多。
编辑:
您已将问题更改为包括“在其上执行 ToList() 时的语法”,这是值得考虑的。
现在,在这里我们需要添加第三种可能性:集合大小的知识。
在这里,使用可能new List(capacity)会阻止正在构建的列表的分配。这确实是一笔可观的节省。
如果ToList调用它的对象实现了,ICollection<T>那么ToList最终将首先对 的内部数组进行一次分配,T然后调用ICollection<T>.CopyTo().
这意味着你的GetList2结果会ToList()比你的GetList1.
然而,你GetList2已经浪费了时间和内存做什么ToList(),GetList1反正结果会怎样!
它应该在这里做的只是return new List<T>(collection);完成它。
如果我们需要在or内部实际做一些事情(例如转换元素、过滤元素、跟踪平均值等),那么内存会更快更轻。如果我们从不调用它会轻得多,如果我们调用它会稍微轻一点,因为再一次,更快和更轻的抵消首先被更慢和更重的量完全相同。GetList1GetList2GetList1ToList()ToList()ToList()GetList2