Iteration Statements

An iteration statement allows a sequence of other statements to be executed several times. (Repeated execution is also often known as a loop because, like the race car, the code goes round and round again.) This seems like it could be useful in our race data analysis—race cars usually complete many laps, so we will probably have multiple sets of data to process. It would be annoying to have to write the same code 60 times just to process all the data for a 60-lap race. Fortunately, we don’t have to—we can use one of C#’s iteration statements.

Imagine that instead of passing in timing or fuel information as command-line arguments, the data was in files. We might have a text file containing one line per lap, with the elapsed time at the end of each lap. Another text file could contain the remaining fuel at the end of each lap. To illustrate how to work with such data, we’ll start with a simple example: finding the lap on which our driver went quickest.

Since this code is a little different from the previous example, start a new project if you want to follow along. Make another console application called LapAnalysis.

To be able to test our code we’ll need a file containing the timing information. You can add this to your Visual Studio project. Right-click on the LapAnalysis project in the Solution Explorer and select AddNew Item from the context menu. (Or just press Ctrl-Shift-A.) In the Installed Templates section on the left, select the General category under Visual C# Items, and then in the central area select Text File. Call the file LapTimes.txt and click Add. You’ll need this file to be somewhere the program can get to. Go to the Properties panel for the file—this is usually below the Solution Explorer panel, but if you don’t see it, right-click on LapTimes.txt in the Solution Explorer and select Properties. In the Properties panel, you should see a Copy to Output Directory property. By default, this is set to “Do not copy”. Change it to “Copy if newer”—Visual Studio will ensure that an up-to-date copy of the file is available in the bin\Debug folder in which it builds your program. You’ll need some data in this file. We’ll be using the following—these numbers represent the elapsed time in seconds since the start of the race at the end of each lap:

78.73
157.2
237.1
313.8
390.7
470.2

The program is going to read in the contents of the file. To do this, it’ll need to use types from the System.IO namespace, so you’ll need to add the following near the top of your Program.cs file:

using System.IO;

Then inside the Main method, use the following code to read the contents of the file:

string[] lines = File.ReadAllLines("LapTimes.txt");

The File type is in the System.IO namespace, and its ReadAllLines method reads in all the lines of a text file and returns an array of strings (string[]) with one entry per line. The easiest way to work through all these entries is with a foreach statement.

A foreach statement executes a block of statements once for every item in a collection such as an array. For example, this:

foreach (string line in lines)
{
    Console.WriteLine(line);
}

will display every line of text from the lines array we just built. The block to execute each time around is, as ever, delimited by a { } pair.

We have to provide the C# compiler with two things at the start of a foreach loop: the variable we’d like to use to access each item from the collection, and the collection itself. The string line part declares the first bit—the so-called iteration variable. And then the in lines part says that we want to iterate over the items in the lines array. So each time around the loop, line will contain the next string in lines.

We can use this to discover the fastest lap time, as shown in Example 2-12.

The currentLapStartTime begins at zero, but is updated to the end time of the previous lap each time around the loop—we need this to work out how long each lap took, because each line of the file contains the total elapsed race time at each lap. And the fastestLapTime variable contains the time of the fastest lap yet found—it’ll be updated each time a faster lap is found. (We also update it when it’s zero, which it will be the first time we go around.)

This finds the fastest lap time—76.7 seconds in the example data we’re using. But it doesn’t tell us which lap that was. Looking at the numbers, we can see that it happens to be the fourth, but it would be nice if the program could tell us. One way to do this is to declare a new variable called lapNumber, initializing it to 1 outside the loop, and adding one each time around, to keep track of the current lap. Then we can record the lap number on which we found the fastest time. Example 2-13 shows a modified version, with the additional code in bold.

If you’re trying this out, this might be a good opportunity to acquaint yourself with Visual Studio’s debugging features—see the sidebar below.

Example 2-13 works well enough, but there’s an alternative iteration statement you can use for this sort of scenario: a for statement.

A for statement is a loop in which some variable is initialized to a start value, and is modified each time around the loop. The loop will run for as long as some condition remains true—this means a for loop does not necessarily have to involve a collection, unlike a foreach loop. Example 2-14 is a simple loop that counts to 10.

The for keyword is followed by parentheses containing three pieces. First, a variable is declared and initialized. Then the condition is specified—this particular loop will iterate for as long as the variable i is less than or equal to 10. You can use any Boolean expression here, just like in an if statement. And finally, there is a statement to be executed each time around the loop—adding one to i in this case. (As you saw earlier, i++ adds one to i. We could also have written i += 1, but the usual if arbitrary convention in C-style languages is to use the ++ operator here.)

We could use this construct as an alternative way to find the fastest lap time, as shown in Example 2-15.

This is pretty similar to the foreach example. It’s marginally shorter, but it’s also a little more awkward—our program is counting the laps starting from 1, but arrays in .NET start from zero, so the line that parses the value from the file has the slightly ungainly expression lines[lapNumber - 1] in it. (Incidentally, this example avoids using a short iteration variable name such as i because we’re numbering the laps from 1, not 0—short iteration variable names tend to be associated with zero-based counting.) Arguably, the foreach version was clearer, even if it was ever so slightly longer. The main advantage of for is that it doesn’t require a collection, so it’s better suited to Example 2-14 than Example 2-15.

