迭代器块的正确使用方法

8

我之前在重构一些代码时,发现了一个迭代器块的实现,我对此不太确定。在系统的集成层中,客户端调用外部API获取一些数据,我有一组翻译器,将从API返回的数据转换为逻辑层中使用的业务实体集合。通常情况下,翻译器类看起来像这样:

// translate a collection of entities coming back from an extrernal source into business entities
public static IEnumerable<MyBusinessEnt> Translate(IEnumerable<My3rdPartyEnt> ents) {

    // for each 3rd party ent, create business ent and return collection
    return from ent in ents
           select new MyBusinessEnt {
               Id = ent.Id,
               Code = ent.Code
           };
}

今天我遇到了以下代码。再一次,这是一个翻译器类,其目的是将参数中的集合转换为方法返回类型。然而,这次它是一个迭代器块:
// same implementation of a translator but as an iterator block
public static IEnumerable<MyBusinessEnt> Translate(IEnumerable<My3rdPartyEnt> ents) {
    foreach(var ent in ents)
    {
        yield return new MyBusinessEnt {
            Id = ent.Id,
            Code = ent.Code
        };
    }
}

我的问题是:这是否是迭代器块的有效用法?我看不出以这种方式创建翻译器类的好处。这样做会导致一些意外行为吗?


在我看来,它似乎是完全有效的——它提供了两个实体之间的编译安全翻译。它有什么问题吗? - Chris Shain
5个回答

16

你这两个示例基本上做的是完全一样的事情。查询版本将被重写为调用Select,并且Select就像你的第二个示例一样编写;它迭代源集合中的每个元素并产生一个经过转换的元素。

这是使用迭代器块的完全有效的用法,当然,现在已经不必像这样编写自己的迭代器块,因为可以直接使用Select。


3

没错,这是有效的。使用foreach的好处是可以进行调试,因此我更喜欢这种设计。


那么迭代器块可能会导致一些混淆,对于稍后在系统中尝试调试此业务实体集合的foreach循环的任何人来说? - james lewis
1
我认为并没有必要混淆; foreach 块只是使得针对正在被翻译的行项目标更容易。如果翻译变得不再是微不足道的,这会使得调试更加简单。两种实现都是完全正确的。 - eouw0o83hf

3

第一个示例不是迭代器。它只是创建并返回 IEnumerable<MyBusinessEnt>

第二个示例是迭代器,我没有看出任何问题。每次调用者迭代该方法的返回值时,yield 将返回一个新元素。


3

是的,这很好用,而且结果非常类似。

两者都创建了一个能够返回结果的对象。两者都依赖于源可枚举对象在结果完成(或被截断)之前保持原样。两者都使用延迟执行,也就是当您迭代结果时,对象会一次创建一个。

不同之处在于第一个返回使用库方法生成枚举器的表达式,而第二个创建了一个自定义枚举器。


好的,但在这个例子中没有必要创建自定义迭代器,对吧?正如@Aliostad所指出的那样,迭代器块最好与条件一起使用。因此,第二种方法可能稍微不太易读\易调试,因此第一种方法可能更适合这种情况。 - james lewis
另外一个区别是自定义枚举器会立即运行,而第一个(从那里选择这个)的运行会延迟。 - Aliostad
@Aliostad - 那个,我不太理解...所以第一个例子返回的是尚未枚举的集合。第二个例子在每次调用时都对返回的集合进行枚举?是这样吗? - james lewis
@jameslewis:LINQ 处理条件语句非常好。这就是 where 的作用。 - Brian
@Aliostad:第二个立即开始,但它只生成第一个项目。直到你消费它之前,它不会运行剩下的项目。 - Guffa
1
@jameslewis:yield return 基本上会暂停方法中的代码并返回该项,然后当您请求下一项时,代码将从该点继续执行,绕过循环,直到再次到达 yield return。因此,代码不会每次需要从源返回项目时都枚举源。 - Guffa

-1

主要区别在于每个代码运行的时间。第一个延迟到返回值被迭代时才运行,而第二个则立即运行。我的意思是,for循环强制迭代运行。该类公开了一个IEnumerable<T>,在这种情况下是延迟的,这是另一回事。

这与简单的Select没有任何优势。当涉及条件时,yield的真正威力才得以发挥:

foreach(var ent in ents)
{
    if(someCondition)
    yield return new MyBusinessEnt {
        Id = ent.Id,
        Code = ent.Code
    };
}

6
稍等片刻--你的版本与简单的“Where”和“Select”后跟没有任何优势。是的,Chris Shouts的评论是正确的;它们都推迟执行。 - Eric Lippert
@EricLippert 是的,因为他使用了循环而不是选择。但正如我所说,在这种情况下,我只会使用普通的Linq。 - Aliostad
3
第一个立即返回一个对象,该对象延迟执行直到调用MoveNext。第二个也“返回一个对象,该对象延迟其执行直到调用MoveNext”。它们都使用延迟执行。但是,第一个需要“两次跳跃”才能获取数据,而第二个只需要“一次跳跃”,性能差异可能很小。 - Eric Lippert
2
@Aliostad:我猜测你被“second one runs immediately.”这句话所误导了。这个陈述太容易被解释为第二个没有延迟执行了。 - Brian
这种误解被加粗的“主要区别”所加强...第一个是延迟... 这使得它听起来像是差异非常重要、根本性的,甚至是使用第二种方法的致命缺陷。Aliostad,你为什么在这里强调了这么多?需要澄清。如果我理解正确的话,当调用者想要使用可以稍后使用的IEnumerable时,这一点很重要;如果调用者立即开始枚举结果,那么没有有效的区别,对吧? - ToolmakerSteve
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接