C#收益率回报表现

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,因为列表必须有一个有限的大小.


Jon*_*nna 7

为使用 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 将遍历数字110包含在内。

现在让我们按照如果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