Previous Page
Next Page

6.18. do-while Loops

Don't use do...while loops.

Like any other postfix looping construct, a do...while loop is intrinsically hard to read, because it places the controlling condition at the end of the loop, rather than at the beginning.

More importantly, in Perl a do...while loop isn't a "first-class" loop at all. Specifically, you can't use the next, last, or redo commands within a do...while. Or, worse still, you can use those control directives; they just won't do what you expect.

For example, the following code looks like it should work:

    sub get_big_int {
        my $int;

        TRY:
        do {
            # Request an integer...
            print 'Enter a large integer: ';
            $int = <>;

            # That's not an integer!...
            next TRY if $int !~ /\A [-+]? \d+ \n? \z/xms;

            # Otherwise tidy it up a little...
            chomp $int;
        } while $int < 10;   # Until the input is more than a single digit

        return $int;
    }

    # and later...

    for (1..$MAX_NUMBER_OF_ATTEMPTS) {
        print sqrt get_big_int(  ), "\n";
    }

That looks okay, but it isn't. Specifically, if a non-integer is ever entered and the next trY command is invoked, that next starts looking for an appropriately labeled loop to re-iterate. But the do...while isn't actually a loop; it's a postfix-modified do block. So the next ignores the trY: label attached to the do. Control passes out of the do block, and then out of the subroutine call (a subroutine isn't a loop either), until it returns to the for loop. But the for loop isn't labeled trY:, so control passes outwards again, this time right out of the program.

In other words, if the user ever enters a value that isn't a pure integer, the entire application will immediately terminatenot a very robust or graceful way to respond to errors. That kind of bug is particularly hard to find too, because it's one of those rare cases of a Perl construct not doing what you mean. It looks right, but it works wrong.

The best practice is to avoid do...while entirely. The simple way to do that is to use a regular while loop instead, but to "counter-initialize" the $int variable, to guarantee that the loop executes at least once:


    sub get_big_int {
        my $int = 0;   
# Small value so the while loop has to iterate at least once
TRY: while ($int < 10) { print 'Enter a large integer: '; $int = <>; next TRY if $int !~ /\A [-+]? \d+ \n? \z/xms; chomp $int; } return $int; }

Sometimes, however, the condition to be met is too complex to permit counter-initialization, or perhaps no counter-initial value is possible. This most commonly occurs when the test is performed by a separate subroutine. In such cases, either use a flag:


    sub get_big_int {
        my $tried = 0;
        my $int;

        while (!$tried || !is_big($int)) {
            print 'Enter a valid integer: ';
            $int = <>;

            chomp $int;

            $tried = 1;
        }


        return $int;
    }

or, better still, use a return to explicitly escape from an infinite loop:


    sub get_big_int {
        while (1) {
            print 'Enter a valid integer: ';
            my $int = <>;

            chomp $int;

            return $int if is_big($int);
        }

        return;
    }

    Previous Page
    Next Page