哪种方法可以终止阅读循环?


13

当您需要遍历读取器的地方时,要读取的项目数是未知的,唯一的方法是保持读取直到结束。

这通常是您需要无限循环的地方。

  1. 还有就是始终true表示必须有一个breakreturn声明的某处块内。

    int offset = 0;
    while(true)
    {
        Record r = Read(offset);
        if(r == null)
        {
            break;
        }
        // do work
        offset++;
    }
    
  2. 读for循环方法。

    Record r = Read(0);
    for(int offset = 0; r != null; offset++)
    {
        r = Read(offset);
        if(r != null)
        {
            // do work
        }
    }
    
  3. 有单个read while循环。并非所有语言都支持此方法

    int offset = 0;
    Record r = null;
    while((r = Read(++offset)) != null)
    {
        // do work
    }
    

我想知道哪种方法最不可能引入错误,且可读性最高且常用。

每次我必须写其中之一时,我认为“必须有更好的方法”


2
为什么要保持补偿?大多数Stream Readers都不允许您简单地“下一步阅读”吗?
罗伯特·哈维

我目前需要的@RobertHarvey读者拥有一个基础SQL查询,该查询使用偏移量对结果进行分页。我不知道查询结果要等多久才能返回空结果。但是,对于这个问题,这并不是真正的要求。
Reactgular 2013年

3
您似乎很困惑-问题标题与无穷循环有关,但问题文本与终止循环有关。经典的解决方案(从结构化编程的时代开始)是进行预读,在有数据时循环,然后作为循环中的最后一个动作再次读取。它很简单(满足错误要求),最常见(因为已经编写了50年)。最可读的是见解。
andy256

@ andy256对我来说是咖啡前的状况。
Reactgular 2013年

1
嗯,所以正确的程序是1)避免键盘时喝咖啡,2)开始编码循环。
andy256

Answers:


49

我会退后一步。您将注意力集中在代码的挑剔细节上,但却错过了大图。让我们看一下您的示例循环之一:

int offset = 0;
while(true)
{
    Record r = Read(offset);
    if(r == null)
    {
        break;
    }
    // do work
    offset++;
}

该代码的含义是什么?含义是“对文件中的每个记录做一些工作”。但这不是代码的样子。该代码看起来像“保持偏移量。打开文件。输入没有结束条件的循环。读取记录。测试是否为空。” 在我们开始工作之前,所有这些!您应该问的问题是“ 如何使此代码的外观与其语义相匹配? ”此代码应为:

foreach(Record record in RecordsFromFile())
    DoWork(record);

现在,代码读起来像它的意图一样。 将机制与语义分开。在原始代码中,您将机制-循环的详细信息-以及语义-混合了对每条记录所做的工作。

现在我们必须执行RecordsFromFile()。最好的实现方法是什么? 谁在乎?那不是任何人都会看的代码。它是基本的机制代码,有十行。随便写。这个怎么样?

public IEnumerable<Record> RecordsFromFile()
{
    int offset = 0;
    while(true)
    {
        Record record = Read(offset);
        if (record == null) yield break;
        yield return record;
        offset += 1;
    }
}

现在,我们正在处理懒散计算的记录序列,各种情况都成为可能:

foreach(Record record in RecordsFromFile().Take(10))
    DoWork(record);

foreach(Record record in RecordsFromFile().OrderBy(r=>r.LastName))
    DoWork(record);

foreach(Record record in RecordsFromFile().Where(r=>r.City == "London")
    DoWork(record);

等等。

每当您编写一个循环时,都要问自己“这个循环是像机制还是代码的含义?” 如果答案是“像一种机制”,则尝试将该机制移至其自己的方法,并编写代码以使含义更明显。


3
+1终于是一个明智的答案。这正是我要做的。谢谢。
Reactgular 2013年

1
“尝试将该机制移至其自己的方法” –听起来很像Extract Method重构,不是吗?“将片段转换为一种方法,其名称说明该方法的用途。”
gnat 2013年

2
@gnat:我的建议比“提取方法”要复杂得多,我认为这只是将一大堆代码移到另一个地方。提取方法绝对是使代码更像其语义一样读的好步骤。我建议在进行方法提取时应谨慎考虑,以期保持政策和机制的分离。
埃里克·利珀特

1
@gnat:是的!在这种情况下,我要提取的详细信息是一种在维护策略的同时从文件中读取所有记录的机制。政策是“我们必须对每条记录做一些工作”。
埃里克·利珀特

1
我知道了。这样,就更易于阅读和维护。学习此代码,我可以分别关注策略和机制,这不会迫使我分散注意力
gnat 2013年

19

您不需要无限循环。在C#读取方案中,您永远不需要一个。这是我的首选方法,假设您确实需要保持偏移量:

Record r = Read(0);
offset=1;
while(r != null)
{
    // Do work
    r = Read(offset);
    offset++
}

这种方法承认存在以下事实:读取器有一个设置步骤,因此有两个读取方法调用。该while条件位于循环的顶部,以防万一读取器中根本没有数据。


6

好吧,这取决于您的情况。但是我能想到的更多“ C#ish”解决方案之一是使用内置的IEnumerable接口和foreach循环。IEnumerator的接口仅使用true或false调用MoveNext,因此大小可能是未知的。然后,将终止逻辑写入一次(在枚举器中),而不必重复多个位置。

MSDN提供了IEnumerator <T>示例。您还需要创建IEnumerable <T>以返回IEnumerator <T>。


你能举一个例子吗?
Reactgular

谢谢,这是我决定要做的。虽然我不认为每次遇到无限循环都创建新类可以解决问题。
Reactgular 2013年

是的,我知道您的意思-很多开销。迭代器的真正天才/问题在于,它们肯定设置为能够同时对一个集合进行两件事迭代。所以,很多时候你不需要这些功能,所以你可能只是周围的对象同时实现IEnumerable和IEnumerator的包装。另一种看待方式是,您要迭代的任何基础底层内容的收集都没有考虑到公认的C#范例。没关系。迭代器的另一个好处是,您可以免费获得所有并行的LINQ东西!
J特拉纳

2

当我进行初始化,条件和增量操作时,我喜欢使用诸如C,C ++和C#之类的语言的for循环。像这样:

for (int offset = 0, Record r = Read(offset); r != null; r = Read(++offset)){
    // loop here!
}

或者,如果您认为这更具可读性,则可以这样做。我个人更喜欢第一个。

for (int offset = 0, Record r = Read(offset); r != null; offset++, r = Read(offset)){
    // loop here!
}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.