Jump statements are used to transfer control from one location in your code to another. Unlike selection statements, jump statements transfer control unconditionally. Some jump statements, such as the return statement, allow you to pass additional context information that can be used when control is obtained at the new location. Other statements, such as break and goto, simply transfer control to a new location.
Jump statements also differ in the context in which they’re used. The break and continue statements are used to control the flow of execution within other constructions, such as loops or selection statements. In contrast, the goto, return, and throw statements can be used in almost any part of your program. Used properly, jump statements make your code more readable and maintainable. Used improperly, jump statements not only make your code difficult to maintain, they can also make your program less robust.
The break statement is used to break out of a control structure, such as a loop or a switch statement, as shown here:
int n = 0; while(true) { if(++n == 3) break; }
In this example, the while loop is terminated after three items have been displayed, even though the while statement would potentially allow the loop to run forever.
The most common use of the break statement is to terminate the processing of case labels in a switch statement, as follows:
switch(name) { case "Ali": PlaySoccer(); break; }
When multiple control structures are nested, a break statement will terminate processing of only the most immediate control structure. For example, if a while loop is located inside a foreach loop, a break statement will stop the processing of only the innermost loop, as shown here:
int [] rg = {1, 2, 3, 4, 5}; foreach(int i in rg) { int n = 1; int factorial = 1; while(true) { factorial *= n; if(n++ == i) break; } Console.WriteLine("{0}! is {1}", i, factorial); }
If you must completely break out of nested loops, you have two options: you can either use a goto statement, as will be discussed later in this chapter, in the section “The goto Statement,” or use a flag to indicate that control should break out of both loops.
The continue statement is used to return to the beginning of a loop statement, short-circuiting any further processing of the current iteration, as shown here:
for(int i = 0; i < 20; ++i) { if(i % 2 == 0) continue; Console.WriteLine("{0}", i); }
This code prints only the odd numbers, bypassing display of the even values.
Although the continue statement is useful, in most cases it can be replaced by more structured code. For example, the preceding loop could be rewritten as follows:
for(int i = 1; i < 20; i += 2) { Console.WriteLine("{0}", i); }
Another common scenario for employing a continue statement is when a special case is detected within a loop, as in the following code:
for(int user = 0; user < lastUser; ++user) { if(SessionExpired(nameList[user]) == true) continue; ProcessUser(nameList[user]); }
This code could be rewritten as follows:
for(int user = 0; user < lastUser; ++user) { if(SessionExpired(nameList[user]) == false) { ProcessUser(nameList[user]); } }
Of course, your goal should always be to write code that clearly conveys the algorithm that you’re implementing. In some cases, a continue statement is the best choice. If you find yourself creating a loop with multiple continue statements, however, you can probably rewrite your loop in a more structured manner.
Like many programming languages, C# offers a goto statement, which is used to transfer control to a label elsewhere in the current method, as shown here:
label: goto label;
One of the most common uses for goto in C# is in a switch statement, where a goto can be used to explicitly continue processing at another label, as follows:
switch(name) { case "Ali": LookForSocks(); goto case "Mackenzie"; }
The goto statement has a reputation for leading to the destruction of otherwise well-structured code. It’s true that, in most cases, a goto statement can be replaced by a more structured control element. It’s possible to write quite large applications in C# without ever touching a goto, and many people do.
In C#, the capability of the goto statement to ruin code, lives, and property is mitigated by the restriction that a goto can jump only to a label within the current method. One good example of the usefulness of goto is breaking out of deeply nested loops, as follows:
private string [,] table = new string [4,5]; for(int i = 0; i < table.GetLength(0); ++i) { for(int j = 0; j < table.GetLength(1); ++j) { if(name == table[i,j]) goto found; } } found: UseName(name);
In this example, a goto allows you to completely break out of the nested loop easily. Without a goto statement, writing equivalent code would be much more difficult; as mentioned, you’d have to set up a flag for each loop to test to determine whether the loop needs to exit.
The return statement is used to initiate a return from a method call, passing control back to the caller. A return statement in a constructor, a destructor, or a method that’s prototyped as returning void specifies no return value, as shown here:
public void f() { return; }
In a method that specifies a return value, the return statement must include a return value, which will be passed to the method’s caller, as shown here:
public int Cube(int n) { return n * n * n; } int result = Cube(5); Console.WriteLine(result);
When executing inside a try block that has an associated finally clause (see Chapter 2 for a detailed discussion of exception handling), the return value doesn’t immediately transfer control out of the method. Instead, control is first passed to the finally block and then passed back to the method’s caller.
As discussed in Chapter 2, the throw statement is used to throw an exception. When an exception is thrown, control is immediately passed to the first catch block capable of handling the exception, which can be supplied by the caller. The following code shows how to throw an exception using the throw statement:
bool SessionExpired(string userName) { if(UserExists(userName) == 0) throw new InvalidOperationException("Not a current user"); }
An exception shouldn’t be used as a general-purpose flow control statement because an exception is relatively expensive to create and propagate. Reserve the use of exceptions to error handling or other truly exceptional conditions.
Jump statements behave in specific ways in the presence of try and finally blocks. When an exception attempts to transfer control out of a try block, control is first offered to the associated catch block, if one exists. If there’s no catch block associated with the try block, or if the catch block isn’t capable of handling the thrown exception, control is first transferred to the finally block. After the finally block executes, control is transferred according to the intent of the jump statement.
Consider the following example:
string [] table; static void WriteNames(string filename) { FileStream fs = new FileStream(filename, FileMode.Create); StreamWriter writer = new StreamWriter(fs); try { if(table == null ¦¦ table.Length == 0) throw new InvalidOperationException(); foreach(string name in table) { } } finally { if(writer) writer.Close(); } }
In this example, FileStream and StreamWriter objects are created to handle text output to a file. However, if table (an array of string) isn’t properly allocated, an exception is thrown. Because the try block isn’t associated with a catch block to catch the exception, control is passed to the finally block, which will properly close the StreamWriter instance (as well as the underlying instance of FileStream). After the finally block has executed, the exception will be passed to the first appropriate catch block in the calling functions, using the search algorithm discussed in Chapter 2.
An interesting aspect of the finally block is that it must be allowed to run to completion. No jump statements can be invoked in order to leave the finally block early. This restriction is due to the fact that a finally statement might be executing due to a pending jump statement; that jump statement is currently on hold and must be allowed to execute immediately after the finally block has completed. Allowing a new jump statement inside a finally block would significantly complicate error handling for C# programmers.