正确使用“收益率回报”

产量关键字是C#中的关键字之一,它继续让我感到困惑,我从来没有确信我正确地使用了它。

以下两段代码中,哪一段是首选的,为什么?

版本1:使用收益率返回

public static IEnumerable<Product> GetAllProducts(){using (AdventureWorksEntities db = new AdventureWorksEntities()){var products = from product in db.Productselect 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.Productselect product;
return products.ToList<Product>();}}
285192 次浏览

那这个呢?

public static IEnumerable<Product> GetAllProducts(){using (AdventureWorksEntities db = new AdventureWorksEntities()){var products = from product in db.Productselect product;
return products.ToList();}}

我想这要干净得多。不过,我手头没有VS2008来检查。在任何情况下,如果Products实现了IENumable(就像它看起来的那样-它被用在Foreach语句中),我会直接返回它。

假设您的产品LINQ类使用类似的产量进行枚举/迭代,第一个版本更有效,因为它每次迭代时只产生一个值。

第二个示例是使用ToList()方法将枚举器/迭代器转换为列表。这意味着它手动迭代枚举器中的所有项目,然后返回一个平面列表。

当我计算列表中的下一个项目(甚至下一组项目)时,我倾向于使用收益率返回。

使用版本2,您必须在返回之前拥有完整的列表。如果你使用的是回收站,你只需要在返回前找到下一个项目即可。

除其他外,这有助于将复杂计算的计算成本分摊到更大的时间范围内。例如,如果列表连接到GUI,并且用户从不转到最后一页,则永远不会计算列表中的最终项目。

另一种更适合产率返回的情况是,如果IENumable代表一个无限集合。考虑素数列表,或随机数的无限列表。你永远不可能一次返回完整的IENumable,所以你使用产率返回递增返回列表。

在您的特定示例中,您有完整的产品列表,因此我将使用版本2。

直接返回列表。好处:

  • 这更清楚了
  • 列表是可重用的。(迭代器不是)不是真的,谢谢你,乔恩

当你认为可能不需要一直迭代到列表末尾,或者列表没有末尾时,你应该使用迭代器。例如,客户端调用将搜索第一个满足某个谓词的产品,你可能会考虑使用迭代器,尽管这是一个人为的例子,可能有更好的方法来实现它。基本上,如果你提前知道需要计算整个列表,只需预先计算。如果你认为不会,然后考虑使用迭代器版本。

这两段代码实际上做了两件不同的事情。第一个版本将在您需要时提取成员。第二个版本将把所有结果加载到内存之前中,您开始使用它做任何事情。

这个答案没有对错之分。哪一个更可取只是取决于具体情况。例如,如果你必须完成查询的时间有限,并且你需要对结果做一些半复杂的事情,那么第二个版本可能更可取。但是要小心大型结果集,特别是如果你在32位模式下运行此代码。在执行此方法时,我多次被OutOfMemory异常所困扰。

但要记住的关键是:差异在于效率。因此,您可能应该选择使您的代码更简单的任何一个,并在分析后才更改它。

这有点离题,但由于这个问题被标记为最佳实践,我将继续投入我的两分钱。对于这种类型的事情,我更喜欢把它变成一个属性:

public static IEnumerable<Product> AllProducts{get {using (AdventureWorksEntities db = new AdventureWorksEntities()) {var products = from product in db.Productselect product;
return products;}}}

当然,它更像一个样板,但使用它的代码看起来会更清晰:

prices = Whatever.AllProducts.Select (product => product.price);

vs

prices = Whatever.GetAllProducts().Select (product => product.price);

备注:对于任何可能需要一段时间才能完成工作的方法,我都不会这样做。

我知道这是一个古老的问题,但我想提供一个如何创造性地使用产量关键字的例子。我从这项技术中受益真的。希望这对任何遇到这个问题的人都有帮助。

注意:不要认为这仅仅是构建集合的另一种方式。很大一部分的产量的力量来自于这样一个事实,即执行是暂停方法或属性,直到调用代码迭代下一个值。这是我的例子:

使用的关键字(与Rob Eisenburg的Caliburn. Micro协程实现)允许我表达一个异步调用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();}

这将做的是打开我的BusyIndex ator,调用我的Web服务上的Login方法,将我的IsLoggedIn标志设置为返回值,然后关闭BusyIndex ator。

这是它的工作原理:IResult有一个Execute方法和一个截止事件。Caliburn. Micro从对HandleButtonClick()的调用中获取IE分子并将其传递到Coroutine. Start inExecute方法中。BeginExecute方法开始遍历IResult。当返回第一个IResult时,HandleButtonClick()内部暂停执行,并且BeginExecute()将事件处理程序附加到截止事件并调用Execute()。IResult. Execute()可以执行同步或异步任务,并在完成后触发已完成事件。

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; }}

