正确使用'收益率'

sen*_*nfo 873 c# yield-return

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

版本2:返回列表

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

        return products.ToList<Product>();
    }
}
Run Code Online (Sandbox Code Playgroud)

abe*_*nky 783

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

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

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

另一种情况,其中yield-return是优选的,如果IEnumerable表示无限集.考虑素数列表,或无限的随机数列表.您永远不能一次返回完整的IEnumerable,因此您使用yield-return以递增方式返回列表.

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

  • 我很挑剔,在你的问题3的例子中,有两个好处.1)它分散计算成本(有时是好处,有时不是)2)在许多用例中,它可能懒得避免计算无限期.你没有提到它保持中间状态的潜在缺点.如果你有大量的中间状态(例如HashSet用于重复消除),那么使用yield会增加你的内存占用. (31认同)
  • 另一个可能有趣的例子是读取相当大的CSV文件.您想要阅读每个元素,但您也想要提取您的依赖关系.返回IEnumerable <>的yield将允许您返回每一行并单独处理每一行.无需将10 Mb文件读入内存.一次只有一行. (12认同)
  • 此外,如果每个单独的元素非常大,但它们只需要按顺序访问,则产量更好. (8认同)
  • 最后......使用yield以非常序列化的形式编写异步代码有一种稍微不稳定但偶尔有效的技术. (2认同)

ana*_*lov 615

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

  • 我完全清楚这个答案不是技术上的答案,但我相信在理解yield关键字时,yield和视频流之间的相似性就是一个很好的例子.关于这个主题的所有技术都已经说过了,所以我试着解释"换句话说".是否有社区规则说您无法用非技术术语解释您的想法? (173认同)
  • 仍然掌握这一概念,这有助于将其进一步聚焦,这是一个很好的类比. (20认同)
  • 我不确定是谁投票给你或为什么(我希望他们会评论),但我认为它确实从非技术性的角度描述了它. (12认同)
  • 我喜欢这个答案,但它没有回答这个问题. (10认同)

Kac*_*che 68

作为理解何时应该使用的概念性示例,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
}
Run Code Online (Sandbox Code Playgroud)

没有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]
Run Code Online (Sandbox Code Playgroud)

使用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
Run Code Online (Sandbox Code Playgroud)

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


Ada*_*ley 27

我知道这是一个老问题,但我想提供一个如何创造性地使用yield关键字的示例.我真的受益于这种技术.希望这对那些偶然发现这个问题的人有所帮助.

注意:不要将yield关键字视为构建集合的另一种方式.产量的很大一部分来自于在您的方法或属性中暂停执行,直到调用代码迭代下一个值.这是我的例子:

使用yield关键字(以及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();
}
Run Code Online (Sandbox Code Playgroud)

这样做是打开我的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; }
}
Run Code Online (Sandbox Code Playgroud)

设置这样的东西并逐步执行以观察正在发生的事情可能会有所帮助.

希望这可以帮助别人!我非常喜欢探索可以使用产量的不同方法.


Rob*_*ney 26

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


And*_*vic 14

对于需要遍历数百万个对象的算法,收益率返回非常有用.请考虑以下示例,您需要计算rideshare的可能行程.首先我们生成可能的旅行:

    static IEnumerable<Trip> CreatePossibleTrips()
    {
        for (int i = 0; i < 1000000; i++)
        {
            yield return new Trip
            {
                Id = i.ToString(),
                Driver = new Driver { Id = i.ToString() }
            };
        }
    }
Run Code Online (Sandbox Code Playgroud)

然后遍历每次旅行:

    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
            }
        }
    }
Run Code Online (Sandbox Code Playgroud)

如果使用List而不是yield,则需要将100万个对象分配给内存(~190mb),这个简单的示例需要大约1400ms才能运行.但是,如果使用yield,则不需要将所有这些临时对象放入内存中,并且您将获得明显更快的算法速度:此示例仅运行约400毫秒而根本没有内存消耗.

  • 什么是收益?我本以为它是一个列表,因此它将如何提高内存使用率? (2认同)
  • @rolls `yield` 通过在内部实现状态机在幕后工作。[这是一个带有 3 篇详细 MSDN 博客文章的 SO 答案](/sf/answers/51975241/),它非常详细地解释了实现。作者:Raymond Chen @ MSFT (2认同)

Jas*_*ker 12

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

这个没有正确或错误的答案.哪一个更可取仅取决于具体情况.例如,如果您必须完成查询的时间有限,并且您需要执行与结果半复杂的操作,则第二个版本可能更可取.但要注意大型结果集,特别是如果您在32位模式下运行此代码.在执行此方法时,我多次被OutOfMemory异常所困.

要记住的关键是:差异在于效率.因此,您可能应该选择使代码更简单的方法,并在分析后更改它.


Shi*_*ala 11

产量有两个很大的用途

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

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

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

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


Teo*_*ahi 9

这就是Chris SellsC#编程语言中讲述的那些陈述;

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

    int F() {
return 1;
return 2; // Can never be executed
}
Run Code Online (Sandbox Code Playgroud)

相反,可以执行第一次收益返回后的代码:

IEnumerable<int> F() {
yield return 1;
yield return 2; // Can be executed
}
Run Code Online (Sandbox Code Playgroud)

这经常在if语句中咬我:

IEnumerable<int> F() {
if(...) { yield return 1; } // I mean this to be the only
// thing returned
yield return 2; // Oops!
}
Run Code Online (Sandbox Code Playgroud)

在这些情况下,记住收益率回报并不像返回那样"最终"是有帮助的.


Sov*_*iut 8

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

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


Mar*_*osi 8

除了这一点之外,这有点类似,但由于这个问题被标记为最佳实践,我将继续并投入我的两分钱.对于这种类型的东西,我更喜欢把它变成一个属性:

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

            return products;
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

当然,这是一个更多的锅炉板,但使用它的代码看起来会更清洁:

prices = Whatever.AllProducts.Select (product => product.price);
Run Code Online (Sandbox Code Playgroud)

VS

prices = Whatever.GetAllProducts().Select (product => product.price);
Run Code Online (Sandbox Code Playgroud)

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


pet*_* k. 7

那怎么样?

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

        return products.ToList();
    }
}
Run Code Online (Sandbox Code Playgroud)

我想这更干净了.不过,我手边没有VS2008.在任何情况下,如果Products实现IEnumerable(似乎 - 它在foreach语句中使用),我会直接返回它.

  • 请编辑OP以包含更多信息而不是发布答案. (2认同)

Int*_*ary 5

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

如果此方法的调用者一次需要"一个"信息并且下一个信息的消耗是按需的,那么使用yield return将是有益的,这将确保执行命令将在返回给调用者时返回给调用者.提供一个信息单位.

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

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

要回答你的问题,我会使用版本2.