正确使用“收益率”


903

产量关键字是其中的一个关键字,在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>();
    }
}

38
yield被绑定到IEnumerable<T>其种类。在
某种程度上是

这是对类似问题的一个很好的答案。 stackoverflow.com/questions/15381708/...
Sanjeev拉伊

1
这里有一个很好的使用例子:stackoverflow.com/questions/3392612/...
ValGe

6
yield return如果遍历结果的代码GetAllProducts()允许用户有机会过早取消处理,我看到了一个很好的案例。
JMD 2014年

Answers:


806

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

使用版本2之前,您必须具有完整的列表,然后才能返回。通过使用yield-return,您实际上只需要在退货之前就拥有下一项。

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

最好使用yield-return的另一种情况是IEnumerable表示一个无限集。考虑素数列表或无限数量的随机数。您永远无法一次返回完整的IEnumerable,因此您可以使用yield-return递增地返回列表。

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


31
我想在问题3的示例中总结出两个好处。1)它分散了计算成本(有时是收益,有时却没有)2)在许多用例中,它可能会无限期地避免计算。您没有提及它保持中间状态的潜在缺点。如果您有大量的中间状态(例如使用HashSet进行重复消除),那么yield的使用会增加您的内存占用量。
Kennet Belenky 2012年

8
同样,如果每个单独的元素很大,但是只需要顺序访问它们,则产量会更好。
Kennet Belenky 2012年

2
最后...有一种稍微有点奇怪但偶尔有效的技术,可以使用yield以非常序列化的形式编写异步代码。
Kennet Belenky 2012年

12
另一个有趣的示例是读取相当大的CSV文件时。您想阅读每个元素,但同时也要提取依赖关系。返回IEnumerable <>的yield将允许您返回每一行并分别处理每一行。无需将10 Mb文件读入内存。一次仅一行。
Maxime Rouiller 2013年

1
Yield return似乎是编写自己的自定义迭代器类(实现IEnumerator)的简写。因此,上述好处也适用于自定义迭代器类。无论如何,两个构造都保持中间状态。最简单的形式是持有对当前对象的引用。
J. Ouwehand

641

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


180
我完全知道,这个答案不是技术性的答案,但是我认为,当理解yield关键字时,yield和视频流之间的相似之处就是一个很好的例子。关于该主题的所有技术知识都已经讲过,因此我尝试用“换句话说”进行解释。是否有社区规定说您不能用非技术性术语解释您的想法?
anar khalilov

13
我不确定是谁投票赞成您或为什么投票(我希望他们会发表评论),但我认为这确实是从非技术性的角度来描述的。
SENFO

22
仍然抓住了这个概念,这有助于将其进一步聚焦,很好的类比。
托尼

11
我喜欢这个答案,但不能回答这个问题。
2015年

73

作为理解何时应使用的概念性示例,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 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,因为无论如何您已经拥有完整的列表。


30

我知道这是一个古老的问题,但是我想举一个例子,说明如何创造性地使用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; }
}

设置类似的内容并逐步执行以观察发生了什么可能会有所帮助。

希望这可以帮助某人!我非常喜欢探索产量的​​不同使用方式。


1
您的代码示例是一个很好的示例,说明了如何使用for或foreach块的yield OUTSIDE。大多数示例显示了迭代器中的收益率回报。非常有用,因为我正要问关于如何在迭代器之外使用yield的问题!
shelbypereira

我从来没有想过用yield这种方式。似乎是一种模仿异步/等待模式的好方法(我假设将使用它代替yield今天重写的模式)。yield自从您回答了这个问题以来,随着C#的发展,您发现这些创造性的用途多年来已经产生了(没有双关语意)递减的收益吗?还是您仍在提出诸如此类的现代化智能用例?如果是这样,您介意为我们分享另一个有趣的场景吗?
凌晨

27

这似乎是一个奇怪的建议,但我yield通过阅读有关Python生成器的演示文稿:David M. Beazley的http://www.dabeaz.com/generators/Generators.pdf了解了如何在C#中使用关键字。您不需要了解太多Python就可以了解演示文稿-我不是。我发现它不仅有助于解释发电机的工作原理,还可以解释为什么您应该关心它。


1
该演示文稿提供了简单的概述。Ray Chen在stackoverflow.com/a/39507/939250 的链接中讨论了它在C#中的工作方式的详细信息。第一个链接详细说明了在yield return方法的末尾有第二个隐式返回。
Donal Lafferty 2012年

