首页 文章

正确使用'yield return'

提问于
浏览
820

yield关键字是C#中的其中一个keywords,它继续使我神秘,我've never been confident that I'正确使用它 .

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

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

Version 2: 返回列表

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

        return products.ToList<Product>();
    }
}

16 回答

  • 8

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

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

    使用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();
    }
    

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

    以下是它的工作原理: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; }
    }
    

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

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

  • 6

    这是Chris SellsThe C# Programming Language中讲述的那些陈述;

    我有时会忘记yield return与return不同,因为yield返回后的代码可以执行 . 例如,第一次返回此处后的代码永远不能执行:int F(){
    返回1;
    返回2; //永远不会被执行
    }
    相反,可以执行第一次yield返回后的代码:IEnumerable <int> F(){
    收益率1;
    收益率2; //可以执行
    }
    这通常会在if语句中引起我的注意:IEnumerable <int> F(){
    if(...){yield return 1; } //我的意思是这是唯一的
    //回来的东西
    收益率2; //哎呀!
    }
    在这些情况下,记住收益率回报并不像返回那样“最终”是有帮助的 .

  • 26

    yield 的用法类似于关键字 return ,但它将返回generator . generator 对象只会遍历 once .

    yield 有两个好处:

    • 您不需要两次读取这些值;

    • 您可以获得许多子节点,但不必将它们全部放在内存中 .

    还有另一个明确的explanation也许可以帮到你 .

  • 12

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

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

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

  • 61

    对于需要迭代数百万个对象的算法,yield yield非常强大 . 请考虑以下示例,您需要计算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() }
                };
            }
        }
    

    然后遍历每次旅行:

    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,则不需要将所有这些临时对象放入内存中,并且您将获得明显更快的算法速度:此示例将仅运行约400毫秒而根本没有内存消耗 .

  • 566

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

  • 742

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

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

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

    • 复杂的逐步计算,其中调用者一次等待一个步骤的数据

    • GUI中的分页 - 用户可能永远不会到达最后一页,只需要在当前页面上公开信息的子集

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

  • 25

    直接返回列表 . 优点:

    • 更清楚

    • 该列表是可重用的 . (迭代器不是)实际上不是真的,谢谢Jon

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

  • 2

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

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

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

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

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

  • 4

    那怎么样?

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

  • 8

    产量有两个很大的用途

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

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

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

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

  • 10

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

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

  • 1

    作为理解何时应该使用 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 ,它重新排列,有点工作"in parallel":

    //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,因为你已经有了完整的列表 .

  • -3

    yield return keyphrase用于维护特定集合的状态机 . 只要CLR看到正在使用的yield return keyphrase,CLR就会对该段代码实现Enumerator模式 . 这种类型的实现有助于开发人员处理所有类型的管道,否则我们将缺少关键字 .

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

    更多关于the keyword here at this article .

  • 10

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

  • 10

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

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

    Note: 我不会这样做的可能需要一段时间才能完成工作的方法 .

相关问题