Next Previous Contents

Unix Review Column 21 -- Выбор среди операций выбора

Randal Schwartz

Август 1998

Перевод Anton Petrusevich <casus@mail.ru> и Alex Ott <ott@phtd.tpu.edu.ru>

Большинство современных алгоритмических языков имеют разные способы выполнения ``условий'' и ``итераций'', или они будут бесполезными (или полезными только для простых программ). Perl имеет разные способы выбора и оценки циклов. Давайте рассмотрим некоторые из этих способов.

Базовым элементом для выполнения выбора является if:

    if ($somecondition) {
      true_branch;
      true_branch;
    } else {
      false_branch;
      false_branch;
    }

вместе с предложением elsif:

    if ($cond_1) {
      true_branch_for_1;
      true_branch_for_1;
    } elsif ($cond_2) {
      true_branch_for_2;
      true_branch_for_2;
    } elsif ($cond_3) {
      true_branch_for_3;
      true_branch_for_3;
    } else {
      all_false_branch;
      all_false_branch;
    }

Каждое из условий приведенных условий выполняется последовательно и первое из условий, результат которого будет истинным вызовет выполнение соответствующей ветви, пропуская все остальные условия. Удобная конструкция.

Hо также существуют другие способы получения тех же результатов. Многие из них перечислены ``man perlsyn'' (или ``perldoc perlsyn'' если у вас не установлены справочные страницы). Hапример, условие if может быть переписано как:

    $cond && do {
      true_branch;
      true_branch;
    };

или мы можем окружить его ``backwards-if'', как в коде:

    do {
      true_branch;
      true_branch;
    } if $cond;

Я сам лично не использую этот прием часто. Мне кажется, что он выглядит слишком задом наперед. Однако он является более простой формой:

    $action_expression if $cond;

которую я использую часто. Hапример:

    print "a is $a, b is $b\n"
    if $debug;

Здесь я устанавливаю $debug в начале программы, и все эти операторы начинают выполняться.

Для получения большего количества вариантов, давайте сначала глянем на построение блоков циклов. Базовый цикл while дает нам повторяемое выполнение блока кода до тех пор, пока условное выражение не станет ложным:

    while ($cond) {
      body;
      body;
    }

мы можем прервать выполнение цикла используя команду last, как в коде:

    while ($cond) {
      body;
      last if $cond_2;
      body;
    }

Здесь обратный-if прерывает цикл в том случае, когда $cond_2 становится равным истинному выражению. Поскольку last рассчитывается как выражение, то мы можем использовать его в выражении, разделенном запятыми, для выполнения сразу нескольких операций:

 
    while ($cond) {
      body;
      ($a = "something"),
      ($b = "something else"),
      last
        if $cond_2;
      body;
    }

Начав однажды, я предпочитаю сохранять левую часть оператора обратный-if достаточно простой, так, что я использую ее таким же ограниченным способом. Но рассматривайте, что может быть что-то подобное такому непростому варианту:

 
    while ($cond) {
      body;
      if ($cond_2) {
        $a = "something";
        $b = "something else";
        last;
      }
      body;
    }

который работает, но занимает несколько видимых строк. Также имеется оператор next, который прыгает вперед в том случае, когда мы видим, что есть вещи, которые нам не хочется выполнять:

    foreach $item (split) {
      body_1;
      next if $item =~ /^a/;
      body_2;
    }

В этом случае объекты начинающиеся с ``a'' обрабатываются операторами body_1, но не обрабатываются операторами body_2. Оператор next автоматически выполняет оператор continue, который мы можем иногда указать сами:

    foreach $item (split) {
      body_1;
      next if $item =~ /^a/;
      body_2;
      next if $item =~ /z$/;
      body_3;
    } continue {
      body_4;
    }

В этом случае объекты начинающиеся с``a'' выполняют только блоки body_1 и body_4, в то время, как начинающиеся с ``z'' выполняют body_1, body_2 и body_4, а все остальные объекты выполняют все 4 действия. Это достаточно гибко. Практическим применением этого является получение правильной переменной $. в течении обработки чтения файла используя <>. Вам необходимо закрыть ARGV когда вы обнаруживаете конец файла, но только после того как вы выполните всю обработку строки. Так что простой надежный цикл выглядит примерно так:

    while (<>) {
      next unless /perl/i;
      print "$ARGV:$.:$_";
    } continue {
      close(ARGV) if eof;
    }

