Почему моя подпрограмма Perl не видит значение переменной в цикле foreach, который ее вызвал? - PullRequest
5 голосов
/ 15 марта 2010

Надеюсь, это что-то прямое, что я делаю неправильно. Я видел что-то в Интернете о «переменном самоубийстве», которое выглядело хорошо, но это было для более старой версии, и я нахожусь на 5.10.1.

В любом случае - переменная, которую я объявил - $ RootDirectory - просто внезапно теряет свое значение, и я не могу понять, почему.

Вот скрипт для воспроизведения проблемы. Когда я запускаю скрипт в режиме отладки (perl -d), я могу заставить его распечатать $ RootDirectory в строках 21 и 26. Но он ушел в строку 30.

use strict;
my $RootDirectory; 
my @RootDirectories; 

@RootDirectories = (
   'c:\\P4\\EDW\\PRODEDW\\EDWDM\\main\\db\\'
   ,'c:\\P4\\EDW\\PRODEDW\\EDWADS\\main\\db\\'
   ,'c:\\P4\\EDW\\PRODEDW\\FJE\\main\\db\\'
   );

foreach $RootDirectory (@RootDirectories) { 
   # $RootDirectory = 'c:\\P4\\EDW\\PRODEDW\\EDWDM\\main\\db\\';
   # print ' In foreach ' . $RootDirectory. "\n";
   RunSchema ();
} 

exit(0);

sub RunSchema() { 
   # print ' In RunSchema ' . $RootDirectory. "\n";
   CreateTables ();
} 

sub CreateTables() { 
   # print ' In CreateTables ' . $RootDirectory. "\n";
   SQLExecFolder ('tbl');
} 

sub SQLExecFolder() { 
   print ' In SQLExecFolder ' . $RootDirectory. "\n";       # Variable $RootDirectory value is gone by now
} 

EDIT Спасибо за все комментарии! Я думаю, что пока я буду использовать ключевое слово «наш», которое, кажется, работает хорошо - спасибо Натан. Также спасибо инструменту об использовании предупреждений - я думаю, что я продан на этом!

Меня по-прежнему смущает то, что когда я сделал режим отладки (perl -d) и прошел по коду, выполнив «p $ RootDirectory», я получил ожидаемый результат в строках 21 и 26, но не в строке 30. Чем отличается ситуация в строке 30?

Кроме того, я ценю комментарии о том, как лучше передавать $ RootDirectory в качестве параметра функции. Я хотел избежать этого, потому что после этого у меня так много функций - то есть RunSchema вызывает CreateTables, которая вызывает SQLExecFolder. Всем им нужно было бы передать один и тот же параметр. Имеет ли это смысл в этом случае, или есть ли лучшие способы структурировать это?

Ответы [ 7 ]

8 голосов
/ 15 марта 2010

То, что сказал Натан, верно. Кроме того, почему бы вам не передать значение? В любом случае, лучше практиковаться:

foreach $RootDirectory (@RootDirectories) { 
   # $RootDirectory = 'c:\\P4\\EDW\\PRODEDW\\EDWDM\\main\\db\\';
   # print ' In foreach ' . $RootDirectory. "\n";
   RunSchema ($RootDirectory);
} 

sub SQLExecFolder { 
   my $RootDirectory = shift;
   print ' In SQLExecFolder ' . $RootDirectory. "\n";
} 
5 голосов
/ 15 марта 2010

Вы объявляете $RootDirectory как переменную цикла в цикле foreach. Насколько я понимаю, это означает, что его значение local привязано к циклу, а его значение восстанавливается до его предыдущего значения в конце цикла.

В вашем случае переменная никогда не была назначена, поэтому в конце цикла она возвращается к своему предыдущему значению undef.

Редактировать : На самом деле проблема в том, что $RootDirectory объявлен с my, поэтому он не определен в других областях. В функциях RunSchema, CreateTables и SQLExecFolder переменная не определена независимо от локализации foreach.

Если вы хотите, чтобы переменная была объявлена ​​для strict ness, но вы хотите, чтобы она была глобальной, объявите $RootDirectory с our:

