正确使用“收益回报”
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-return。
使用你的版本2,你必须在返回之前有完整的列表。 通过使用yield-return,你只需要在返回之前有下一个项目。
除此之外,这有助于在较大的时间范围内分摊复杂计算的计算成本。 例如,如果列表连接到GUI,并且用户永远不会到最后一页,则永远不会计算列表中的最终项目。
如果IEnumerable表示一个无限集,另一个情况是yield-return更可取。 考虑素数列表,或随机数的无限列表。 你永远不能一次返回完整的IEnumerable,所以你使用yield-return来递增返回列表。
在你的例子中,你有完整的产品列表,所以我会使用版本2。
填充临时列表就像下载整个video,而使用yield
就像stream式传输video。
作为理解何时应该使用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, eg 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 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,因为您已经拥有完整的列表。
这似乎是一个奇怪的build议,但我学习了如何在C#中使用yield
关键字,通过阅读Python中的生成器的演示文稿:David M. Beazley的generators/Generators.pdf 。 你不需要知道太多的Python来理解演示文稿 – 我没有。 我发现它不仅解释发电机是如何工作的,而且还有助于解释为什么你应该关心。
我知道这是一个老问题,但我想提供一个如何创造性地使用yield关键字的例子。 我真的从这个技术中受益。 希望这会帮助那些绊倒这个问题的人。
注意:不要将yield关键字看作是构build集合的另一种方式。 yield的一大部分来源于这样一个事实:执行在你的方法或属性中暂停 ,直到调用代码迭代下一个值。 这是我的例子:
使用yield关键字(与Rob Eisenburg的Caliburn.Micro协程实现一起)可以让我expression一个对Web服务的asynchronous调用,如下所示:
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标志设置为返回值,然后closuresBusyIndicator。
这是如何工作的:IResult有一个Execute方法和一个Completed事件。 Caliburn.Micro从调用HandleButtonClick()获取IEnumerator,并将其传递给Coroutine.BeginExecute方法。 BeginExecute方法开始遍历IResults。 当返回第一个IResult时,执行在HandleButtonClick()内暂停,BeginExecute()将一个事件处理程序附加到Completed事件并调用Execute()。 IResult.Execute()可以执行同步任务或asynchronous任务,并在完成时触发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; } }
这可能有助于build立这样的事情,并逐步执行,以观察发生了什么事情。
希望这可以帮助别人! 我真的很喜欢探索不同的产量可以使用的方式。
这两个代码实际上是做两件不同的事情。 第一个版本会拉你的成员,因为你需要他们。 第二个版本会在你开始做任何事情之前把所有的结果加载到内存中。
这个没有对或错的答案。 哪一个才是最好的取决于情况。 例如,如果您需要完成查询的时间有限,并且您需要对结果进行一些半复杂的处理,则可能需要使用第二个版本。 但要小心大的结果集,特别是如果你在32位模式下运行这个代码。 这种方法我已经多次被OutOfMemoryexception咬了。
要记住的关键是这个:差异在效率上。 因此,您可能应该使用简单的代码,并在分析之后才能更改代码。
收益率有两大用途
它有助于提供创build临时集合的自定义迭代。 (加载所有数据和循环)
它有助于做有状态的迭代。 (stream媒体)
以下是我为了支持上述两点而创build的一个简单的video
这点还有点不错,但是因为这个问题被贴上了最佳做法,我会继续前进,投入我的两分钱。 对于这种types的东西我非常喜欢把它变成一个属性:
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);
VS
prices = Whatever.GetAllProducts().Select (product => product.price);
注:我不会这样做的任何方法,可能需要一段时间才能完成工作。
Chris Sells在C#编程语言中讲述了这些陈述,
我有时会忘记yield return与return不同,因为yield return之后的代码可以被执行。 例如,第一次返回后的代码永远不会被执行:
int F() { return 1; return 2; // Can never be executed }
相反,在这里第一个yield返回之后的代码可以被执行:
IEnumerable<int> F() { yield return 1; yield return 2; // Can be executed }
这经常让我在一个if语句中咬我:
IEnumerable<int> F() { if(...) { yield return 1; } // I mean this to be the only // thing returned yield return 2; // Oops! }
在这些情况下,记住收益率回报不是“最终的”,就像回报是有帮助的。
假设您的产品LINQ类使用类似的yield进行枚举/迭代,第一个版本效率更高,因为它每次迭代时只产生一个值。
第二个例子是使用ToList()方法将枚举器/迭代器转换为列表。 这意味着它手动迭代枚举器中的所有项目,然后返回一个平面列表。
那这个呢?
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语句中使用的那样),我会直接返回它。
对于需要遍历数百万个对象的algorithm,产量回报可能非常强大。 考虑下面的例子,你需要计算可能的车次旅行。 首先我们产生可能的旅行:
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(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,则不需要将所有这些临时对象放在内存中,并且algorithm速度会显着提高:此示例只需要大约400毫秒就可以运行,而无需任何内存消耗。
直接返回列表。 优点:
- 这更清楚
-
该列表是可重用的。(迭代器不是)不是真的,谢谢乔恩
你应该使用迭代器(yield),当你认为你可能不需要一直迭代到列表的末尾,或者当它没有结束时。 例如,客户端调用将search满足某个谓词的第一个产品,您可以考虑使用迭代器,虽然这是一个人为的例子,并且可能有更好的方法来实现它。 基本上,如果你事先知道整个清单需要计算,那么就先做。 如果你认为它不会,那么考虑使用迭代器版本。
在这种情况下,我会使用第二版的代码。 由于您拥有可用产品的完整列表,并且这是此方法调用的“消费者”所期望的,因此将需要将完整的信息发回给调用者。
如果这个方法的调用者每次需要“一个”信息并且下一个信息的消费是按需的,那么使用收益率回报将是有利的,这将确保执行命令将被返回给调用者一个信息单位是可用的。
一些可以使用收益率回报的例子是:
- 呼叫者一次等待一个步骤的数据的复杂的分步计算
- 在GUI中进行分页 – 用户可能永远不会到达最后一页,并且只需要在当前页面上公开信息的子集
要回答你的问题,我会使用版本2。
yield return keyphrase用于维护特定集合的状态机。 无论CLR在哪里看到正在使用的yield return keyphrase,CLR都会为该段代码实现一个Enumerator模式。 这种types的实现可以帮助开发人员从所有types的pipe道中获得,否则我们必须在没有关键字的情况下执行这些pipe道。
假设开发人员过滤了一些集合,迭代集合,然后在一些新集合中提取这些对象。 这种水暖很单调。
这篇文章更多关于这个关键字 。
我不认为有人提到它,我希望我是正确的说这个,但代码版本1的一个可能的问题是,数据库连接保持打开,只要你没有完成迭代通过集合。
yield的使用与关键字return相似,只不过它会返回一个生成器 。 发生器对象只会遍历一次 。
产量有两个好处:
- 您不需要两次读取这些值;
- 你可以得到许多子节点,但不必把它们都放在内存中。
还有一个明确的解释可能会帮助你。