正确使用“收益回报”

Posted

技术标签:

【中文标题】正确使用“收益回报”【英文标题】:Proper use of 'yield return' 【发布时间】:2010-09-29 10:19:00 【问题描述】:

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>();
    

【问题讨论】:

yieldIEnumerable&lt;T&gt; 及其同类相关联。它在某种程度上是懒惰的评估 这里是类似问题的一个很好的答案。 ***.com/questions/15381708/… 这是一个很好的用法示例:***.com/questions/3392612/… 我认为使用yield return 是一个很好的例子,如果迭代GetAllProducts() 的结果的代码允许用户提前取消处理。 我发现这个帖子真的很有帮助:programmers.stackexchange.com/a/97350/148944 【参考方案1】:

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

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

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

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

【讨论】:

【参考方案2】:

这就是Chris Sells 在The C# Programming Language 中讲述的这些陈述;

我有时会忘记 yield return 与 return 不同,在 可以执行yield return之后的代码。例如, 此处第一次返回后的代码永远无法执行:

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

相比之下,这里第一次yield return之后的代码可以 执行:

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!

在这些情况下,请记住收益回报不是“最终的” 回报很有帮助。

【讨论】:

为了减少歧义,请在您说可以、会、会或可能的时候澄清一下?是否有可能第一个返回而不执行第二个收益? @JohnoCrawford 只有在枚举 IEnumerable 的第二个/下一个值时才会执行第二个 yield 语句。它完全有可能不会,例如F().Any() - 这将在尝试仅枚举第一个结果后返回。一般来说,您不应该依赖IEnumerable yield 来更改程序状态,因为它实际上可能不会被触发【参考方案3】:

作为理解何时应该使用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 版,因为您已经有了完整的列表。

【讨论】:

【参考方案4】:

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

    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 毫秒即可运行,完全没有内存消耗。

【讨论】:

在幕后什么是产量?我会认为它是一个列表,因此它将如何提高内存使用率? @rolls yield 通过在内部实现状态机在幕后工作。 Here's an SO answer with 3 detailed MSDN blog posts 详细解释了实现。由 Raymond Chen @ MSFT 撰写【参考方案5】:

yield 的用法与关键字return 类似,不同之处在于它将返回generatorgenerator 对象只会遍历一次

产量有两个好处:

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

还有一个明确的explanation或许能帮到你。

【讨论】:

【参考方案6】:

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

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

可以使用 yield return 的一些例子是:

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

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

【讨论】:

【参考方案7】:

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

【讨论】:

我非常清楚这个答案不是技术答案,但我相信在理解 yield 关键字时,yield 和视频流之间的相似性是一个很好的例子。关于这个主题的所有技术都已经说过,所以我试图“换句话说”解释。是否有社区规则规定您不能用非技术术语解释您的想法?【参考方案8】:

产量有两大用途

它有助于在不创建临时集合的情况下提供自定义迭代。 (加载所有数据并循环)

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

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

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

【讨论】:

【参考方案9】:

那么这个呢?

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 语句中使用),我会直接返回它。

【讨论】:

【参考方案10】:

我知道这是一个老问题,但我想提供一个示例,说明如何创造性地使用 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();

这将打开我的 BusyIndi​​cator,在我的 Web 服务上调用 Login 方法,将我的 IsLoggedIn 标志设置为返回值,然后重新关闭 BusyIndi​​cator。

这是如何工作的: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 的不同方式。

【讨论】:

您的代码示例是关于如何在 for 或 foreach 块中使用 yield OUTSIDE 的绝佳示例。大多数示例显示迭代器内的收益返回。非常有帮助,因为我正要问关于 SO 如何在迭代器之外使用 yield 的问题! 我从来没有想过以这种方式使用yield。这似乎是模拟 async/await 模式的一种优雅方式(如果今天重写,我假设将使用它而不是 yield)。自从您回答这个问题以来,随着 C# 的发展,您是否发现这些年来yield 的这些创造性使用已经产生(不是双关语)收益递减?或者你还在想出像这样的现代化聪明用例吗?如果是这样,您介意为我们分享另一个有趣的场景吗?【参考方案11】:

