4

我对 perl 很陌生,而且我被困在一个家庭作业问题上。我有一个带有类变量的对象,它计算创建的实例数。然后我有一个带有实例变量的子类。

我的第一个问题是,如何使类变量对用户隐藏?我尝试使用闭包,但无法弄清楚如何使用它进行继承。它是一个类变量这一事实使情况变得更糟,因为递增它的代码执行了两次,它说我有两个实例,而我有一个。不完全确定它为什么会发生,但它是有道理的。我尝试使用标量,但变量再次没有正确递增。还没有尝试过“由内而外的对象”,我不确定我是否愿意,这似乎超出了我的想象。我感觉封装类变量与封装实例变量不同,但我找不到任何解释如何做到这一点的东西。

我的第二个问题是,正如我所提到的,我无法通过封装来处理继承。当您从子类调用超级构造函数时使用闭包,您将获得对子例程的引用,因此(据我所知)无法向其中添加实例变量。

这是我的基类:

#!/usr/bin/perl -w
use strict;
package Base;

my $count = 1;

sub new {
    my $class = shift;
    my $self = {
        _Count => $count # not hidden
    };
    $count++; # increment count
    bless $self, $class;
    return $self;
}

sub Count { # getter
    my $self = shift;
    return $self->{_Count};
}
1;

这是我的子类:

#!/usr/bin/perl -w
use strict;
package Sub;
use Base;
our @ISA = qw(Base);

sub new {
    my $class = shift;
    my $self = $class->SUPER::New();
    $self->{_Name} = undef; # not hidden
    return $self;
}

sub Name { #getter/setter
    my($self, $name) = @_;
    $self->{_Name} = $name if defined($name);
    return $self->{_Name};
}
1;
4

5 回答 5

4

如果您使用的是裸 Perl 5(而不是使用 OO 框架),则执行类变量的常用方法是作为仅对访问者可见的词法:

{
    my $count = 0;

    sub Count {
        my ($self, $new_count) = @_;

        if (defined $new_count) { # NB only works if undef is not a legit value
            $count = $new_count;
        }

        return $count;
     }
}

$count仅在封闭块中可见;甚至同一类的其他方法也看不到它。但是任何人都可以使用$base_obj->Count或对其进行操作Base->Count,并且任何此类操作都会影响共享变量。

您还可以使用闭包来提供真正隐藏的实例变量。除非您正在履行家庭作业的任意规则,否则这是不值得做的。

package Base;

sub new {
    my ($class, $name) = @_;
    die "Need name!" unless defined $name;

    my $age;

    return bless sub {
        my ($attribute, @args) = @_;

        if ($attribute eq 'name') {
            if (@args) {
                die "Attempt to set read-only attribute!";
            }
            return $name;
        }

        if ($attribute eq 'age') {
            if (@args) {
                 ($age) = @args;
            }
            return $age;
        }

        die "Unknown attribute $attribute";
    } => $class;
}

sub name {
    my ($self, @args) = @_;

    return $self->(name => @args);
}

sub age {
    my ($self, @args) = @_;

    return $self->(age => @args);
}

这里发生的情况是,由返回的祝福 subnew关闭了两个词汇,$name并且$age. 返回时new,这些词法超出范围,从那时起访问它们的唯一方法是通过闭包。闭包可以检查它的参数以允许或拒绝访问它持有的值。只要它从不返回引用,就可以确定它只有直接访问这些变量的权限。

这也适用于继承,没有太多额外的微妙之处:

package Derived;
use base 'Base';
sub new {
    my ($class, $name, $color) = @_;

    my $base_instance = $class->SUPER::new($name);

    return bless sub {
        my ($attribute, @args) = @_;

        if ($attribute eq 'color') {
            if (@args) {
                 ($color) = @args;
            }
            return $color;
        }

        # base class handles anything we don't, possibly by dying
        return $base_instance->($attribute, @args);
    } => $class;
}

这模拟了对基类和派生类实例数据具有不同存储的语言所做的事情,要么在本地处理请求,要么将其传递给已添加到闭包中的基类实例。更深的继承树将导致闭包关闭闭包,每个闭包也可以选择关闭该特定类所需的实例变量。

这是一个相当大的混乱,很难检查和调试,这就是为什么我要再强调一次,你不应该这样做。但了解这一点非常有用,我将您推荐给SICP

于 2012-12-07T21:08:43.690 回答
3

作为模块局部my变量,$count已经对模块/类的用户隐藏。看起来好像您将实例变量_Count用作“当前 ID”类型变量,以便创建的每个对象(实例)都从 1 开始获得一个新 ID。(如果相反,它是为了跟踪活动实例的数量,那么您需要将其递减,DESTROY并且无需在对象中存储副本。)如果您的测试代码仅创建一个实例,则其Count()方法应返回 1 但$count将是 2,因为它从 1 开始并在存储后递增对象中的旧值。