Располагая close внутри continue, мы получаем гарантию, что эта функция будет выполнена даже в том случае, если мы выполним оператор next.

Также существует оператор redo, который перезапускает текущую итерацию. Вот великолепный практический пример использования redo --обработка продолжающихся строк в файле настройки:

    while (<>) {
      s/#.*//;
      next unless /\S/;
      if (s/\s*\\\s*\n//) {
        $_ .= <>;
        redo;
      }
      ...;
    }

Мы считываем данные по одной строке за раз в переменную $_. Если строка содержит комментарий, то они удаляются из нее. Если строка полностью состоит из пробельных символов (не содержит непробельных символов), то она игнорируется. Хитрой частью является подстановка. Любой символ обратный слэш в конце строки (возможно окруженный пробелами) будет удален и мы добавим к строке следующую строку из потока ввода.

Однако мы опять получаем строку, для которой надо выполнить удаление комментариев, проверку на то, что строка не пустая и возможно выполнить дальнейшую обработку символов обратный слэш, так что самым простым способом будет выполнить оператор redo, который выполнит переход в начало цикла. Красиво и компактно.

Операторы last, next, and redo применяются к ``явному блоку'', точно также как и итераторы (while, until, for и foreach). Используя явные блоки (такие, которые не являются частью более крупной конструкции) мы скорее можем создать произвольные контрольные структуры. Например, давайте будем складывать все числа, которые вводятся по одному, останавливаясь только при вводе слова ``end''.

    {
      print "number? ";
      chomp($n = <STDIN>);
      last if $n eq "end";
      $sum += $n;
      redo;
    }

Здесь явный блок работает как граница для заключенных в него операторов last и redo. Эта проблема может быть решена, но мы по существу создаем цикл, в который входят в центре, или из которого выходят в центре, а зависимости от того, как вы рассматриваете его. Используя явные блоки мы также можем создать многовариантные проверки if. Давайте рассмотрим некоторую элементарную обработку @ARGV:

{
    last unless $ARGV[0] =~ /^-/;
    $_ = shift;
    last if /^--$/; $verbose++, redo if /^-v$/; $radians++, redo if /^-r$/;
    $expr = ($1 || shift), redo if /^-e(.*)$/;
    $file = ($1 || shift), redo if /^-f(.*)$/; die ``unknown arg: $_'';
}

В этом коде мы обрабатываем записи в @ARGV до тех пор, пока в них не будет отсутствовать начальное тире. Если первый элемент начинается с тире, то мы помещаем его в переменную $_. Если мы встречаем двойное тире, то мы прекращаем обработку аргументов. Однако, если мы встречаем -v или -r, то мы устанавливаем соответствующую переменную и опять переходим в начало цикла. Если мы встречаем -e или -f, то как параметр мы берем остаток аргумента (или следующий аргумент, если нет последующего текста). В заключение, если мы прошли по всем вариантам и не обнаружили то, что мы хотим увидеть, то мы можем прекратить работу.

Вы можете выполнить это также с помощью операторов and и or как в следующем коде:

{
     $ARGV[0] =~ /^-/ or last;
     $_ = shift;
     /^--$/ and last;
     /^-v$/ and $verbose++, redo;
     /^-r$/ and $radians++, redo;
     /^-e(.*)$/ and $expr = ($1 || shift), redo;
     /^-f(.*)$/ and $file = ($1 || shift), redo;
     die ``unknown arg: $_'';
}

Этот код выполняет тоже самое, что и предыдущий пример. Когда я начал писать этот пример, я стал ``yuck'', поскольку иногда я до некоторой степени был враждебно настроен против использования операторов and и or в пустом (void) контексте (просто из-за их постороннего эффекта выполнять правую часть оператора в зависимости от условного выражения). Однако после тщательного рассмотрения этого кода, я стал более благосклонным к таком коду, поскольку он был для меня более ясным. Отрицательным эффектом может быть то, что кто-то просматривающий код может не понимать как работают операторы and и or, так что может быть полезным комментирование кода.

Итак, я надеюсь, что я дал вам дополнительные варианты создания условных выражений и позволил вам увидеть дополнительные формы операторов повторения. Всегда помните девиз Perl: Существует более чем один способ решения! Увидимся в следующий раз.


Next Previous Contents