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 Add→New 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.
Example 2-12. Finding the fastest lap with foreach
string[] lines = File.ReadAllLines("LapTimes.txt"); double currentLapStartTime = 0; double fastestLapTime = 0; foreach (string line in lines) { double lapEndTime = double.Parse(line); double lapTime = lapEndTime - currentLapStartTime; if (fastestLapTime == 0 || lapTime < fastestLapTime) { fastestLapTime = lapTime; } currentLapStartTime = lapEndTime; } Console.WriteLine("Fastest lap time: " + fastestLapTime);
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.
Example 2-13. Fastest lap including lap number
string[] lines = File.ReadAllLines("LapTimes.txt"); double currentLapStartTime = 0; double fastestLapTime = 0; int lapNumber = 1; int fastestLapNumber = 0; foreach (string line in lines) { double lapEndTime = double.Parse(line); double lapTime = lapEndTime - currentLapStartTime; if (fastestLapTime == 0 || lapTime < fastestLapTime) { fastestLapTime = lapTime; fastestLapNumber = lapNumber; } currentLapStartTime = lapEndTime; lapNumber += 1; } Console.WriteLine("Fastest lap: " + fastestLapNumber); Console.WriteLine("Fastest lap time: " + fastestLapTime);
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.
Example 2-14. Counting with a for loop
for (int i = 1; i <= 10; i++) { Console.WriteLine(i); } Console.WriteLine("Coming, ready or not!");
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.)
Earlier we recommended using variable names that are long enough
to be descriptive, so you might be raising an eyebrow over the use of
i
as a variable name. There’s a
convention with for
loops where the
iteration variable just counts up from zero—short
variable names such as i
, j
, k
,
x
, and y
are often used. It’s not a universal
convention, but you’ll see it widely used, particularly with short
loops.
We’re using this convention in Example 2-14 only because you will come across it sooner or later, and so we felt it was important to show it. But it’s arguably not an especially good way to write clear code, so feel free to choose more meaningful names in your own code.
We could use this construct as an alternative way to find the fastest lap time, as shown in Example 2-15.
Example 2-15. Finding the fastest lap with for
string[] lines = File.ReadAllLines("LapTimes.txt"); double currentLapStartTime = 0; double fastestLapTime = 0; int fastestLapNumber = 0; for (int lapNumber = 1; lapNumber <= lines.Length; lapNumber++) { double lapEndTime = double.Parse(lines[lapNumber - 1]); double lapTime = lapEndTime - currentLapStartTime; if (fastestLapTime == 0 || lapTime < fastestLapTime) { fastestLapTime = lapTime; fastestLapNumber = lapNumber; } currentLapStartTime = lapEndTime; } Console.WriteLine("Fastest lap: " + fastestLapNumber); Console.WriteLine("Fastest lap time: " + fastestLapTime);
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.
Example 2-16. Iterating through a file with a while loop
static void Main(string[] args) { using (StreamReader times = File.OpenText("LapTimes.txt")) { while (!times.EndOfStream) { string line = times.ReadLine(); double lapEndTime = double.Parse(line); Console.WriteLine(lapEndTime); } } }
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.
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.
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.)
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.