ChatGPT解决这个技术问题 Extra ChatGPT

正确使用“收益回报”

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>();
    }
}
yieldIEnumerable<T> 及其类型相关联。它在某种程度上是懒惰的评估
这是对类似问题的一个很好的答案。 stackoverflow.com/questions/15381708/…
这是一个很好的用法示例:stackoverflow.com/questions/3392612/…
如果迭代 GetAllProducts() 结果的代码允许用户有机会提前取消处理,我认为使用 yield return 是一个很好的案例。
我发现这个帖子真的很有帮助:programmers.stackexchange.com/a/97350/148944

a
abelenky

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

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

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

另一种优选收益回报的情况是 IEnumerable 表示无限集。考虑素数列表,或无限的随机数列表。您永远无法一次返回完整的 IEnumerable,因此您使用 yield-return 逐步返回列表。

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


我会挑剔,在您的问题 3 中的示例中将两个好处混为一谈。 1)它分散了计算成本(有时是好处,有时不是) 2)它可以在许多用例中无限期地懒惰地避免计算。您没有提到它保持中间状态的潜在缺点。如果您有大量的中间状态(例如用于重复消除的 HashSet),那么使用 yield 会增加您的内存占用。
此外,如果每个单独的元素都非常大,但它们只需要按顺序访问,则产量会更好。
最后... 有一种稍微有点古怪但偶尔有效的技术可以使用 yield 以非常序列化的形式编写异步代码。
另一个可能有趣的例子是读取相当大的 CSV 文件时。您想读取每个元素,但也想提取您的依赖关系。 Yield 返回 IEnumerable<> 将允许您返回每一行并单独处理每一行。无需将 10 Mb 文件读入内存。一次只有一行。
Yield return 似乎是编写您自己的自定义迭代器类(实现 IEnumerator)的简写。因此,上述好处也适用于自定义迭代器类。无论如何,两种构造都保持中间状态。在其最简单的形式中,它是关于持有对当前对象的引用。
A
Ajay

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


我完全清楚这个答案不是技术答案,但我相信在理解 yield 关键字时,yield 和视频流之间的相似之处可以作为一个很好的例子。关于这个主题的所有技术都已经说过,所以我试图“换句话说”解释。是否有一条社区规则规定您不能用非技术术语解释您的想法?
K
Kache

作为理解何时应该使用 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 yield & Consume
Produce consumable[1]                   // ConsumeLoop iterates, requesting next item
Consume consumable[1]                   // consume next
Produce consumable[2]
Consume consumable[2]                   // consume next
Produce consumable[3]
Consume consumable[3]                   // consume next

最后,正如之前许多人已经建议的那样,您应该使用第 2 版,因为无论如何您已经有了完整的列表。


A
Adam W. McKinley

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

注意:不要认为 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; }
}

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

希望这可以帮助某人!我真的很喜欢探索使用 yield 的不同方式。


您的代码示例是关于如何在 for 或 foreach 块中使用 yield OUTSIDE 的绝佳示例。大多数示例显示迭代器内的收益返回。非常有帮助,因为我正要问关于 SO 如何在迭代器之外使用 yield 的问题!
我从来没有想过以这种方式使用 yield。这似乎是模拟 async/await 模式的一种优雅方式(如果今天重写,我假设将使用它而不是 yield)。自从您回答这个问题以来,随着 C# 的发展,您是否发现这些年来 yield 的这些创造性使用已经产生(不是双关语)收益递减?或者你还在想出像这样的现代化聪明用例吗?如果是这样,您介意为我们分享另一个有趣的场景吗?
简而言之,这是响应式编程(只是不太优雅和灵活)。
p
pogosama

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

    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 毫秒,完全没有内存消耗。


在幕后什么是产量?我会认为这是一个列表,因此它将如何提高内存使用率?
@rolls yield 通过在内部实现状态机在幕后工作。 Here's an SO answer with 3 detailed MSDN blog posts 非常详细地解释了实现。由 Raymond Chen @ MSFT 撰写
m
maples

这就是 Chris SellsThe C# Programming Language 中这些语句的说明;

我有时会忘记 yield return 与 return 不同,因为 yield return 之后的代码可以执行。比如这里第一次return之后的代码永远无法执行: int F() { return 1;返回 2; // 永远无法执行 } 相比之下,这里第一次yield return之后的代码是可以执行的: IEnumerable F() { yield return 1;收益回报2; // 可以执行 } 这在 if 语句中经常让我感到厌烦: IEnumerable F() { if(...) { yield return 1; // 我的意思是这是唯一返回的东西 } yield return 2; // 哎呀!在这些情况下,记住 yield return 不像 return 那样“最终”是有帮助的。


为了减少歧义,请澄清您何时说可以,那是,将还是可能?第一个返回而不执行第二个收益是否有可能?
@JohnoCrawford 只有在枚举 IEnumerable 的第二个/下一个值时才会执行第二个 yield 语句。它完全有可能不会,例如 F().Any() - 这将在尝试仅枚举第一个结果后返回。通常,您不应依赖 IEnumerable yield 来更改程序状态,因为它实际上可能不会被触发
J
Jason Baker

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

这个问题没有正确或错误的答案。哪个更可取取决于情况。例如,如果您必须完成查询的时间有限,并且您需要对结果做一些半复杂的事情,那么第二个版本可能更可取。但要注意大型结果集,尤其是在 32 位模式下运行此代码时。在执行此方法时,我多次被 OutOfMemory 异常所困扰。

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


S
Shivprasad Koirala

产量有两个很好的用途

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

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

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

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


S
Soviut

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

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


M
Mark A. Nicolosi

这有点无关紧要,但由于这个问题被标记为最佳实践,我会继续投入我的两分钱。对于这种类型的东西,我非常喜欢将它变成一个属性:

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

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


p
petr k.

那这个呢?

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


s
shA.t

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

如果这个方法的调用者一次需要“一个”信息,而下一个信息的消费是按需的,那么使用yield return是有益的,它可以确保执行命令在何时返回给调用者一个信息单元可用。

可以使用 yield return 的一些示例是:

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

为了回答你的问题,我会使用版本 2。


r
recursive

直接返回列表。好处:

更清楚

该列表是可重复使用的。 (迭代器不是)实际上不是真的,谢谢乔恩

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


不要忘记它返回的是 IEnumerable,而不是 IEnumerator - 您可以再次调用 GetEnumerator。
即使您事先知道需要计算整个列表,使用收益回报率仍然可能是有益的。一个例子是当集合包含数十万个项目时。
d
dns

鉴于确切的两个代码片段,我认为版本 1 更好,因为它可以更有效。假设有很多产品,调用者想要转换为 DTO。

var dtos = GetAllProducts().Select(ConvertToDto).ToList();

在版本 2 中,首先会创建一个 Product 对象列表,然后是另一个 ProductDto 对象列表。在版本 1 中,没有 Product 对象列表,只构建了所需的 ProductDto 对象列表。

即使没有转换,我认为版本 2 也存在一个问题:列表作为 IEnumerable 返回。 GetAllProducts() 的调用者不知道枚举结果的代价有多大。如果调用者需要多次迭代,她可能会使用 ToList() 实现一次(ReSharper 等工具也建议这样做)。这会导致已经在 GetAllProducts() 中创建的列表的不必要副本。因此,如果应使用版本 2,则返回类型应为 List 而不是 IEnumerable。


1
123

yield 的用法与关键字 return 类似,只是它会返回一个 generatorgenerator 对象只会遍历 一次

产量有两个好处:

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

还有另一个明确的explanation可能对您有所帮助。