使用 yield return 语法的方法背后没有底层集合。有一个对象,但它不是集合。它需要占用多少空间取决于它需要跟踪的内容。不会重新分配,与创建具有预定义容量的列表相比,它几乎肯定占用更少的内存。举个手动的例子,假设我们有以下代码:
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();
}
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
更多的内存,但它不会增加很多;枚举器需要跟踪的唯一内容是由在
collection
上调用
GetEnumerator()
并将其包装而生成的枚举器。
这比您提到的浪费第二种方法要少得多,而且启动速度快得多。
编辑:
您已将问题更改为包括"执行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
完全抵消。
yield return
生成一个枚举器 - 没有存储。 - ChrisList<T>
的构造函数被优化为接收ICollection<T>
。尽管如此,该优化所做的区别与GetList2
故意比GetList1
更慢、更占用内存的悲观化完全相同,因此它是平衡的。 - Jon Hanna