our $RootDirectory;

Редактировать : При этом не всегда рекомендуется использовать глобальную переменную. Вам лучше передавать переменную в качестве параметра функциям, как предлагали другие.

4 голосов
/ 15 марта 2010

Другие ответили на ваш вопрос правильно. Я просто хочу подчеркнуть, что вы должны добавить use warnings; в ваш код. Это дало бы понять вашу проблему и предупредило бы вас о другой потенциальной опасности.

3 голосов
/ 15 марта 2010

foreach переменная особая - она ​​локальна для цикла.

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

Пожалуйста, посмотрите здесь

2 голосов
/ 16 марта 2010

RE: Когда использовать глобальную переменную?

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

Когда имеет смысл использовать глобал? Когда выгоды перевешивают риски.

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

НЕПРАВИЛЬНО. В этом случае правильным подходом является объединение всех этих различных переменных в одну структуру данных контейнера. Таким образом, вместо foo( $frob, $grizzle, $cheese, $omg, $wtf ); у вас есть foo( $state, $frob ); Где $state = { grizzle => $grizzle, cheese => $cheese, omg => $omg, wtf => $wtf };.

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

На данный момент у вас есть несколько вариантов:

  1. Сделайте $state глобальным и просто получите к нему прямой доступ.
  2. Превратите $state в объект конфигурации и используйте методы для управления доступом к атрибутам.
  3. Превратить весь модуль в класс и сохранить всю информацию о состоянии в объекте.

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

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

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

Ваш пример кода кажется хорошим кандидатом на вариант 3. Я создал класс с именем MySchema, и все методы, работающие с конкретным каталогом, теперь являются методами. Вызывающий объект несет необходимые ему данные.

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

use strict;
use warnings;

my @directories = (
   'c:\\P4\\EDW\\PRODEDW\\EDWDM\\main\\db\\',
   'c:\\P4\\EDW\\PRODEDW\\EDWADS\\main\\db\\',
   'c:\\P4\\EDW\\PRODEDW\\FJE\\main\\db\\',
);

for my $schema ( make_schemata(@directories) ) {

    $schema->run;

}

sub make_schemata {
    my @schemata = map { MySchema->new( directory => $_ } @_;

    return @schemata;
}


BEGIN {
    package MySchema;

    use Moose;

    has 'directory' => (
        is => 'ro',
        isa => 'Str',
        required => 1,
    );

    sub run { 
       my $self = shift;

       $self->create_tables;
    } 

    sub create_tables { 
       my $self = shift;

       $self->sql_exec_folder('tbl');
    }

    sub sql_exec_folder {
        my $self = shift;

        my $dir = $self->directory;

        print "In SQLExecFolder $dir\n";
    }

    1;
} 

В качестве бонуса код в блоке BEGIN может быть удален и помещен в отдельный файл для повторного использования другим скриптом. Все, что нужно для полноценного модуля - это собственный файл с именем MySchema.pm.

2 голосов
/ 15 марта 2010

Переменная итератора в цикле foreach всегда локализуется в цикле. См. Раздел foreach в perlsyn . Вы можете передать его подпрограмме в качестве параметра.

0 голосов
/ 15 марта 2010

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

use strict;
use warnings;

sub RunSchema() {
   my $root_dir = shift;
   CreateTables($root_dir);
}

sub CreateTables() {
   my $root_dir = shift;
   SQLExecFolder('tbl', $root_dir);
}

sub SQLExecFolder() {
   my ($name, $root_dir) = @_;
}
######################################################


my @RootDirectories = qw(
   c:\\P4\\EDW\\PRODEDW\\EDWDM\\main\\db\\
   c:\\P4\\EDW\\PRODEDW\\EDWADS\\main\\db\\
   c:\\P4\\EDW\\PRODEDW\\FJE\\main\\db\\
);

foreach my $RootDirectory (@RootDirectories) {
   # print ' In foreach ' . $RootDirectory. "\n";
   RunSchema($RootDirectory);
}

exit(0);
...