yield 关键字是 C# 中的 keywords 关键字之一,它一直让我感到困惑,而且我从未确信自己是否正确使用了它。
在以下两段代码中,哪个是首选,为什么?
版本 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
与 IEnumerable<T>
及其类型相关联。它在某种程度上是懒惰的评估
GetAllProducts()
结果的代码允许用户有机会提前取消处理,我认为使用 yield return
是一个很好的案例。
当我计算列表中的下一个项目(甚至是下一组项目)时,我倾向于使用 yield-return。
使用您的第 2 版,您必须在返回之前拥有完整的列表。通过使用yield-return,您实际上只需要在返回之前拥有下一个项目。
除其他外,这有助于将复杂计算的计算成本分散到更大的时间范围内。例如,如果列表连接到 GUI 并且用户从未进入最后一页,则您永远不会计算列表中的最终项目。
另一种优选收益回报的情况是 IEnumerable 表示无限集。考虑素数列表,或无限的随机数列表。您永远无法一次返回完整的 IEnumerable,因此您使用 yield-return 逐步返回列表。
在您的特定示例中,您拥有完整的产品列表,因此我将使用版本 2。
填充临时列表就像下载整个视频,而使用 yield
就像流式传输该视频。
作为理解何时应该使用 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 关键字。我真的从这项技术中受益匪浅。希望这对偶然发现这个问题的其他人有所帮助。
注意:不要认为 yield 关键字仅仅是构建集合的另一种方式。 yield 的强大功能的很大一部分在于,在调用代码迭代下一个值之前,您的方法或属性中的执行会暂停。这是我的例子:
使用 yield 关键字(与 Rob Eisenburg 的 Caliburn.Micro coroutines 实现一起)允许我表达对 Web 服务的异步调用,如下所示:
public IEnumerable<IResult> HandleButtonClick() {
yield return Show.Busy();
var loginCall = new LoginResult(wsClient, Username, Password);
yield return loginCall;
this.IsLoggedIn = loginCall.Success;
yield return Show.NotBusy();
}
这将打开我的 BusyIndicator,在我的 Web 服务上调用 Login 方法,将我的 IsLoggedIn 标志设置为返回值,然后关闭 BusyIndicator。
以下是它的工作原理: IResult 有一个 Execute 方法和一个 Completed 事件。 Caliburn.Micro 从对 HandleButtonClick() 的调用中获取 IEnumerator 并将其传递给 Coroutine.BeginExecute 方法。 BeginExecute 方法开始遍历 IResults。当返回第一个 IResult 时,在 HandleButtonClick() 中暂停执行,BeginExecute() 将事件处理程序附加到 Completed 事件并调用 Execute()。 IResult.Execute() 可以执行同步或异步任务,并在完成时触发 Completed 事件。
LoginResult 看起来像这样:
public LoginResult : IResult {
// Constructor to set private members...
public void Execute(ActionExecutionContext context) {
wsClient.LoginCompleted += (sender, e) => {
this.Success = e.Result;
Completed(this, new ResultCompletionEventArgs());
};
wsClient.Login(username, password);
}
public event EventHandler<ResultCompletionEventArgs> Completed = delegate { };
public bool Success { get; private set; }
}
设置这样的东西并逐步执行以观察正在发生的事情可能会有所帮助。
希望这可以帮助某人!我真的很喜欢探索使用 yield 的不同方式。
yield
。这似乎是模拟 async/await 模式的一种优雅方式(如果今天重写,我假设将使用它而不是 yield
)。自从您回答这个问题以来,随着 C# 的发展,您是否发现这些年来 yield
的这些创造性使用已经产生(不是双关语)收益递减?或者你还在想出像这样的现代化聪明用例吗?如果是这样,您介意为我们分享另一个有趣的场景吗?
对于需要迭代数百万个对象的算法,收益回报可能非常强大。考虑以下示例,您需要计算可能的拼车行程。首先我们生成可能的行程:
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,则不需要将所有这些临时对象放入内存,并且您将获得显着更快的算法速度:此示例运行只需约 400 毫秒,完全没有内存消耗。
yield
通过在内部实现状态机在幕后工作。 Here's an SO answer with 3 detailed MSDN blog posts 非常详细地解释了实现。由 Raymond Chen @ MSFT 撰写
这就是 Chris Sells 对 The C# Programming Language 中这些语句的说明;
我有时会忘记 yield return 与 return 不同,因为 yield return 之后的代码可以执行。比如这里第一次return之后的代码永远无法执行: int F() { return 1;返回 2; // 永远无法执行 } 相比之下,这里第一次yield return之后的代码是可以执行的: IEnumerable
F().Any()
- 这将在尝试仅枚举第一个结果后返回。通常,您不应依赖 IEnumerable yield
来更改程序状态,因为它实际上可能不会被触发
这两段代码实际上是在做两件不同的事情。第一个版本将根据需要拉取成员。第二个版本将在您开始使用它之前将所有结果加载到内存中。
这个问题没有正确或错误的答案。哪个更可取取决于情况。例如,如果您必须完成查询的时间有限,并且您需要对结果做一些半复杂的事情,那么第二个版本可能更可取。但要注意大型结果集,尤其是在 32 位模式下运行此代码时。在执行此方法时,我多次被 OutOfMemory 异常所困扰。
但是要记住的关键是:差异在于效率。因此,您可能应该选择使您的代码更简单的任何一个,并且仅在分析后更改它。
产量有两个很好的用途
它有助于在不创建临时集合的情况下提供自定义迭代。 (加载所有数据并循环)
它有助于进行有状态的迭代。 (流媒体)
下面是一个简单的视频,我制作了完整的演示,以支持上述两点
http://www.youtube.com/watch?v=4fju3xcm21M
假设您的产品 LINQ 类使用类似的产量进行枚举/迭代,第一个版本更有效,因为它每次迭代时只产生一个值。
第二个示例是使用 ToList() 方法将枚举器/迭代器转换为列表。这意味着它手动迭代枚举器中的所有项目,然后返回一个平面列表。
这有点无关紧要,但由于这个问题被标记为最佳实践,我会继续投入我的两分钱。对于这种类型的东西,我非常喜欢将它变成一个属性:
public static IEnumerable<Product> AllProducts
{
get {
using (AdventureWorksEntities db = new AdventureWorksEntities()) {
var products = from product in db.Product
select product;
return products;
}
}
}
当然,它有点样板,但使用它的代码看起来会更干净:
prices = Whatever.AllProducts.Select (product => product.price);
对比
prices = Whatever.GetAllProducts().Select (product => product.price);
注意:对于任何可能需要一段时间才能完成工作的方法,我不会这样做。
那这个呢?
public static IEnumerable<Product> GetAllProducts()
{
using (AdventureWorksEntities db = new AdventureWorksEntities())
{
var products = from product in db.Product
select product;
return products.ToList();
}
}
我想这更干净。不过,我手头没有 VS2008 可供检查。在任何情况下,如果 Products 实现了 IEnumerable (看起来 - 它在 foreach 语句中使用),我会直接返回它。
在这种情况下,我会使用代码的第 2 版。由于您拥有可用产品的完整列表,并且这是此方法调用的“消费者”所期望的,因此需要将完整信息发送回调用者。
如果这个方法的调用者一次需要“一个”信息,而下一个信息的消费是按需的,那么使用yield return是有益的,它可以确保执行命令在何时返回给调用者一个信息单元可用。
可以使用 yield return 的一些示例是:
复杂的分步计算,其中调用者一次等待一个步骤的数据 在 GUI 中分页 - 用户可能永远不会到达最后一页,并且只需要在当前页面上披露信息的子集
为了回答你的问题,我会使用版本 2。
直接返回列表。好处:
更清楚
该列表是可重复使用的。 (迭代器不是)实际上不是真的,谢谢乔恩
当您认为您可能不必一直迭代到列表末尾或当它没有结尾时,您应该使用迭代器(yield)。例如,客户端调用将搜索满足某个谓词的第一个产品,您可能会考虑使用迭代器,尽管这是一个人为的示例,并且可能有更好的方法来完成它。基本上,如果您事先知道需要计算整个列表,请提前进行。如果您认为不会,请考虑使用迭代器版本。
鉴于确切的两个代码片段,我认为版本 1 更好,因为它可以更有效。假设有很多产品,调用者想要转换为 DTO。
var dtos = GetAllProducts().Select(ConvertToDto).ToList();
在版本 2 中,首先会创建一个 Product 对象列表,然后是另一个 ProductDto 对象列表。在版本 1 中,没有 Product 对象列表,只构建了所需的 ProductDto 对象列表。
即使没有转换,我认为版本 2 也存在一个问题:列表作为 IEnumerable 返回。 GetAllProducts() 的调用者不知道枚举结果的代价有多大。如果调用者需要多次迭代,她可能会使用 ToList() 实现一次(ReSharper 等工具也建议这样做)。这会导致已经在 GetAllProducts() 中创建的列表的不必要副本。因此,如果应使用版本 2,则返回类型应为 List 而不是 IEnumerable。
yield 的用法与关键字 return 类似,只是它会返回一个 generator。 generator 对象只会遍历 一次。
产量有两个好处:
您不需要读取这些值两次;您可以获得许多子节点,但不必将它们全部放在内存中。
还有另一个明确的explanation可能对您有所帮助。
Yield return
似乎是编写您自己的自定义迭代器类(实现 IEnumerator)的简写。因此,上述好处也适用于自定义迭代器类。无论如何,两种构造都保持中间状态。在其最简单的形式中,它是关于持有对当前对象的引用。