Next Previous Contents

Unix Review Column 38

Randal Schwartz

Июнь 2001

[предполагаемый заголовок: ``Это все о контексте'']

Hедавно в статье на Slashdot обсуждалась неожиданная история о студенте высшей школы, который сделал небольшую ошибку при программировании на Perl, которая привела к большим неприятностям. Hа динамически создаваемой странице он использовал код:


  my($f) = `fortune`;

тогда как должен был написать:


  my $f = `fortune`;

Здесь, оба варианта запускают программу fortune, получая случайные шутливые тексты, когда школьная администрация посетила данную страницу, программа fortune выбрала цитату из рассказа William Gibson:


 I put the shotgun in an Adidas bag and padded it out with four pairs of tennis
 socks, not my style at all, but that was what I was aiming for:  If they think
 you're crude, go technical; if they think you're technical, go crude.  I'm a
 very technical boy.  So I decided to get as crude as possible.  These days,
 though, you have to be pretty technical before you can even aspire to 
 crudeness.
 - Johnny Mnemonic, by William Gibson

Если вы не можете сказать что будет в $f для обоих кусков кода, то прочитайте и увидите как непреднамеренная ошибка может привести к неожиданным результатам.

Эта проблема является предметом контекста (при использовании более одного варианта). Операторы Perl чувствительны к контексту, так что оператор может определить, используется ли он в скалярном контексте, а не в списочном и возвращать соответствующий результат. В нашем случае оператор запуска программы возвращал разные варианты в зависимости от того, использовался ли он в скалярном или в списочном контексте.

Для понимания этого различия, давайте сначала разберемся как определить контекст. Hачнем с основ, правая часть присвоения скалярной переменной должна быть скалярным значением:


  $a = ...

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

Аналогичным образом, правая часть присвоения массиву может быть любым списочным значением:


  @b = ...

Давайте поместим некоторые данные в оба типа переменных и посмотрим как они различаются. Я думаю, что вы знакомы с оператором считывания строки, который читается как ``знак-меньше файловый-дескриптор знак больше'':


  $a = <STDIN>

В скалярном контексте оператор считывания возвращает следующую строку или неопределенное значение если происходит ошибка ввода/вывода (такая как обнаружение конца файла).

Однако тот же самый оператор в списочном контексте получает все оставшиеся в файле строки, или пустой список в том случае если конец файла уже достигнут:


  @b = <STDIN>

Конечно Larry Wall мог бы создать два отдельных оператора для этих аналогичных операций, но делая операторы зависимыми от контекста мы сохраняем свою память и количество набираемого текста. Видимо мы, люди достаточно хорошо различаем контексты, так почему бы не не использовать это свойство в языке?

Аналогичным образом, оператор поиска соответствия шаблону возвращает флаг сообщающий о результате выполнения операции:


  $a = /(\w+) (\d+)/

здесь возвращается истинное значение, когда есть переменная $_ соответствует шаблону и ложное значение в противном случае. Если результатом является истина, то в переменной $1 будет храниться слово, а в переменной $2 строка цифр. Более короткой записью этих действий будет использование того же регулярного выражения в списочном контексте:


  @b = /(\w+) (\d+)/

Теперь регулярное выражение не возвращает истинное/ложное значение, а вместо этого возвращает два значения (запомненных при поиске) или пустой список если соответствия не было. Так что $b[0] содержит слово, а $b[1] хранит строку цифр.

В обоих этих операторах, скалярная и списочная интерпретация связаны, но не никакой предсказуемой формулой. Это является общей методой. Вы не можете применять главное правило за исключением тех случаев, когда правил не существует. Это то, что по мнению Larry должно было бы быть наиболее практическим и полезным, и по крайней мере неожиданным (хорошо, по крайней мере неожиданным для Larry).

Вот несколько дополнительных примеров для того, чтобы сохранить наши ступни более влажными и затем мы рассмотрим дополнительные способы определения контекста.

В скалярном контексте, gmtime возвращает понятную для человека строку времени GMT (по умолчанию возвращающую текущее время, но также может преобразовывать заданное время в формате Unix-epoch). Hо в списковом контексте возвращается список из девяти элементов содержащих секунды, минуты, часы (и так далее), что упрощает работу с временем.

В скалярном контексте оператор readdir работает аналогично оператору readline, возвращая ``следующее'' имя из каталога, а в списковом контексте возвращаются все оставшиеся имена.

