关于C#:正确使用yield return

Proper use of 'yield return'

yield关键字是C中继续迷惑我的关键字之一,我从未确信我正确使用它。

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

版本1:使用收益率返回

1
2
3
4
5
6
7
8
9
10
11
12
13
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:返回列表

1
2
3
4
5
6
7
8
9
10
public static IEnumerable<Product> GetAllProducts()
{
    using (AdventureWorksEntities db = new AdventureWorksEntities())
    {
        var products = from product in db.Product
                       select product;

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


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

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

除此之外,这有助于将复杂计算的计算成本分摊到更大的时间范围内。例如,如果列表连接到一个图形用户界面,而用户从未访问最后一页,则永远不会计算列表中的最后一项。

如果IEnumerable表示一个无限集,则收益率返回更可取。考虑素数列表,或无限随机数列表。不能一次返回完整的IEnumerable,因此使用yield return以递增方式返回列表。

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


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


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

1
2
3
4
5
6
7
8
9
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()的调用可能需要很长时间,因为您必须在返回之前完成列表:

1
2
3
4
5
6
7
8
9
//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,它会重新排列,有点"并行"工作:

1
2
3
4
5
6
7
8
9
//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,因为您已经拥有了完整的列表。


这似乎是一个奇怪的建议,但我通过阅读关于python中生成器的演示,了解了如何使用c中的yield关键字:david m.beazley的http://www.dabeaz.com/generators/generators.pdf。你不需要知道太多的python来理解这个演示——我没有。我发现它不仅有助于解释生成器是如何工作的,而且有助于解释为什么你应该关心它。


我知道这是一个老问题,但我想举一个例子说明如何创造性地使用yield关键字。我真的从这项技术中受益匪浅。希望这能对任何一个偶然发现这个问题的人有所帮助。

注意:不要认为yield关键字只是构建集合的另一种方法。屈服力的很大一部分来自这样一个事实:执行在你的方法或属性,直到调用代码迭代下一个值。下面是我的例子:

使用yield关键字(与rob eisenburg的caliburn.micro coroutines实现一起),我可以对如下Web服务表示异步调用:

1
2
3
4
5
6
7
8
9
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服务上的登录方法,将我的isloggedin标志设置为返回值,然后关闭busyindicator。

这是如何工作的:IResult有一个执行方法和一个完成的事件。micro从对handleButtonClick()的调用中获取IEnumerator,并将其传递给coroutine.beginExecute方法。BeginExecute方法开始迭代IResults。返回第一个IResult时,将在handleButtonClick()内暂停执行,BeginExecute()将事件处理程序附加到完成的事件并调用Execute()。execute()可以执行同步或异步任务,并在完成后激发完成的事件。

LoginResult看起来像这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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; }
}

它可能有助于设置类似这样的内容,并逐步执行以监视正在发生的事情。

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


这两段代码实际上在做两件不同的事情。第一个版本将根据需要拉成员。第二个版本将把所有的结果加载到内存中,然后再开始处理它。

这个答案没有对错之分。哪一个更好,这取决于具体情况。例如,如果您必须完成查询的时间有限,并且您需要对结果执行一些半复杂的操作,那么最好使用第二个版本。但要注意大型结果集,尤其是在32位模式下运行此代码时。在执行此方法时,我曾多次被内存不足异常咬伤。

但要记住的关键是:效率的差异。因此,您可能应该选择使代码更简单的代码,并且只有在分析之后才进行更改。


对于需要迭代数百万个对象的算法,yield返回可能非常强大。请考虑下面的示例,您需要在其中计算乘客共享的可能行程。首先,我们产生可能的旅行:

1
2
3
4
5
6
7
8
9
10
11
    static IEnumerable<Trip> CreatePossibleTrips()
    {
        for (int i = 0; i < 1000000; i++)
        {
            yield return new Trip
            {
                Id = i.ToString(),
                Driver = new Driver { Id = i.ToString() }
            };
        }
    }

然后迭代每次行程:

1
2
3
4
5
6
7
8
9
10
11
    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,则不需要将所有这些temp对象都放到内存中,而且算法速度会显著加快:这个示例只需要大约400毫秒就可以运行,而不需要消耗任何内存。


产量有两大用途

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

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

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

http://www.youtube.com/watch?V= 4FJU3XCM21M


这就是Chris销售的关于C语言编程语言中的语句的内容;

I sometimes forget that yield return is not the same as return , in
that the code after a yield return can be executed. For example, the
code after the first return here can never be executed:

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

In contrast, the code after the first yield return here can be
executed:

1
2
3
4
IEnumerable<int> F() {
yield return 1;
yield return 2; // Can be executed
}

This often bites me in an if statement:

1
2
3
4
5
IEnumerable<int> F() {
if(...) { yield return 1; } // I mean this to be the only
// thing returned
yield return 2; // Oops!
}

In these cases, remembering that yield return is not"final" like
return is helpful.


这有点离题了,但既然这个问题被贴上了"最佳实践"的标签,我会继续写我的两分钱。对于这种类型的东西,我非常喜欢把它变成一种财产:

1
2
3
4
5
6
7
8
9
10
11
public static IEnumerable<Product> AllProducts
{
    get {
        using (AdventureWorksEntities db = new AdventureWorksEntities()) {
            var products = from product in db.Product
                           select product;

            return products;
        }
    }
}

当然,这是一个有点锅炉板,但代码使用这将看起来更清洁:

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

VS

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

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


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

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


那这个呢?

1
2
3
4
5
6
7
8
9
10
public static IEnumerable<Product> GetAllProducts()
{
    using (AdventureWorksEntities db = new AdventureWorksEntities())
    {
        var products = from product in db.Product
                       select product;

        return products.ToList();
    }
}

我想这个要干净得多。不过,我手头没有VS2008。在任何情况下,如果产品实现IEnumerable(正如它看起来的那样——它在foreach语句中使用),我将直接返回它。


在本例中,我将使用代码的版本2。由于您有完整的可用产品列表,这也是此方法调用的"使用者"所期望的,因此需要将完整的信息发送回调用方。

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

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

  • 复杂的分步计算,调用者一次等待一个步骤的数据
  • 在GUI中分页-用户可能永远无法访问最后一页,并且只需要在当前页上公开子信息集。
  • 为了回答你的问题,我会使用版本2。


    直接返回列表。效益:

    • 更清楚
    • 列表是可重用的。(迭代器不是).not actually true,thanks jon

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


    yield-return关键字短语用于维护特定集合的状态机。当clr看到正在使用的yield返回关键字短语时,clr将对该代码段实现一个枚举器模式。这种类型的实现可以帮助开发人员处理所有类型的管道,否则在没有关键字的情况下,我们将不得不这样做。

    假设开发人员正在过滤某个集合,遍历该集合,然后在某个新集合中提取这些对象。这种管道很单调。

    关于本文中的关键字的更多信息。


    yield的用法类似于关键字return,只是它将返回生成器。而生成器对象只会遍历一次。

    收益有两个好处:

  • 您不需要读取这些值两次;
  • 您可以获得许多子节点,但不必将它们全部放到内存中。
  • 还有另一个明确的解释也许对你有帮助。