Proper use of 'yield return'
yield关键字是C中继续迷惑我的关键字之一,我从未确信我正确使用它。
以下两段代码中,哪一段是首选代码,为什么?
版本1:使用收益率返回
1 2 3 4 5 6 7 8 9 10 11 12 13 | 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:返回列表
1 2 3 4 5 6 7 8 9 10 | public static IEnumerable<Product> GetAllProducts() { using (AdventureWorksEntities db = new AdventureWorksEntities()) { var products = from product in db.Product select product; return products.ToList<Product>(); } } |
当我计算列表中的下一项(甚至下一组项)时,我倾向于使用yield return。
使用您的版本2,您必须在返回之前拥有完整的列表。通过使用yield return,您实际上只需要在返回之前拥有下一个项目。
除此之外,这有助于将复杂计算的计算成本分摊到更大的时间范围内。例如,如果列表连接到一个图形用户界面,而用户从未访问最后一页,则永远不会计算列表中的最后一项。
如果IEnumerable表示一个无限集,则收益率返回更可取。考虑素数列表,或无限随机数列表。不能一次返回完整的IEnumerable,因此使用yield return以递增方式返回列表。
在您的特定示例中,您有完整的产品列表,因此我将使用版本2。
填充一个临时列表就像下载整个视频,而使用
作为理解何时应该使用
1 2 3 4 5 6 7 8 9 | void ConsumeLoop() { foreach (Consumable item in ProduceList()) // might have to wait here item.Consume(); } IEnumerable<Consumable> ProduceList() { while (KeepProducing()) yield return ProduceExpensiveConsumable(); // expensive } |
如果没有
1 2 3 4 5 6 7 8 9 | //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] |
使用
1 2 3 4 5 6 7 8 9 | //pseudo-assembly Produce consumable[0] Consume consumable[0] // immediately Consume Produce consumable[1] Consume consumable[1] // consume next Produce consumable[2] Consume consumable[2] // consume next Produce consumable[3] Consume consumable[3] // consume next |
最后,正如之前所建议的那样,您应该使用版本2,因为您已经拥有了完整的列表。
这似乎是一个奇怪的建议,但我通过阅读关于python中生成器的演示,了解了如何使用c中的
我知道这是一个老问题,但我想举一个例子说明如何创造性地使用yield关键字。我真的从这项技术中受益匪浅。希望这能对任何一个偶然发现这个问题的人有所帮助。
注意:不要认为yield关键字只是构建集合的另一种方法。屈服力的很大一部分来自这样一个事实:执行在你的方法或属性,直到调用代码迭代下一个值。下面是我的例子:
使用yield关键字(与rob eisenburg的caliburn.micro coroutines实现一起),我可以对如下Web服务表示异步调用:
1 2 3 4 5 6 7 8 9 | 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服务上的登录方法,将我的isloggedin标志设置为返回值,然后关闭busyindicator。
这是如何工作的:IResult有一个执行方法和一个完成的事件。micro从对handleButtonClick()的调用中获取IEnumerator,并将其传递给coroutine.beginExecute方法。BeginExecute方法开始迭代IResults。返回第一个IResult时,将在handleButtonClick()内暂停执行,BeginExecute()将事件处理程序附加到完成的事件并调用Execute()。execute()可以执行同步或异步任务,并在完成后激发完成的事件。
LoginResult看起来像这样:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | 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; } } |
它可能有助于设置类似这样的内容,并逐步执行以监视正在发生的事情。
希望这能帮助别人!我真的很喜欢探索收益率的不同使用方式。
这两段代码实际上在做两件不同的事情。第一个版本将根据需要拉成员。第二个版本将把所有的结果加载到内存中,然后再开始处理它。
这个答案没有对错之分。哪一个更好,这取决于具体情况。例如,如果您必须完成查询的时间有限,并且您需要对结果执行一些半复杂的操作,那么最好使用第二个版本。但要注意大型结果集,尤其是在32位模式下运行此代码时。在执行此方法时,我曾多次被内存不足异常咬伤。
但要记住的关键是:效率的差异。因此,您可能应该选择使代码更简单的代码,并且只有在分析之后才进行更改。
对于需要迭代数百万个对象的算法,yield返回可能非常强大。请考虑下面的示例,您需要在其中计算乘客共享的可能行程。首先,我们产生可能的旅行:
1 2 3 4 5 6 7 8 9 10 11 |
然后迭代每次行程:
1 2 3 4 5 6 7 8 9 10 11 | static void Main(string[] args) { foreach (var trip in CreatePossibleTrips(trips)) { // possible trip is actually calculated only at this point, because of yield if (IsTripGood(trip)) { // match good trip } } } |
如果使用list而不是yield,则需要将100万个对象分配给内存(~190MB),这个简单的例子需要大约1400ms才能运行。但是,如果使用yield,则不需要将所有这些temp对象都放到内存中,而且算法速度会显著加快:这个示例只需要大约400毫秒就可以运行,而不需要消耗任何内存。
产量有两大用途
它有助于在创建临时集合时提供自定义迭代。(加载所有数据和循环)
它有助于进行有状态的迭代。(流媒体)
下面是一个简单的视频,我创建了完整的演示,以支持以上两点
http://www.youtube.com/watch?V= 4FJU3XCM21M
这就是Chris销售的关于C语言编程语言中的语句的内容;
I sometimes forget that yield return is not the same as return , in
that the code after a yield return can be executed. For example, the
code after the first return here can never be executed:
1
2
3
4 int F() {
return 1;
return 2; // Can never be executed
}In contrast, the code after the first yield return here can be
executed:
1
2
3
4 IEnumerable<int> F() {
yield return 1;
yield return 2; // Can be executed
}This often bites me in an if statement:
1
2
3
4
5 IEnumerable<int> F() {
if(...) { yield return 1; } // I mean this to be the only
// thing returned
yield return 2; // Oops!
}In these cases, remembering that yield return is not"final" like
return is helpful.
这有点离题了,但既然这个问题被贴上了"最佳实践"的标签,我会继续写我的两分钱。对于这种类型的东西,我非常喜欢把它变成一种财产:
1 2 3 4 5 6 7 8 9 10 11 | public static IEnumerable<Product> AllProducts { get { using (AdventureWorksEntities db = new AdventureWorksEntities()) { var products = from product in db.Product select product; return products; } } } |
当然,这是一个有点锅炉板,但代码使用这将看起来更清洁:
1 | prices = Whatever.AllProducts.Select (product => product.price); |
VS
1 | prices = Whatever.GetAllProducts().Select (product => product.price); |
注意:对于任何可能需要一段时间才能完成工作的方法,我都不会这样做。
假设您的products-linq类使用类似的yield进行枚举/迭代,那么第一个版本的效率更高,因为它每次迭代时只生成一个值。
第二个示例是使用to list()方法将枚举器/迭代器转换为列表。这意味着它手动迭代枚举器中的所有项,然后返回一个简单列表。
那这个呢?
1 2 3 4 5 6 7 8 9 10 | public static IEnumerable<Product> GetAllProducts() { using (AdventureWorksEntities db = new AdventureWorksEntities()) { var products = from product in db.Product select product; return products.ToList(); } } |
我想这个要干净得多。不过,我手头没有VS2008。在任何情况下,如果产品实现IEnumerable(正如它看起来的那样——它在foreach语句中使用),我将直接返回它。
在本例中,我将使用代码的版本2。由于您有完整的可用产品列表,这也是此方法调用的"使用者"所期望的,因此需要将完整的信息发送回调用方。
如果这个方法的调用者一次需要"一"个信息,并且下一个信息的消耗是按需的,那么使用yield return将是有益的,它将确保当一个信息单元可用时,执行命令将返回给调用者。
一些可以使用收益率回报的例子是:
为了回答你的问题,我会使用版本2。
直接返回列表。效益:
- 更清楚
列表是可重用的。(迭代器不是).not actually true,thanks jon
当您认为可能不需要一直迭代到列表末尾,或者当列表没有结尾时,应该使用迭代器(yield)。例如,客户机调用将要搜索满足某些谓词的第一个产品,您可以考虑使用迭代器,尽管这是一个做作的示例,而且可能有更好的方法来完成它。基本上,如果你提前知道需要计算整个列表,那么就提前做。如果您认为它不会,那么考虑使用迭代器版本。
yield-return关键字短语用于维护特定集合的状态机。当clr看到正在使用的yield返回关键字短语时,clr将对该代码段实现一个枚举器模式。这种类型的实现可以帮助开发人员处理所有类型的管道,否则在没有关键字的情况下,我们将不得不这样做。
假设开发人员正在过滤某个集合,遍历该集合,然后在某个新集合中提取这些对象。这种管道很单调。
关于本文中的关键字的更多信息。
yield的用法类似于关键字return,只是它将返回生成器。而生成器对象只会遍历一次。
收益有两个好处:
还有另一个明确的解释也许对你有帮助。