这有点不重要,但由于这个问题被标记为最佳实践,我会继续投入我的两分钱。对于这种类型的东西,我非常喜欢把它变成一个属性:

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

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

【讨论】:

【参考方案12】:

这两段代码实际上是在做两件不同的事情。第一个版本将根据需要拉取成员。第二个版本会将所有结果加载到内存中你开始使用它之前。

这个问题没有正确或错误的答案。哪个更可取取决于情况。例如,如果您必须完成查询的时间有限,并且您需要对结果做一些半复杂的事情,那么第二个版本可能更可取。但要注意大型结果集,尤其是在 32 位模式下运行此代码时。我在做这个方法的时候被OutOfMemory异常咬过好几次了。

但要记住的关键是:不同之处在于效率。因此,您可能应该选择任何一种使您的代码更简单的方法,并且仅在分析后进行更改。

【讨论】:

【参考方案13】:

直接返回列表。好处:

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

当您认为您可能不必一直迭代到列表末尾时,或者当它没有结尾时,您应该使用迭代器(yield)。例如,客户端调用将搜索满足某个谓词的第一个产品,您可能会考虑使用迭代器,尽管这是一个人为的示例,并且可能有更好的方法来完成它。基本上,如果您事先知道需要计算整个列表,请提前进行。如果您认为不会,请考虑使用迭代器版本。

【讨论】:

不要忘记它返回的是 IEnumerable,而不是 IEnumerator - 你可以再次调用 GetEnumerator。 即使您事先知道需要计算整个列表,使用收益回报率仍然可能是有益的。一个例子是集合包含数十万个项目。【参考方案14】:

当我计算列表中的下一个项目(甚至是下一组项目)时,我倾向于使用 yield-return。

使用您的第 2 版,您必须在返回之前拥有完整的列表。 通过使用yield-return,您实际上只需要在返回之前拥有下一项。

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

如果 IEnumerable 表示无限集,则 yield-return 更可取的另一种情况。考虑素数列表,或无限的随机数列表。您永远无法一次返回完整的 IEnumerable,因此您可以使用 yield-return 逐步返回列表。

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

【讨论】:

我会挑剔,在您的问题 3 示例中,将两个好处混为一谈。 1)它分散了计算成本(有时是好处,有时不是) 2)它可以在许多用例中无限期地懒惰地避免计算。您没有提到它保持中间状态的潜在缺点。如果您有大量的中间状态(例如用于消除重复的 HashSet),那么使用 yield 会增加您的内存占用。 另外,如果每个单独的元素都很大,但只需要按顺序访问,那么有一个yield会更好。 最后......有一种有点不稳定但偶尔有效的技术可以使用yield以非常序列化的形式编写异步代码。 另一个有趣的例子是读取相当大的 CSV 文件时。您想读取每个元素,但也想提取您的依赖关系。 Yield 返回 IEnumerable 将允许您返回每一行并单独处理每一行。无需将 10 Mb 文件读入内存。一次只有一行。 Yield return 似乎是编写自己的自定义迭代器类(实现 IEnumerator)的简写。因此,上述好处也适用于自定义迭代器类。无论如何,两种构造都保持中间状态。最简单的形式是持有对当前对象的引用。【参考方案15】:

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

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

【讨论】:

以上是关于正确使用“收益回报”的主要内容,如果未能解决你的问题,请参考以下文章

Java中的收益回报

收益回报与回报 IEnumerable<T>

使用MATLAB分析风险之下的投资分配方案

如何在 C# 中使用迭代器反向读取文本文件

韦杜汽车众筹-韦杜汽车众筹-广州韦杜贸易有限公司

一文读懂Elephant Swap,为何为ePLATO带来如此高的溢价?