И в заключение приведем часто используемый оператор для использования имени массива в обоих контекстах. `Оператор'' @x в списочном контексте получает текущие элементы массива @x. Однако, в скалярном контексте тот же самый ``оператор'' возвращает количество элементов того же самого массива (иногда он называется ``длиной массива'', но это может смутить, так что я не буду использовать это название).

Пожалуйста заметьте, что в последнем примере Perl извлекает все элементы в скалярном контексте, только для того, чтобы ``преобразовать'' их в число элементов. С самого начала Perl знает, что операция @x выполняется в скалярном контексте и выполняет ``скалярную'' версию этой операции.

Пойдем другим путем, не существует способа чтобы ``привести'' или ``преобразовать'' список в скаляр, потому-что это никогда не сможет произойти, назло неправильным предположениям некоторых так называемых коммерческих версий документации по Perl.

Итак, где возникает контекст? Везде! Давайте придем к соглашению для того, чтобы облегчить рассказ. Если часть выражения оценивается в скалярном контексте, мы будем использовать SCALAR для обозначения этого факта:


  $a = SCALAR;

И аналогичным образом, мы будем показывать списочный контекст используя LIST:


  @x = LIST;

Так что давайте взглянем на некоторые другие случаи. Присвоение элементу массива выглядит следующим образом:


  $w[SCALAR] = SCALAR;

Заметьте, что индексное выражение оценивается в скалярном контексте. Это означает, что если слева у нас имеется имя массива, а справа используется оператор считывания, то мы будем использовать скаляры с обоих сторон.


  $w[@x] = <STDIN>;

И присваивается одиночная строка (или неопределенное значение) элементу массива @w индекс которого равен количеству элементов массива @x. Это выражение всегда оценивается до того как производится присвоение, так что:


  $w[@w] = <STDIN>;

добавляет следующую строку в конец массива @w, хотя вы вероятно будете пугаться людей делающих так:

Срезы работают в списочном контексте, даже хотя в качестве индекса используется одиночное значение:


  @w[LIST] = LIST;
  @w[3] = LIST;

Даже срезы хешей работают также:


  @h{LIST} = LIST;

Списки скалярных значений всегда являются списками, даже когда слева одиночное значение (или нет значений):


  ($a, $b, $c) = LIST;
  ($a) = LIST;
  () = LIST;

А вот список контекстов используемых некоторыми общими операциями:


  foreach (LIST) { ... }
  if (SCALAR) { ... }
  while (SCALAR) { ... }
  @w = map { LIST } LIST;
  @x = grep { SCALAR } LIST;

Одним полезным правилом является то, что все что оценивается в истинное/ложное значение всегда является скаляром, как показано для операторов if, while и grep.

Подпрограммы выступают ``в некотором отдалении''. Значение возвращаемое подпрограммой всегда оценивается в контексте запуска подпрограммы. Вот основная форма:


  $a = &fred(LIST); sub fred { ....; return SCALAR; }
  @b = &barney(LIST); sub barney { ....; return LIST; }

Hо что делать, если я использую fred для обоих контекстов? Да, контекст может быть передан и будет отличен для различных способов вызова! Если это заставляет кружиться вагу голову, не используйте это до тех пор пока вы не станете хорошо понимать данный вопрос.

Говоря о подпрограммах: общим приемом является создание лексической переменной (часто называемой my-переменной) для хранения входных параметров подпрограмм или временных значений, как в примере:


  sub marine {
    my ($a) = @_;
    ...
  }

В этом случае, если используются скобки, у нас будет списочный контекст (представте, что my здесь нет). Возвращается множество элементов массива @_, но только первый элемент сохраняется в переменной $a (остаток массива игнорируется).

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


  my $a = @_;

что возвращает число элементов массива $@ (список аргументов). Здесь нет ``более правильных'' форм; вам необходимо знать о различии и использовать соответствующую форму.

И это возвращает нас обратно к вопросу, который я задал в начале статьи. В чем различие? Вызов программы в скалярном контексте возвращает все значение как одну строку:


  my $f = `fortune`;

но тоже самое в списочном контексте создает список (по одной строке на элемент, точно также как при считывании из файла), и только первый элемент присваивается скаляру слева:


  my ($f) = `fortune`;

Так что $f получает только первую строку текста возвращенного fortune, удобную для одно строковых выражений, но достаточно разрушительную когда официальные лица школы увидели, что студент вероятно написал:


 I put the shotgun in an Adidas bag
 and padded it out with four pairs of tennis

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

Была вызвана полиция, парень был допрошен и теперь на него имеется полицейское досье, только потому что он добавил ошибочные скобки. Штрафов не последовало, но такие затруднения определенно нежелательны. (Я говорю это по своему опыту: моя собственная сага о несоответствующем понимании и последующих криминальных платах может быть найдена по адресу http://www.lightlink.com/fors/).

И затруднений можно избежать немного больше беспокоясь о программировании и тестировании качества. Так что когда вы пишете на Perl, и вы удивлены контекстом, то возьмите этот текст и заучите его. До следующей встречи, наслаждайтесь!


Next Previous Contents