Perl封装了类变量?

时间:2012-12-07 20:05:54

标签: perl oop inheritance closures encapsulation

我对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;

5 个答案:

答案 0 :(得分: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->CountBase->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);
}

这里发生的事情是new返回的受祝福的子关闭了两个词法$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

答案 1 :(得分:3)

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

在perl中,通常会在$self哈希中存储实例变量,而不会隐藏它们,尽管有时会使用前缀来避免冲突。它们受到约定的保护(依赖于实现细节是不安全的,因为它们可能会更改)而不是语言功能。

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

答案 2 :(得分:3)

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

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

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

{
  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:对象作为coderefs

又一个坏主意,但代码很有趣:

{
    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"),文档也相应更新,因为我们不能安全地使用左值注释。

答案 3 :(得分:3)

引用perldoc perlmodlib," Perl不强制执行其模块的私有和公共部分,因为您可能习惯于其他语言,如C ++,Ada或Modula-17。 Perl并没有对强制隐私感到迷恋。因为你没被邀请,所以你宁愿离开它的起居室,也不是因为它有霰弹枪。"

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

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

那么,我的问题是,你实际上是想要实现什么,为什么你觉得需要制作你的对象数据Sooper-Sekret并且完全无法访问?通过简单地标记您认为应该被视为私有的东西,然后信任他人让他们独自(除非他们有充分理由不这样做),您将获得什么好处?

答案 4 :(得分: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