它可能有助于设置这样的东西,并通过执行来观察发生了什么。

希望这能帮助到别人!我真的很喜欢探索产量的不同使用方式。

在这种情况下,我将使用代码的版本2。由于您有可用产品的完整列表,并且这是此方法调用的“消费者”所期望的,因此需要将完整的信息发送回调用者。

如果此方法的调用者一次需要“一个”信息,并且下一个信息的消费是按需的,那么使用产量返回将是有益的,这将确保执行命令将在单位信息可用时返回给调用者。

可以使用收益率回报的一些例子是:

  1. 复杂的分步计算,调用者一次等待一个步骤的数据
  2. GUI中的分页-用户可能永远不会到达最后一页,并且只需要在当前页面上披露信息的子集

为了回答你的问题,我会使用版本2。

作为理解何时应该使用yield的概念示例,假设方法ConsumeLoop()处理ProduceList()返回/产生的项目:

void ConsumeLoop() {foreach (Consumable item in ProduceList())        // might have to wait hereitem.Consume();}
IEnumerable<Consumable> ProduceList() {while (KeepProducing())yield return ProduceExpensiveConsumable();    // expensive}

如果没有yield,调用ProduceList()可能需要很长时间,因为您必须在返回之前完成列表:

//pseudo-assemblyProduce consumable[0]                   // expensive operation, e.g. disk I/OProduce consumable[1]                   // waiting...Produce consumable[2]                   // waiting...Produce consumable[3]                   // completed the consumable listConsume consumable[0]                   // start consumingConsume consumable[1]Consume consumable[2]Consume consumable[3]

使用yield,它变得重新排列,有点交错:

//pseudo-assemblyProduce consumable[0]Consume consumable[0]                   // immediately yield & ConsumeProduce consumable[1]                   // ConsumeLoop iterates, requesting next itemConsume consumable[1]                   // consume nextProduce consumable[2]Consume consumable[2]                   // consume nextProduce consumable[3]Consume consumable[3]                   // consume next

最后,正如之前许多人已经建议的那样,您应该使用版本2,因为无论如何您已经有了完整的列表。

填充临时列表就像下载整个视频,而使用yield就像流式传输该视频。

产量有两个伟大的用途

它有助于提供自定义迭代,而无需创建临时集合。(加载所有数据并循环)

它有助于进行有状态的迭代。(流)

下面是一个简单的视频,我已经创建了完整的演示,以支持上述两点

http://www.youtube.com/watch?v=4fju3xcm21M

这就是克里斯卖C#编程语言中那些陈述的描述;

我有时会忘记,收益率回报与回报不一样,在可以执行产量返回后的代码。例如,第一次返回后的代码永远不会执行:

int F() {return 1;return 2; // Can never be executed}

相比之下,在这里第一次产生返回后的代码可以是执行:

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!}

在这种情况下,记住收益率不是“最终”的返回是有用的

对于需要迭代数百万个对象的算法来说,收益率回报可能非常强大。考虑以下示例,您需要计算拼车的可能行程。首先我们生成可能的行程:

    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 yieldif (IsTripGood(trip)){// match good trip}}}

如果你使用List而不是产量,你需要将100万对象分配到内存(~190mb),这个简单的例子将需要~1400ms来运行。然而,如果你使用产量,你不需要将所有这些temp对象放在内存中,你将获得明显更快的算法速度:这个例子只需要~400ms来运行,根本没有内存消耗。

产量的用法与关键字返回类似,只是它将返回生成器发生器对象只会遍历一次

产量有两个好处:

  1. 您不需要两次读取这些值;
  2. 您可以获得许多子节点,但不必将它们全部放在内存中。

还有一个明确的解释可能会帮助你。

鉴于确切的两个代码片段,我认为版本1更好,因为它可以更有效。假设有很多产品,调用者想转换为DTO。

var dtos = GetAllProducts().Select(ConvertToDto).ToList();

在版本2中,首先创建一个产品对象列表,然后创建另一个ProductDto对象列表。在版本1中,没有产品对象列表,只有所需的ProductDto对象列表被构建。

即使不转换,版本2在我看来也有一个问题:列表以IE的形式返回。GetAllProducts()的调用者不知道结果的枚举有多昂贵。如果调用者需要多次迭代,她可能会使用ToList()实现一次(ReSharper等工具也建议这样做)。这会导致GetAllProducts()中已经创建的列表的不必要副本。所以如果应该使用版本2,返回类型应该是List而不是IE的。