在 perl 中,典型的做法是将实例变量存储在$self哈希中,而不隐藏它们,尽管有时会使用前缀来避免冲突。与语言特性相比,它们更受惯例的保护(依赖实现细节是不安全的,因为它们可能会改变)。

如果您想对 perl 类进行更高级别的控制,请查看Moose模块套件。

于 2012-12-07T21:21:01.867 回答
3

在 Perl 中,通常不会通过语言的语义来隐藏字段,而是通过文档形式的合同来隐藏字段。但是,可以通过使用闭包来隐藏字段。还值得注意的是,Perl 并没有在语义上区分类方法和实例方法。

实现对象的标准方法之一是祝福哈希,就像你一样。此哈希包含所有实例变量/字段。习惯上用下划线开始“私有”字段。通常,合约(文档)不会说明这些字段是如何存储的,但会要求类的用户通过各种方法调用。

类变量不应与实例一起存储。最好使用全局变量或词法变量。在您提供的代码中,$count它只是一个计数器,但您永远不会将它作为类变量访问。相反,您为每个实例分配一个唯一 ID。要将它用作类变量,请提供适当的访问器(我去掉了像returns 这样的不必要的东西):

{
  package Base;

  my $count = 0;

  sub new {
    my ($class) = @_;
    my $self = {
      ID => $count++,
    };
    bless $self, $class;
  }

  sub Count  { $count }
  sub ID     { my ($self) = @_; $self->{ID} }
  sub report { my ($self) = @_; "I am the Base object ".($self->ID)."." }
}

=head1 Base

A generic base class

=head2 Base->Count

Return the object count.

=head2 $base->ID

Give the unique ID of this object.

=head2 $base->report

Returns a string containing a short description.

=cut

子类与计数无关。这是由上述变量的范围强制执行的$count,通过外部花括号表示。subs 是这个变量的闭包。

{
  package Sub;
  use parent -norequire, qw(Base); # remove `-norequire` if Base in different file

  sub new {
    my ($class) = @_;
    my $self = $class->SUPER::new;
    $self->{Name} = undef;
    $self;
  }

  sub Name :lvalue {
    my ($self) = @_;
    $self->{Name};
  }
  sub report {
    my ($self) = @_;
    "I am the Sub object ".($self->ID)." called ".($self->Name).".";
  }
}

=head1 Sub

A generic subclass. It subclasses Base.

=head2 $sub->Name [= SCALAR]

Gets or sets the name of $sub.

    my $oldname = $sub->Name;
    $sub->name = "new name";

=cut

如您所见,Sub构造函数调用Base初始化程序,然后添加一个新字段。它没有类方法或类变量。该类无法访问该$count变量,除非通过访问器类方法。合同通过 POD 文档说明。

(在该Name方法中,我使用了:lvalue注解。这使我可以简单地分配给对象中的适当字段。但是,这不允许检查参数。)

测试用例

my $base1 = Base->new; my $base2 = Base->new;
print "There are now " . Base->Count . " Base objects\n";
my $sub1 = Sub->new;   my $sub2 = Sub->new;
print "There are now " . Base->Count . " Base objects\n";

$sub2->Name = "Fred";

print $_->report . "\n" for ($base1, $sub1, $base2, $sub2);

印刷

There are now 2 Base objects
There are now 4 Base objects
I am the Base object 0.
I am the Sub object 2 called .
I am the Base object 1.
I am the Sub object 3 called Fred.

漂亮,不是吗?(除了$sub1,那个对象没有它的名字。)

可以使用 来查看文档perldoc -F FILENAME,并会输出类似

Base
       A generic base class

   Base->Count
       Return the object count.

   $base->ID
       Give the unique ID of this object.

   $base->report
       Returns a string containing a short description.

Sub
       A generic subclass. It subclasses Base.

   $sub->Name [= SCALAR]
       Gets or sets the name of $sub.

           my $oldname = $sub->Name;
           $sub->name = "new name";

只有在 *nix 系统上才能更好地排版。

在 v5.12.4 下测试。

编辑:由内而外的对象

虽然由内而外的对象提供了更好的封装,但它们并不是一个好主意:难以理解、难以调试和难以继承,它们提供的问题多于解决方案。

