【问题标题】:Perl encapsulate class variable?Perl封装类变量?
【发布时间】:2012-11-26 01:17:27
【问题描述】:

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

【问题讨论】:

  • 我认为我们需要弄清楚您所说的“封装”是什么意思。如果您的意思是任何想要查看的人都可以访问$self 的内容这一事实,那么这就是该领域的一部分。基于散列的 Perl 对象没有其他语言可能理解的私有数据成员,并且尝试在 Perl 中生成这些功能不太可能以愉快的方式结束。
  • 是的,我的意思是我希望对象的属性不能在类之外访问。这绝对不是令人愉快的,我已经研究了 2 天的闭包之类的东西,但仍然不明白。
  • 我想在这种情况下,您必须构建自己的基于闭包的 OO 方法。这是一个奇怪的要求,特别是因为 Perl 在处理由内向外对象方面的经验是如此令人痛心,但这是可以做到的。
  • 更重要的是,您确定拒绝访问(而不仅仅是将它们记录为私有)是一项要求吗?有几种方法可以做到这一点,由内而外的对象更简单,但它们很难重新实现,并且对于大多数实际的编程任务来说不是必需的。
  • @darch 是的。由内而外的对象是否适用于继承?在子类中,添加新属性容易吗?

标签: perl oop inheritance closures encapsulation


【解决方案1】:

如果您使用的是裸 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 返回的祝福 sub 关闭了两个词法,$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

【讨论】:

    【解决方案2】:

    在 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:对象作为 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"),并相应更新文档,因为我们不能在这里安全地使用左值注释。

    【讨论】:

    • 感谢您的回答,听起来不错,但我仍然希望自己无法从课堂外访问,而不是仅将其记录为私有。
    • @maus 我添加了一个可怕的例子来说明由内向外对象的一种可能实现。类的合同没有改变,测试用例像以前一样运行。但是,代码很脆弱,很糟糕,不会在生产中使用。驼鹿可能是要走的路。
    【解决方案3】:

    引用perldoc perlmodlib,“Perl 不强制执行其模块的私有和公共部分,就像您在其他语言(如 C++、Ada 或 Modula-17)中可能习惯的那样。Perl 不喜欢强制执行隐私。它宁愿你呆在它的客厅外面,因为你没有被邀请,而不是因为它有猎枪。”

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

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

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

    【讨论】:

    • 就像我在问题中提到的,这是一项家庭作业。我认为我的导师对要求有点过火了。
    【解决方案4】:

    首先,我不确定您所说的“对用户隐藏”究竟是什么意思,但看起来您可能正在寻找包作用域变量 (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

    【讨论】:

    • 我希望从类外部无法访问这些属性。我试图通过祝福一个子例程来为此使用闭包,但是在子类中我不知道如何在调用超级构造函数后添加新属性,因为返回值不是哈希。
    • 要使 $counter 无法访问,只需将 our 替换为 my
    • 除非你的任务明确要求使用闭包或子类化一个有福的 coderef,否则不要。你可能找错树了。
    【解决方案5】:

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

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

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2011-09-26
      • 1970-01-01
      • 2023-03-15
      • 2014-12-09
      • 1970-01-01
      • 1970-01-01
      • 2018-10-11
      • 1970-01-01
      相关资源
      最近更新 更多