C# offers a third kind of iteration statement: the while loop. This is like a simplified for loop—it has only the Boolean expression that decides whether to carry on looping, and does not have the variable initialization part, or the statement to execute each time around. (Or if you prefer, a for loop is a fancy version of a while loop—neither for nor foreach does anything you couldn’t achieve with a while loop and a little extra code.) Example 2-16 shows an alternative approach to working through the lines of a text file based on a while loop.

The while statement is well suited to the one-line-at-a-time approach. It doesn’t require a collection; it just loops until the condition becomes false. In this example, that means we loop until the StreamReader tells us we’ve reached the end of the file.[8] (Chapter 11 describes the use of types such as StreamReader in detail.) The exclamation mark (!) in front of the expression means not—you can put this in front of any Boolean expression to invert the result. So the loop runs for as long as we are not at the end of the stream.

Note

We could have used a for loop to implement this one-line-at-a-time loop—it also iterates until its condition becomes false. The while loop happens to be a better choice here simply because in this example, we have no use for the variable initialization or loop statement offered by for.

The approach in Example 2-16 would be better than the previous examples for a particularly large file. The code can start working straight away without having to wait for the entire file to load, and it will use less memory because it doesn’t build the array containing every single line—it can hold just one line at a time in memory. For our example lap time file with just six lines of data, this won’t make any difference, but if you were processing a file with hundreds of thousands of entries, this while-based example could provide noticeably better performance than the array-based examples.

Warning

This does not mean that while is faster than for or foreach. The performance difference here is a result of the code working with the file in a different way, and has nothing to do with the loop construct. In general, it’s a bad idea to focus on which language features are “fastest.” Performance usually depends on the way in which your code solves a problem, rather than which particular language feature you use.

Note that for and while loops might never execute their contents at all. If the condition is false the first time around, they’ll skip the loop entirely. This is often desirable—if there’s no data, you probably want to do no work. But just occasionally it can be useful to write a loop that is guaranteed to execute at least once. We can do this with a variation on the while loop, called the do while loop:

do
{
    Console.WriteLine("Waiting...");
}
while (DateTime.Now.Hour < 8);

The while keyword and condition come at the end, and we mark the start of the loop with the do keyword. This loop always executes at least once, testing the condition at the end of each iteration instead of the start. So this code will repeatedly show the message “Waiting...” until the current time is 8:00 a.m. or later. If it’s already past 8:00 a.m., it’ll still write out “Waiting...” once.

It can sometimes be useful to abandon a loop earlier than its natural end. In the case of a foreach loop, this might mean stopping before you’ve processed every item in the collection. With for or while loops, you get to write the loop condition so that you can stop under whatever conditions you like, but it can sometimes be more convenient to put the code that makes a decision to abandon a loop somewhere inside the loop body rather than in the condition. For these eventualities, C# provides the break keyword.

We saw break already in a switch statement in Example 2-11—we used it to say that we’re done with the switch and want to break out of that statement. The break keyword does the same thing in a loop:

using (StreamReader times = File.OpenText("LapTimes.txt"))
{
    while (!times.EndOfStream)
    {
        string line = times.ReadLine();
        if (line == "STOP!")
        {
            break;
        }
        double lapEndTime = double.Parse(line);
        Console.WriteLine(lapEndTime);
    }
}

This is the loop from Example 2-16, modified to stop if it comes across a line in the input file that contains the text “STOP!” This breaks out immediately, abandoning the rest of the loop and leaping straight to the first line of code after the enclosing loop’s closing brace. (In that case, this happens to be the enclosing using statement’s closing brace, which will close the file handle.)

Note

Some people regard this use of break as bad practice. It makes it harder to understand the loop. When a loop contains no break statements, you can understand its lifetime by looking at the while (or for, or foreach) part. But if there are break statements, you need to look at more of the code to get a complete understanding of when the loop will finish.

More generally, flow control that jumps suddenly out of the middle of a construct is frowned upon, because it makes it much harder for someone to understand how execution flows through a program, and programs that are hard to understand tend to be buggy. The computer scientist Edsger Dijkstra submitted a short letter on this topic in 1968 to an academic journal, which was printed under a now infamous heading, “Go-to statement considered harmful”. If you’re interested in iconic pieces of computing history, or if you’d like a detailed explanation of exactly why this sort of jumpy flow control is problematic, you can find the original letter at http://www.cs.utexas.edu/users/EWD/ewd02xx/EWD215.PDF.

To recap what we’ve explored so far, we’ve seen how to work with variables to hold information, how to write expressions that perform calculations, how to use selection statements that decide what to do, and how to build iteration statements that can do things repeatedly. There’s one more basic C# programming feature we need to look at to cover the most important everyday coding features: methods.



[8] You’ll have noticed the using keyword on the line where we get hold of the StreamReader. We use this construct when it’s necessary to indicate exactly when we’ve finished with an object—in this case we need to say when we’re done with the file to avoid keeping operating system file handles open.