18

对于需要迭代数百万个对象的算法,收益回报可能非常强大。考虑以下示例,您需要在其中计算出可能的出行次数以进行乘车共享。首先,我们生成可能的行程:

    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毫秒即可运行,而根本不占用任何内存。


2
在幕后收益是多少?我会以为这是一个列表,因此它将如何提高内存使用率?

1
@rolls yield通过内部实现状态机在后台运行。这是SO的答案,其中包含3条详细的MSDN博客文章,这些文章详细解释了实现。由Raymond Chen @ MSFT撰写
Shiva,

13

这两段代码实际上在做两件事。第一个版本将根据需要拉成员。第二个版本将开始执行任何操作之前将所有结果加载到内存中。

这个答案没有对与错。哪种情况更好取决于具体情况。例如,如果您有一定的时间限制来完成查询,并且您需要对结果进行一些半复杂的操作,那么第二个版本可能会更好。但是要当心大型结果集,尤其是如果您以32位模式运行此代码时。执行此方法时,我多次被OutOfMemory异常咬伤。

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


11

产量有两个重大用途

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

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

以下是我制作的简单视频,并进行了全面演示,以支持上述两点

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


10

这是克里斯·塞尔斯讲述这些语句的C#编程语言 ;

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

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

在这些情况下,记住收益率并不是像收益率那样的“最终”收益。


为了减少歧义,请在您可以,是或可能时澄清一下。第一个可能返回而不执行第二个收益吗?
Johno Crawford

@JohnoCrawford,仅当枚举IEnumerable的第二个/下一个值时,第二个yield语句才会执行。例如,这完全有可能不会F().Any()-仅在枚举第一个结果后才会返回。通常,您不应该依赖IEnumerable yield来更改程序状态,因为它实际上可能不会被触发
Zac Faragher

8

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

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


8

这还不止于此,但是由于这个问题被标记为最佳做法,因此我继续讲两分钱。对于这种类型的事情,我非常喜欢将其设置为属性:

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

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


7

那呢?

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
请编辑OP以包括更多信息,而不是发布答案。
布赖恩·拉斯穆森

好吧,你必须告诉我OP到底代表什么:-)谢谢
petr k。

我认为是原始帖子。我无法编辑帖子,因此这似乎是一种方法。
彼得·k。

5

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

如果此方法的调用者一次需要“一个”信息,而下一个信息的使用是按需使用的,则使用yield return将是有益的,它将确保在以下情况下将执行命令返回给调用者:一个信息单元可用。

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

  1. 复杂的分步计算,其中调用方一次等待一个步骤的数据
  2. 在GUI中进行分页-用户可能永远无法到达最后一页,并且仅需要在当前页面上公开子信息集

为了回答您的问题,我将使用版本2。


3

直接返回列表。优点:

  • 比较清楚
  • 该列表是可重用的。(迭代器不是)实际上不是真的,谢谢乔恩

当您认为不必完全迭代列表的末尾或列表没有结束时,应使用迭代器(收益率)。例如,客户端调用将搜索满足某些谓词的第一个产品,您可以考虑使用迭代器,尽管这是一个人为的示例,并且可能有更好的方法来实现它。基本上,如果您事先知道需要计算整个列表,则只需预先进行即可。如果您认为不会,请考虑使用迭代器版本。


不要忘记它以IEnumerable <T>而不是IEnumerator <T>返回-您可以再次调用GetEnumerator。
乔恩·斯基特

即使您事先知道将需要计算整个清单,使用收益率回报还是有好处的。一个示例是集合包含数十万个项目。
Val

1

收益返回关键字短语用于维护特定集合的状态机。无论CLR看到使用了yield return关键短语的任何地方,CLR都会对该代码段实现一个Enumerator模式。这种类型的实现可以帮助开发人员避免所有类型的管道,否则我们将不得不在缺少关键字的情况下进行这些工作。

假设开发人员正在过滤某个集合,迭代该集合,然后将这些对象提取到某个新集合中。这种管道非常单调。

本文的关键字更多。


-4

yield的用法类似于关键字return,不同之处在于它将返回生成器。并且生成器对象将仅遍历一次

产量有两个好处:

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

还有另一种清晰的解释可能会对您有所帮助。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.