{
   package Base;
   my $count = 0;
   sub new    { bless \do{my $o = $count++}, shift }
   sub Count  { $count }
   sub ID     { ${+shift} }
   sub report { my ($self) = @_; "I am the Base object ".($self->ID)."." }
}
{
   package Sub;
   my @_obj = ();
   my $count = 0;
   sub new    {
     my ($class) = @_;
     $count++;
     $_obj[$count - 1] = +{
       parent => Base->new(),
       Name   => undef,
     };
     bless \do{my $o = $count - 1}, shift;
   }
   sub Name :lvalue { $_obj[${+shift}]{Name} }
   sub AUTOLOAD {
     my $self = shift;
     my $package = __PACKAGE__ . "::";
     (my $meth = $AUTOLOAD) =~ s/^$package//;
     $_obj[$$self]{parent}->$meth(@_)
   }
   sub report {
     my ($self) = @_;
     "I am the Sub object ".($self->ID)." called ".($self->Name).".";
   }
}

此实现具有完全相同的接口,并以相同的输出完成测试用例。这个解决方案远非最佳,只支持单继承,做一些中间的事情(自动加载,动态方法调用),但它确实工作得令人惊讶。每个对象实际上只是对 ID 的引用,可用于查找包含字段的实际散列。无法从外部访问保存散列的数组。该类Base没有字段,因此不必创建对象数组。

Edit2:对象作为代码引用

另一个坏主意,但编写代码很有趣:

{
    package Base;
    my $count = 0;
    sub new {
        my ($class) = @_;
        my $id = $count++;
        bless sub {
            my ($field) = @_;
            die "Undefined field name" unless defined $field;
            if ($field eq "ID") { return $id }
            else                { die "Unrecognised name $field" }
        }, $class;
    }
    sub Count  { $count }
    sub ID     { my ($self) = @_; $self->("ID") }
    sub report { my ($self) = @_; "I am the Base object " . $self->ID . "." }
}
{
    package Sub;
    use parent -norequire, qw(Base);
    sub new {
        my ($class) = @_;
        my $name = undef;
        my $super = $class->SUPER::new;
        bless sub {
            my ($field, $val ) = @_;
            die "Undefined field name" unless defined $field;
            if ($field eq "Name") { defined $val ? $name = $val : $name }
            else                  { $super->(@_) }
        }, $class;
    }
    sub Name { my $self = shift; $self->("Name", @_) }
    sub report {
        my ($self) = @_;
        "I am the Sub object ".($self->ID)." called ".($self->Name).".";
    }
}

测试用例必须适应$sub2->Name("Fred"),并相应更新文档,因为我们不能在这里安全地使用左值注释。

于 2012-12-07T21:36:50.970 回答
3

引用perldoc perlmodlib,“Perl 不会像您在 C++、Ada 或 Modula-17 等其他语言中习惯的那样强制执行其模块的私有和公共部分。Perl 并不迷恋强制隐私。它更喜欢这样你离开它的客厅是因为你没有被邀请,而不是因为它有猎枪。”

Perl 中的标准约定是将所有内容放$self入哈希中并使用下划线前缀来指示哪些项目应被视为私有项目......然后信任该类的用户尊重该指示。同样的约定也适用于方法。如果您使用我的一个模块并且您选择偷看并$self直接修改或调用的内容$obj->_some_private_method,那么您将陷入困境并且可能会破坏某些东西,或者在此版本中正常工作的内容可能会在您升级时破坏到下一个版本;如果发生这种情况,您可以保留这两部分。

如果你要坚持让课堂之外的任何人都无法访问数据,有办法做到这一点,但是 a) 它们增加了复杂性,这在几乎所有情况下都是不必要的,并且 b) 正如你已经看到的,他们倾向于使继承变得更加麻烦。

那么,我的问题是,您实际上想要完成什么,为什么您觉得有必要让您的对象数据 Sooper-Sekret 并且完全无法访问?这样做你会获得什么好处,而不是简单地标记你认为应该被视为私人的事情,然后信任其他人不要管他们(除非他们有充分的理由不这样做)?

于 2012-12-07T22:38:38.030 回答
2

首先,我不确定您所说的“对用户隐藏”到底是什么意思,但看起来您可能正在寻找包作用域变量(our)与实例作用域。

package MyBaseClass;
use warnings;
use strict;

our $counter = 0;
sub new { 
    my $class = shift;
    $counter++;
    return bless {}, $class;
}
sub howManyInstances {
    return $counter;
}
1;

关于你的第二个问题,我不确定闭包与继承有什么关系。这是一个简单的子类:

package MySubClass;
use warnings;
use strict;
use parent 'MyBaseClass';  # use parent schema, don't mess with @ISA
sub new {
    my $class = shift;
    my $self = $class->SUPER::new(@_);
    $self->{_name} = undef;  
    return $self;
}
# Your setter/getter looks ok as is, though lowercase is tradional for methods/subs
1;

现在,如果这是真正的代码,你不会这样做——你会使用MooMoose

于 2012-12-07T21:23:13.137 回答