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>();
}
}
答案 0 :(得分:762)
当我计算列表中的下一个项目(甚至是下一组项目)时,我倾向于使用yield-return。
使用版本2,您必须在返回之前拥有完整列表。 通过使用yield-return,您实际上只需要在返回之前使用下一个项目。
除此之外,这有助于在更大的时间范围内分散复杂计算的计算成本。例如,如果列表连接到GUI并且用户永远不会转到最后一页,则永远不会计算列表中的最终项目。
另一种情况,其中yield-return是优选的,如果IEnumerable表示无限集。考虑素数列表,或无限的随机数列表。你永远不能一次返回完整的IEnumerable,所以你使用yield-return来逐步返回列表。
在您的特定示例中,您拥有完整的产品列表,因此我将使用版本2.
答案 1 :(得分:591)
填充临时列表就像下载整个视频一样,而使用yield
就像流式传输视频一样。
答案 2 :(得分:65)
作为理解何时应该使用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,因为你已经有了完整的列表。
答案 3 :(得分:26)
这似乎是一个奇怪的建议,但我通过阅读Python中关于生成器的演示文稿来学习如何在C#中使用yield
关键字:David M. Beazley的http://www.dabeaz.com/generators/Generators.pdf。您不需要了解太多Python来理解演示文稿 - 我没有。我发现它不仅有助于解释生成器的工作方式,还有解释为什么要关注。
答案 4 :(得分:25)
我知道这是一个老问题,但我想提供一个如何创造性地使用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();
}
这将是打开我的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; }
}
设置这样的东西并逐步执行以观察正在发生的事情可能会有所帮助。
希望这可以帮助别人!我非常喜欢探索可以使用产量的不同方法。
答案 5 :(得分:13)
对于需要遍历数百万个对象的算法,yield return非常强大。请考虑以下示例,您需要计算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,你不需要将所有这些临时对象放到内存中,你将获得明显更快的算法速度:这个例子在没有内存消耗的情况下运行只需要约400ms。
答案 6 :(得分:12)
这两段代码实际上做了两件不同的事情。第一个版本将根据您的需要拉取成员。第二个版本会将所有结果加载到内存中开始对它做任何事情。
这个答案没有正确或错误的答案。哪一个更可取仅取决于具体情况。例如,如果您必须完成查询的时间有限,并且您需要执行与结果半复杂的操作,则第二个版本可能更可取。但要注意大型结果集,特别是如果您在32位模式下运行此代码。在执行此方法时,我多次被OutOfMemory异常所困扰。
要记住的关键是:差异在于效率。因此,您可能应该选择使代码更简单的方法,并在分析后更改它。
答案 7 :(得分:11)
Yield有两个很好的用途
它有助于提供自定义迭代而无需创建临时集合。 (加载所有数据和循环)
有助于进行有状态迭代。 (流媒体)
下面是一个简单的视频,我已经完整演示,以支持上述两点
答案 8 :(得分:10)
这是Chris Sells在The C# Programming Language中讲述这些陈述的内容;
我有时会忘记收益率与收益率不一样 可以执行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! }
在这些情况下,记住收益率回报并不像“最终”那样 返回很有帮助。
答案 9 :(得分:8)
假设你的产品LINQ类使用类似的枚举/迭代产量,第一个版本效率更高,因为它每次迭代时只产生一个值。
第二个例子是使用ToList()方法将枚举器/迭代器转换为列表。这意味着它手动迭代枚举器中的所有项目,然后返回一个平面列表。
答案 10 :(得分: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);
VS
prices = Whatever.GetAllProducts().Select (product => product.price);
注意:对于任何可能需要一段时间才能完成工作的方法,我不会这样做。
答案 11 :(得分: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语句中使用),我会直接返回它。
答案 12 :(得分:5)
在这种情况下,我会使用代码的第2版。由于您拥有可用产品的完整列表,并且这是此方法调用的“使用者”所期望的内容,因此需要将完整信息发送回调用方。
如果此方法的调用者一次需要“一个”信息并且下一个信息的消耗是按需的,那么使用yield return将是有益的,这将确保执行命令将返回到当一个信息单位可用时,调用者。
可以使用收益率回报的一些例子是:
要回答你的问题,我会使用版本2.
答案 13 :(得分:3)
直接返回列表。优点:
当您认为可能不必一直迭代到列表末尾或者没有结束时,您应该使用迭代器(yield)。例如,客户端调用将搜索满足某些谓词的第一个产品,您可能会考虑使用迭代器,尽管这是一个人为的例子,并且可能有更好的方法来实现它。基本上,如果您事先知道需要计算整个列表,请事先做好。如果您认为它不会,那么请考虑使用迭代器版本。
答案 14 :(得分:1)
yield return keyphrase用于维护特定集合的状态机。只要CLR看到正在使用的yield return keyphrase,CLR就会对该段代码实现Enumerator模式。这种类型的实现有助于开发人员处理所有类型的管道,否则我们将缺少关键字。
假设开发人员正在过滤某个集合,通过集合进行迭代,然后在某个新集合中提取这些对象。这种管道非常单调。
有关the keyword here at this article的更多信息。
答案 15 :(得分:-3)
产量的使用类似于关键字返回,但它会返回generator。 生成器对象只会遍历一次。
产量有两个好处:
还有另一个明确的explanation可能会帮助你。