Почему моя подпрограмма Perl не может увидеть значение переменной в цикле foreach, который ее вызвал?

Я надеюсь, что это что-то прямое, что я делаю неправильно. Я видел в сети кое-что о "переменном самоубийстве", которое выглядело неплохо, но это было для более старой версии, а я использую 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
} 

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

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

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


person Sylvia    schedule 15.03.2010    source источник
comment
use strict; use warnings; укажет на несколько проблем с вашим кодом.   -  person Ether    schedule 15.03.2010
comment
Кроме того, не следует заранее объявлять переменные без уважительной причины (это не C!) - объявляйте их при первом использовании. Вы можете легко получить более одной переменной с одним и тем же именем (в разных областях), если не будете осторожны, что затруднит отладку вещей позже.   -  person Ether    schedule 15.03.2010
comment
Вы также можете поблагодарить авторов ответов, проголосовав за их ответы (например, Toolic). :)   -  person Christopher Bottoms    schedule 15.03.2010


Ответы (7)


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

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

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

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

our $RootDirectory;

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

person Nathan Fellman    schedule 15.03.2010
comment
И $RoodDirectory следует передать как параметр функции - person Ivan Nevostruev; 15.03.2010
comment
Функции находятся в той же лексической области, что и исходный $RootDirectory. Проблема в том, что foreach создает новую лексическую переменную, когда переменная цикла является лексической. См. perldoc.perl.org/perlsub.html#Private-Variables -via-my () - person cjm; 15.03.2010
comment
@Nathan Fellman: ваше редактирование неверно - моя переменная в верхней части файла видна во всех функциях внутри файла. Здесь нет необходимости использовать наш. - person ; 15.03.2010
comment
@Arkadiy - наш якобы решил проблему. Может я чего то упускаю? - person Sylvia; 15.03.2010
comment
Избегайте соблазна использовать глобальные переменные. - person daotoad; 16.03.2010
comment
Я предполагаю, что это решает проблему, потому что foreach обрабатывает наши файлы иначе, чем мои, и не локализует их (хотя это не очевидно из документации). Я хотел сказать, что ваша проблема вызвана особым обращением с моим циклом for, а не с моим как таковым. - person ; 16.03.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";
} 
person Vivin Paliath    schedule 15.03.2010

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

person toolic    schedule 15.03.2010

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

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

Загляните сюда

person Community    schedule 15.03.2010

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

person Eugene Yarmash    schedule 15.03.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.

person daotoad    schedule 16.03.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);
person mctylr    schedule 15.03.2010
comment
Нет нет нет. Использование прототипов функций полезно, когда вы знаете, что делаете, но не знаете, или определяете параметры для подпрограмм. - person mpeters; 15.03.2010
comment
Нет необходимости определять переменную в Perl перед ее первым использованием. - person Ether; 16.03.2010
comment
Я внес предложенные изменения, с которыми я согласен. Я также улучшил формулировку использования параметра, передаваемого в подпрограмму. - person mctylr; 16.03.2010