yield关键字是c#中一直困扰我的关键字之一,我从来都不确定自己是否正确地使用了它。
在以下两段代码中,哪一段是首选的,为什么?
版本1:使用收益率
public static IEnumerable<Product> GetAllProducts()
{
using (AdventureWorksEntities db = new AdventureWorksEntities())
{
var products = from product in db.Product
select product;
foreach (Product product in products)
{
yield return product;
}
}
}
版本2:返回列表
public static IEnumerable<Product> GetAllProducts()
{
using (AdventureWorksEntities db = new AdventureWorksEntities())
{
var products = from product in db.Product
select product;
return products.ToList<Product>();
}
}
作为理解何时应该使用yield的概念性示例,假设方法ConsumeLoop()处理ProduceList()返回/产生的项:
void ConsumeLoop() {
foreach (Consumable item in ProduceList()) // might have to wait here
item.Consume();
}
IEnumerable<Consumable> ProduceList() {
while (KeepProducing())
yield return ProduceExpensiveConsumable(); // expensive
}
如果没有yield,对ProduceList()的调用可能需要很长时间,因为你必须在返回之前完成列表:
//pseudo-assembly
Produce consumable[0] // expensive operation, e.g. disk I/O
Produce consumable[1] // waiting...
Produce consumable[2] // waiting...
Produce consumable[3] // completed the consumable list
Consume consumable[0] // start consuming
Consume consumable[1]
Consume consumable[2]
Consume consumable[3]
使用yield,它会重新排列,有点交错:
//pseudo-assembly
Produce consumable[0]
Consume consumable[0] // immediately yield & Consume
Produce consumable[1] // ConsumeLoop iterates, requesting next item
Consume consumable[1] // consume next
Produce consumable[2]
Consume consumable[2] // consume next
Produce consumable[3]
Consume consumable[3] // consume next
最后,正如之前许多人已经建议的那样,您应该使用版本2,因为您已经有了完整的列表。
Yield return对于需要遍历数百万个对象的算法来说非常强大。考虑以下示例,您需要计算可能的拼车行程。首先我们生成可能的行程:
static IEnumerable<Trip> CreatePossibleTrips()
{
for (int i = 0; i < 1000000; i++)
{
yield return new Trip
{
Id = i.ToString(),
Driver = new Driver { Id = i.ToString() }
};
}
}
然后迭代每一次旅行:
static void Main(string[] args)
{
foreach (var trip in CreatePossibleTrips())
{
// possible trip is actually calculated only at this point, because of yield
if (IsTripGood(trip))
{
// match good trip
}
}
}
如果您使用List而不是yield,您将需要为内存分配100万个对象(~190mb),而这个简单的示例将花费~1400ms运行。但是,如果使用yield,就不需要将所有这些临时对象都放到内存中,而且算法速度会大大加快:本例只需要大约400ms就可以运行,完全不消耗内存。
考虑到确切的两个代码片段,我认为版本1更好,因为它可以更有效。假设有很多产品,调用者希望将其转换为dto。
var dtos = GetAllProducts().Select(ConvertToDto).ToList();
在版本2中,首先创建一个Product对象列表,然后创建另一个ProductDto对象列表。版本1没有Product对象的列表,只构建了所需的ProductDto对象的列表。
Even without converting, Version 2 has a problem in my opinion: The list is returned as IEnumerable. The caller of GetAllProducts() does not know how expensive the enumeration of the result is. And if the caller needs to iterate more than once, she will probably materialize once by using ToList() (tools like ReSharper also suggest this). Which results in an unnecessary copy of the list already created in GetAllProducts(). So if Version 2 should be used, the return type should be List and not IEnumerable.