In the first installment of C is for Cocoa, we moved from one line of code to two lines of code, pointing out that the second one would be executed directly after the first, like items being crossed off of a to-do list. Our program knows to do the things we tell it to do in the order that we tell it. In lesson 2, we learned how to tweak this simple list approach by making single lines of code call functions where many more lines of code might be found. Our program knows that some of the things we tell it require more than one step.
Although we didn't make note of it at the time, both of these demonstrate the same concept, which is the idea that we'll be tackling today, usually called the flow of control. In our first program, as each line of code was found in order, that line was where the code program "was", and it got executed before the program would move on to the next line. Once we added functions to the mix, the control could move "into" those, and again would take the code on one line at a time, making sure to execute every line of code before it was done.
|
Related Reading
Learning Cocoa with Objective-C |
But sometimes we don't want it to execute every line of code. Sometimes we want it to execute this line of code when the computer is connected to a network, and this line if it's not. Sometimes we want the computer to choose from a whole range of things to do, depending on one circumstance. Sometimes we want the program to do something a whole lot of times, like while the user is idle, or as many times as there were key presses.
In all of these cases, we want to change the flow of control in our program. We want the computer to do something that's not just a simple list of directions, each to be done once and checked off, never to be looked at again. We want our program to do what makes sense given its circumstances, and not bother with the rest. This is what we'll be looking at in this lesson.
But before we do that, we need to learn how to say no. When you ask a simple question, you expect a simple answer. But as we learned in lesson 1, you can't ask the computer a question, and your code certainly won't ask you any. Your program might ask the user questions, but the programmer is left in the position of uttering only imperatives forever and ever. The solution to all of this is borrowed from an old Irish schoolteacher named George Boole, who invented a way to express "yes" and "no" called boolean logic. It is traditional in C to refer to these as TRUE and FALSE (note the case: you must use all caps or it won't work), but Cocoa traditionally uses YES and NO. In this lesson, we'll use the C tradition because they stand out more in prose.
Now you might think that the best way to do this would be to make a
boolean type that handles TRUE/FALSE distinctions. And indeed in a
perfect world that's exactly how this would be handled. In newer
languages, that's what's done. In C, however, we cheat a
little. Instead of actually making a type that handles this, we use a
type we already have and pretend. So we take our familiar
int and whenever it's a zero, we read that as FALSE; when
it's not, we read that as TRUE. This might seem a little odd, but it
actually leads to a number of simplifications later on, which we'll
see in later lessons. But now let's head into this lesson and learn to
control our flow.
The simplest type of flow control structure is the if block.
It's so simple that you probably don't need much help dissecting the
following code block:
int integerForSeedValue(int seedNumber) {
int retval = 0;
if (1) {
printf("This is always true.\n");
retval = seedNumber - 3;
}
return retval;
}
You should use this and later examples in your code to modify the
integerForSeedValue
function we wrote in the last lesson. Each of the following code
blocks will work and do something slightly different. As we discuss
each, run your program with the newest code.
I've added a declaration for the variable retval,
which is the value that the function will return (this
is a common name for such a variable). Also note that the variable is
returned at the end of the function. The middle is where the new conditional
is.
Here we've added if, and then something in parentheses,
and then a block. The
key to it all is that something in the middle, in parentheses. That is
called our condition,
and if it's TRUE, the code in the block gets executed, and the
function returns seedNumber -3. If it's not, then the
computer skips the block entirely, and the function returns the
0 that retval is set to in its declaration.
Since our condition here is the constant
1, the block is always executed. Let's make the code a little more
interesting:
int integerForSeedValue(int seedNumber) {
int retval = 0;
int cond = 17 < 23;
if (cond) {
printf("What do you know: 17 *is* less than 23!\n");
retval = seedNumber - 3;
}
return retval;
}
We added a line of code that seems familiar to us: it's a
variable
declaration that creates a variable named cond. But
how it does that is a little new. Instead of the familiar arithmetic
operators,
we have the comparison
operator "<", the less-than
operator, which you remember from grade school when you were
learning which numbers were bigger than others. Like all operators, it
operates on the things around it. If it was an addition sign, it would
add the two things around it, but since it's the less-than operator,
it compares them. If the thing on the left is less than the thing on
the right, it evaluates to a boolean value for TRUE. Since 17 is
always less than 23, the first line sets cond to
TRUE. Then, when we get to our if condition, since cond
is true, the block is executed. If you switched the operator to be
the
greater-than operator ">", cond would
be FALSE and the if block would not be executed.
What would be executed in that case is the optional else
block, which we can see in this code example:
int integerForSeedValue(int seedNumber) {
int retval = 0;
int cond = 17 > 23; //note we're now using the greater-than operator
if (cond) {
printf("What do you know: 17 *is* greater than 23!\n");
retval = seedNumber - 3;
} else {
printf("Well lookie here: 17 is *less than* 23!\n");
retval = seedNumber * 3;
}
return retval;
}
The else block will be executed whenever the if block is not
executed--and vice
versa. But note that neither of these need to be a block at all. You
can just put a single line of code after the if's
condition or the else, and that line of code will be
executed in lieu of a block:
int integerForSeedValue(int seedNumber) {
int retval = 0;
int cond = 17 > 23;
//no printf's here; only one line statements
if (cond) retval = seedNumber - 3;
else retval = seedNumber * 3;
return retval;
}
It's a good practice to always include the block, because leaving it out can lead to headaches later on if you want to add code to either side of the conditional and you forget to add the brackets then. You're usually better off making a habit of always putting the brackets in whether you need them right away or not.
But it is often the case that the logic you need is more complex than
a simple this or that choice. It's often this, or that, or this other
thing. In that case, we chain our if/else
statements together to allow more and more decisions, like this:
int integerForSeedValue(int seedNumber) {
int retval = 0;
int cond = 17 > 23;
if (cond) {
printf("Amazing! 17 is *greater than* 23! My life has changed\n");
retval = seedNumber - 3;
} else if ((seedNumber < 5) && (seedNumber > -5)) {
printf("That there seed number is real close to zero!\n");
retval = seedNumber * 2;
} else {
printf("No special cases here; do what we normally do!\n");
return = seedNumber * 3;
}
return retval;
}
The second if statement is only tested if the first
fails, which it always will since 17 is never greater than 23. But
here we also do two important new things. First, our condition
includes the function's input. This, as you can imagine, is much more
useful than defining variables that will always be TRUE or FALSE as we
have done before. Secondly, we did some craziness with some ampersands
that's totally new.
What we're doing here is making a
compound condition. Now, instead of just checking one condition, we're
checking two: we want to make sure seedNumber is less
than 5 and greater than -5. That double-ampersand means boolean
and. Why not a single ampersand? That's used for something else,
which you won't need to care about for some time.
Go back over the examples of if/else blocks
and try them in your program; each one works a little differently, so
try each one if you haven't yet. In each example, watch what code does
and does not get executed to see the flow of control. When you're
done, we'll look at a different way to do the same type of things.
|
Some people are all about shortening the code they write. This can be
an admirable goal: less code can mean a faster operation and a simpler
more maintainable codebase. Sometimes brevity can make your code much
cleaner. Toward these goals C includes an operator that performs one
of the tasks that an if/else block is often
called in to perform: setting a variable to two different values based
on some boolean logic. This operator, called the ternary
operator, turns this code:
int integerForSeedValue(int seedNumber) {
int retval = 0;
if (seedNumber == 8) {
retval = seedNumber - 3;
} else {
retval = seedNumber * 3;
}
return retval;
}
Into this code:
int integerForSeedValue(int seedNumber) {
return (seedNumber == 8) ? (seedNumber - 3) : (seedNumber * 3);
}
That's quite a savings of space, but the second snippet looks like quantum physics. Let's take this line apart token by token.
We start with return, which we know and love from our last
lesson. Then we've got a bit in parentheses that is boolean
logic. This is the condition we're used to in a normal
if/else block, expressed in exactly the same
way. If we put a semicolon here, the result of the condition would be
returned. Instead we have a question mark, which tells the computer
that it just read a condition for a ternary operator, and the other
parts are next. The next bit in parentheses is what we want the
ternary operator to evaluate to if the condition is true. This is
first, just like the true condition is first in a normal
if/else block. Then we have a colon, which
tells the computer that we're done with the true case and moving on to
the false case. Here we put what we want the ternary operator to
evaluate to if the condition is false.
So you can read the question mark as "if the following is true, evaluate this" and the colon as "but if the condition was false, evaluate this instead." But note that you can only evaluate the cases, meaning multiple lines isn't an option, and each case must evaluate to be the proper type or you'll get a warning.
Another thing to note is that we've introduced a new operator,
"==",
the
equality conditional. This checks to see if the thing on the left
is equal to the thing on the right, and if they are it evaluates to
TRUE, otherwise to FALSE. Why isn't it a single equals sign? Because
we already use that token to set the value of variables and that means
we can't use it here.
When you're dealing with code that must decide between a few options,
if is the way to go. But if you need to use one input to
decide amongst a lot of things, Then it's much simpler to just use a
switch:
int integerForSeedValue(int seedNumber) {
int retval = 0;
switch (seedNumber) {
case 1:
printf("The Seed was one, not many.\n");
retval = seedNumber;
break;
case 8:
printf("We have an eightfold seed.\n");
retval = seedNumber * 2;
break;
}
return retval;
}
In this section, too, we will examine many different incarnations of
the integerForSeedValue function, and you should run your
program with each of them.
Let's pull the code apart. We start with the token switch,
then something in parentheses. After that is a block. But the stuff in
parentheses isn't a condition, it's the switch variable. This is the
variable whose value determines what gets done inside the switch
block.
Once we know this variable, we move through the switch block looking
for case lines with the same value, and ignoring all the
other code until we find one. When we do find one, we execute the code
after it until we hit a break statement, which pops us
out of the block. Note that hitting another case
statement will not end the block: a break is the only
thing that will stop it. That seems odd until you consider the
following:
int integerForSeedValue(int seedNumber) {
int retval = 0;
switch (seedNumber) {
case 1:
printf("Surreptitiously, a singular seed..\n");
retval = seedNumber;
break;
case 6:
case 7:
case 8:
printf("Our seed is 6, 7, or maybe even 8!\n");
retval = seedNumber * 2;
break;
}
return retval;
}
Which means that if seedNumber is 6, 7, or 8, retval
will be set to seedNumber * 2. In other words, you can
group cases together if you want them all to do the same
code.
But note that each case here is a constant integer. This is a very important
limitation in the way switch works: its cases can only be
constants and can't be boolean expressions, like
if/else blocks' conditions.
The switch syntax also provides a way to catch anything
you didn't account for. The following snippet is an example:
int integerForSeedValue(int seedNumber) {
int retval = 0;
switch (seedNumber) {
case 1:
printf("He is The One.\n");
retval = seedNumber;
break;
case 8:
printf("And the number of the seed shall be eight.\n");
retval = seedNumber * 2;
default:
printf("What a seed it is!\n");
retval++;
}
return retval;
}
We added a default case. The code here will be executed
if no matching case is found, or if no break
is found beforehand. Note that we took away the break in
case 8. This allows the default to be
executed when seedNumber is 8, which it is in our program
as written in Lesson 2.
But you might have noticed that the code in our default case is something
we haven't seen before. The "++" is yet another operator,
the
increment operator. It takes whatever is in retval and adds
one. It's sister the decrement
operator, "--", subtracts one.
Run all the different switch examples and see the results. Try to trace where the control flows.
We've learned three ways of changing how our program control flows, each of which is useful for different situations. We can now change code execution based on boolean logic, we can set variables based on the same logic, and our code can change how it works based on the value of variables.
But there's more. As useful as these structures are, they don't let you do the same thing over and over, which is the exclusive domain of loops, which we will cover in our next lesson.
Seth Roby graduated in May of 2003 with a double major in English and Computer Science, the Macintosh part of a three-person Macintosh, Linux, and Windows graduating triumvirate.
Return to MacDevCenter.com
Copyright © 2009 O'Reilly Media, Inc.