用Guard保护你的代码

我无法总是相信我的子例程能够在离开时保持世界与它们进入时相同。Perl有一些特性可以帮助做到这一点,但Guard模块做得更多。

考虑这样一个例子,我想在我的子例程中临时更改当前工作目录。如果不小心,由于chdir有进程级别的效果,其余的部分最终会出现在一个意外的目录中。

sub do_some_work {
  state $dir = '/usr/local/etc';
  chdir $dir or die "Could not change to $dir! $!";

  ...; # do some work
}

由于我没有回到起始目录,在调用do_some_work之后,程序的其余部分使用/usr/local/etc作为解析相对路径的基准。

如果我很小心,我会在更改之前保存当前工作目录的工作,并且我会切换回那个目录。来自标准库的Cwd模块中的getcw

use Cwd qw(getcwd);

sub do_some_work {
  state $dir = '/usr/local/etc';

  my $old_directory = getcwd();
  chdir $dir or die "Could not change to $dir! $!";

  ...; # do some work

  chdir $old_directory
    or die "Could not change back to $old_directory! $!";

  return $value;
}

这太麻烦了。我一直希望chdir能够像select返回当前默认文件句柄一样返回旧目录。相反,我使用一个带有导入子例程的模块。

完成工作后,我还需要调用另一个chdir,我可能还需要添加一些额外的代码来返回正确的值,因为我不能轻松地组织代码来使用Perl的巧妙的后评估表达式习语(尽管Perl 5.20优化了子例程结束时的返回)。当我想让逻辑部分彼此靠近时,两个chdir放在一起会让我感到代码风格上的不悦。

现在出现了Guard模块,它允许我定义在子例程结束时运行的代码块。在某个作用域内,我使用scope_guard创建一个保护器,该保护器在作用域退出时运行

use v5.10;

use Cwd qw(getcwd);
use Guard;

chdir '/etc' or die "Could not start at /etc: $!";
my $starting_dir = getcwd();

do_some_work();

say "Finally, the directory is ", getcwd();


sub do_some_work {
  state $dir = '/usr/local/etc';

  my $old_directory = getcwd();
  scope_guard {
    say "Guard thinks old directory is $old_directory";
    chdir $old_directory;
  };
  chdir $dir or die "Could not change to $dir! $!";

  say "At the end of do_some_work(), the directory is ", getcwd();
}

输出显示了每个部分认为当前工作目录应该是哪里

At the end of do_some_work(), the directory is /usr/local/etc
Guard thinks old directory is /etc
Finally, the directory is /etc

这仍然有点难看。由于scope_guard只接受一个块或sub {}参数,所以我不能将其参数重构为子例程。这不起作用

scope_guard make_sub_ref();  # wrong sort of argument

不过,我可以在变量中创建一个保护器来解决这个问题。变量保护器不是在作用域退出时执行其工作,而是在清理时执行(我们可以在其作用域结束时自行执行)。在这个例子中,我使用Perl v5.20 子例程签名仅仅因为我可以(即使它们是实验性的,它们也非常好)

use v5.20;
use feature qw(signatures);
no warnings qw(experimental::signatures);

use Cwd qw(getcwd);
use Guard;

chdir '/etc' or die "Could not start at /etc: $!";
my $starting_dir = getcwd();

do_some_work();

say "Finally, the directory is ", getcwd();


sub do_some_work {
  state $dir = '/usr/local/etc';

  my $guard = make_guard( getcwd() );
  chdir $dir or die "Could not change to $dir! $!";

  say "At the end of do_some_work(), the directory is ", getcwd();
}

sub make_guard ( $old_directory ) {
  return guard {
    say "Guard thinks old directory is $old_directory";
    chdir $old_directory;
  };
}

现在do_some_work中的代码看起来更优雅,并且我可以在其他子例程中重用这个保护器。

这是一个额外的技巧,也是我想展示子例程签名的原因之一。我可以为子例程参数声明一个默认值。如果我没有指定make_guard的参数,Perl会使用getcwd的值来填充它

:

sub make_guard ( $old_directory = getcwd() ) {
  return guard {
    say "Guard thinks old directory is $old_directory";
    chdir $old_directory;
    };
}

有了默认值,我可以简化对make_guard的调用,同时仍然有灵活性来提供参数

my $guard = make_guard();

我还可以用M执行其他技巧。我可以定义多个scope_guard。在这种情况下,它们按定义的相反顺序执行(就像END块一样)。使用保护器对象,我可以取消保护器,如果决定不再需要它。

封面图片© Kenny Loule


这篇文章最初发布在PerlTricks.com上。

标签

brian d foy

布莱恩·D·福伊 是一名Perl培训师和作家,也是Perl.com的高级编辑。他是《精通Perl》、《Mojolicious Web Clients》、《学习Perl习题》的作者,以及《Perl编程》、《学习Perl》、《中级Perl》和《高效Perl编程》的合著者。

浏览他们的文章

反馈

这篇文章有问题?请在GitHub上打开一个issue或pull request来帮助我们。