当我对方法执行 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;
}
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;
}
现在让我们调用它并将该可枚举分配给一个变量:
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<int> GetAnswers()
{
while( true )
{
yield return 42;
}
}
你可以调用GetAnswers
方法,每次你请求一个元素时,它都会给你 42;序列永远不会结束。你不能用List
来做到这一点,因为列表必须有一个有限的大小。
使用收益返回语法的方法后面的基础集合保留了多少空间?
没有基础集合。
有一个对象,但它不是一个集合。它将占用多少空间取决于它需要跟踪的内容。
它有可能重新分配
不。
因此,与我创建具有预定义容量的列表的标准方法相比,性能会降低?
与创建具有预定义容量的列表相比,它几乎肯定会占用更少的内存。
让我们尝试一个手动示例。假设我们有以下代码:
public static IEnumerable<int> CountToTen()
{
for(var i = 1; i != 11; ++i)
yield return i;
}
要foreach
,将遍历数字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();
}
现在,由于各种原因,这与您可能从使用yield
的版本获得的代码完全不同,但基本原理是相同的。如您所见,涉及两个对象的分配(与我们拥有集合然后对其进行foreach
的数量相同)和单个 int 的存储。实际上,我们可以期望yield
存储比这更多的字节,但不是很多。
编辑:yield
实际上做了一个技巧,即在获取对象的同一线程上的第一个GetEnumerator()
调用返回相同的对象,为这两种情况提供双重服务。由于这涵盖了超过 99% 的用例yield
因此实际上执行一个分配而不是两个分配。
现在让我们看看:
public IEnumerable<T> GetList1()
{
foreach( var item in collection )
yield return item.Property;
}
虽然这会导致使用的内存不仅仅是return collection
,但它不会产生更多;枚举器生成的唯一真正需要跟踪的是调用GetEnumerator()
collection
然后包装它生成的枚举器。
这将比你提到的浪费的第二种方法的内存少得多,而且开始的速度要快得多。
编辑:
您已经将问题更改为包含"当我对其执行 ToList() 时语法",这值得考虑。
现在,在这里我们需要添加第三种可能性:了解集合的大小。
在这里,使用new List(capacity)
可能会阻止正在构建的列表的分配。这确实可以节省大量资金。
如果ToList
调用的对象实现了ICollection<T>
那么ToList
最终将首先对内部T
数组进行单次分配,然后调用ICollection<T>.CopyTo()
。
这意味着您的GetList2
将导致比GetList1
更快的ToList()
。
但是,您的GetList2
已经浪费了时间和内存,无论如何ToList()
都会对GetList1
的结果进行处理!
它应该在这里做的只是return new List<T>(collection);
并完成它。
如果我们需要在GetList1
或GetList2
内部实际做一些事情(例如转换元素、过滤元素、跟踪平均值等),那么GetList1
内存会更快、更轻。如果我们从不打电话给它ToList()
,就会轻得多,如果我们打电话给ToList()
,就会稍微轻一点,因为同样,更快、更轻的ToList()
被GetList2
首先是完全相同的更慢和更重的量所抵消。