Perl类闭包

7
我一直试图像perltoot描述的那样在对象内部创建闭包。我已经完全复制了它,甚至还复制和粘贴了它,但我仍然能够以常规方式访问对象$obj->('NAME')。我开始对它失去耐心了! 我是否有什么误解?我多年来一直在为个人项目使用perl,并开始逐渐掌握类和OOP等概念。
package Person;

sub new {
     my $that  = shift;
     my $class = ref($that) || $that;
     my $self = {
        NAME  => undef,
        AGE   => undef,
        PEERS => [],
     };
     my $closure = sub {
        my $field = shift;
        if (@_) { $self->{$field} = shift }
        return    $self->{$field};
     };
     bless($closure, $class);
     return $closure;
}

sub name   { &{ $_[0] }("NAME",  @_[ 1 .. $#_ ] ) }
sub age    { &{ $_[0] }("AGE",   @_[ 1 .. $#_ ] ) }
sub peers  { &{ $_[0] }("PEERS", @_[ 1 .. $#_ ] ) }

1;

1
"$obj->('NAME')"是访问闭包对象的正确方式。非闭包对象则需要使用"$obj->{NAME}",但在这里不起作用。具体问题是什么? - jwodder
3
我认为OP希望访问 $obj->name,但无法访问 $obj->('NAME') (即“私有”或“内部”)。如果是这种情况,也许可以考虑使用 inside-out 对象?不过在OP研究了几种可能的Perl OO内部变体之后,我建议使用现有的OO框架,比如Moose。 - Neil Slater
2
我怀疑 OP 希望页面上的这个声明是准确的: "太阳底下没有什么能让执行方法之外的任何人能够获取到这些隐藏数据。好吧,几乎没有。你可以使用调试器逐步执行程序并在方法中找到这些代码片段,但其他人就没那么幸运了。" 如果您可以直接执行闭包(忽略它已经被“祝福”的事实),那么这个声明就不完全正确了,不是吗?除非您进行一些额外的黑客行为,否则 Perl 允许您对对象执行类自己的方法所能做的任何操作。 - Joe Z
感谢您的帮助,Joe Z,您是正确的,我把页面上的内容当作准确的。真希望我几个小时前就发了这个问题!还有很多关于面向对象编程的东西需要学习。 - Steve
1
外部人士通常看不到包内的局部变量。因此,根据您尝试实现的数据隐藏类型,这可能为您提供了一条前进的道路。或者,放松心情,不要过于担心。这是Perl,不是C ++。 - Joe Z
显示剩余2条评论
2个回答

4

对于一个旨在教学用途的软件而言,这看起来有些不太美观。很多的复杂性来自于new之后的方法。可以考虑使用类似以下的方式:

sub name { &{ $_[0] }("NAME",  @_[ 1 .. $#_ ] ) }

是不透明且无必要的。现代的替代品是

sub name {
  my $self = shift;
  $self->('NAME',  @_);
}

关于 $self 是否应该是哈希引用本身还是经过bless的子例程引用,这也是有争议的。我认为它应该是经过bless的子例程引用。

如果我将哈希引用$data重命名(除了在闭包代码内部,它没有其他名称)并将子例程$self重命名,则您可能会看到更容易识别的内容。我还添加了适当的样板和一些额外的空格。

person.pm

use strict;
use warnings;

package Person;

sub new {

  my $class = shift;
  $class = ref($class) || $class;

  my $data = {
    NAME  => undef,
    AGE   => undef,
    PEERS => [],
  };

  my $self = sub {
    my $fname = shift;
    my $field = $data->{$fname};
    $data->{$fname} = shift if @_;
    return $field;
  };

  return bless $self, $class;
}

sub name {
  my $self = shift;
  $self->('NAME', @_);
}

sub age {
  my $self = shift;
  $self->('AGE', @_);
}

sub peers {
  my $self = shift;
  $self->('PEERS', @_);
}

1;

program.pl

use strict;
use warnings;

use Person;

my $person = Person->new;
$person->name('Jason');
$person->age(23);
$person->peers([qw/ Norbert Rhys Phineas /]);

printf "%s is %d years old.\n", $person->name, $person->age;
my $peers = $person->peers;
print "His peers are: ", join(", ", @$peers), "\n";

希望现在更清楚了。您只能祝福(bless)一个标量引用,但通常情况下这是一个哈希引用,而在这里它是指一个闭包的引用,闭包是一段代码和它创建时访问的数据的组合。
每次调用类的new方法都会创建并定义一个新的词法变量$data。通常,该变量(及其引用的匿名哈希)将在子例程结束时超出范围并被删除。但在这种情况下,new返回一个对调用代码的子例程引用。
由调用代码保留传递的引用。如果不保留任何类的new方法返回的对象,则调用任何类的new方法都没有意义。在这种情况下,因为没有任何东西可以再访问它,所以闭包被删除,并且由于同样的原因,$data变量和匿名哈希也被删除。 所有 Perl 子程序引用都是闭包,无论相关数据是否有用。这个引用包含了对$data的隐式引用,只要任何东西持有该闭包的引用,该引用就会被维护。这只是意味着以下这一行:
return $data->{$field};

这里的$data指的是在执行new时存在的同一个数据,因此哈希表是持久的,可以通过调用闭包子例程来填充和检查它。

其他所有方法所做的都是使用特定的第一个参数执行闭包中的子例程。例如,调用:

$person->name('trolley')

执行 Person::name($person, 'trolley'),这将从参数数组 @_ 中删除 $person 并使用特定的第一个参数调用它(因为它是子例程引用),并复制其余的参数数组。 就像 $person->('NAME', 'trolley')。希望这能帮助您正确理解问题。

3
作为一个闭包本身并不会禁止外部调用者的访问,它只是使接口更加难以理解,需要外部调用者进行一些额外的跳转才能获取内部状态。
然而,由于闭包函数仅可通过闭包来访问内部状态,因此您可以在闭包函数中执行某些应用访问控制的操作。
例如,您可以在闭包回调中查看caller的返回值,以确保调用闭包的人在允许的类白名单上。
然后,要规避这种情况,就必须更加努力地挖掘自己的调用代码,以某种方式使其进入白名单。
例如,您可以通过执行以下操作使自己出现在同一程序包中:
sub foo {
      package Person; #haha, hax.
      $object->('NAME');
}

这将欺骗[caller]->[0],使其无法确定哪个调用包在执行代码。

说到底,你没有太多可靠的方法可以隐藏状态以使它不可渗透,这样做也有些不利。例如,通过混淆私有访问,你会让编写测试变得更加困难,并且其他人在测试中使用你的代码也会更加困难,因为测试中人们经常以各种方式调整内部状态,以避免对更复杂和不可控制的事物产生依赖。

而且有多种方法可以限制对私有值的访问控制

例如,我曾经使用Tie::Hash::Method来提供基本的访问控制,例如:

  • 当创建/写入/读取预定义列表之外的哈希键时发出警告/死亡
  • 当不受信任的包访问内部状态时发出警告/死亡

这些技术也可以帮助消除代码错误,而不仅仅是提供访问限制,因为它可以帮助您重构代码并诊断遗留代码仍在使用已弃用的接口的位置。

也许这段相对简单的代码可以给你一些启示:

use strict;
use warnings;
use utf8;

{

    package Foo;
    use Tie::Hash::Method;
    use Carp qw(croak);
    use Class::Tiny qw(name age), {
        peers => sub { [] }
    };

    sub _access_control {
        my $caller = [ caller(2) ]->[0];
        if ( $caller ne 'Foo' ) {
            local @Foo::CARP_NOT;
            @Foo::CARP_NOT = ( 'Foo', 'Tie::Hash::Method' );
            croak "Private access to hash field >$_[1]<";
        }
    }

    sub BUILD {
        my ( $self, $args ) = @_;
        # return # uncomment for production!
        tie %{$self}, 'Tie::Hash::Method', STORE => sub {
            $self->_access_control( $_[1] );
            return $_[0]->base_hash->{ $_[1] } = $_[2];
          },
          EXISTS => sub {
            $self->_access_control( $_[1] );
            return exists $_[0]->base_hash->{ $_[1] };
          },
          FETCH => sub {
            $self->_access_control( $_[1] );
            return $_[0]->base_hash->{ $_[1] };
          };
    }
}

my $foo = Foo->new();
print qq[has name\n]  if defined $foo->name();
print qq[has age\n]   if defined $foo->age();
print qq[has peers\n] if defined $foo->peers();
$foo->name("Bob");
$foo->age("100");
print $foo->{name};  # Dies here.

我认为问题是关于理解所呈现的Perl代码,而不是“如何隐藏调用代码中的对象变量?”更符合Perl的风格是允许你做任何你选择做的事情,而不是被语言阻止“不良实践”。 - Borodin

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接