Perl面向对象动态方法

3
我希望能够动态地在类中定义方法。我正在编写一个跟踪器,比下面的代码更加复杂,它也具有状态感知功能,但这与我的问题无关。 我已经编写了一个TraceSlave类,其中trace方法调用sprintf,将换行符替换为文本\n,非常好。
基本上,我想要实例化我的跟踪器:
my @classes = qw(debug token line src match);
my $trace = Tracer->new(\@classes);

我应该能够调用跟踪器动态定义的方法:

$trace->debug("hello, world");
$trace->match("matched (%s)(%s)(%s)(%s)(%s)", $1, $2, $3, $4, $5);

因此,我的Tracer类将如下所示:

package Tracer;
  sub new {
    my $class = shift;
    my $self = {};
    my @traceClasses = @{$_[0]};
    bless $self, $class;
    for (@traceClasses) {
# This next line is wrong, and the core of my question
      $self->$_ = new TraceSlave($_, ...)->trace
    } # for (@traceClasses)
  }

这段代码无法编译。基本上,我想定义Tracer实例的方法,作为TraceSlave实例的跟踪方法;通过循环实现。

我可以使用AUTOLOAD或eval实现,但那样做是错误的。正确的方法是什么?

这里是完整的TraceSlave。它没问题。

package TraceSlave;
  sub new {
    my $self = { header => $_[1], states => $_[2], stateRef => $_[3] };
    bless $self, $_[0];
    return $self;
  } # new()

  sub trace {
    my $self = shift;
    my @states = @{$self->{states}};
    if ($states[${$self->{stateRef}}]) { # if trace enabled for this class and state
      my @args;
      for (1..$#_) { ($args[$_-1] = $_[$_]) =~ s/\n/\\n/g; } # Build args for sprintf, and replace \n in args
      print $self->{header}.sprintf($_[0], @args)."\n";
    }
  } # trace()

AUTOLOAD 函数有什么问题?http://perldoc.perl.org/AutoLoader.html - mpapec
我知道所有想要的方法的名称。它们都已被传递进来。AUTOLOAD 用于当你不知道可能被调用的方法的名称时。它会更慢,会有问题,并且仅在必要时才对(除非这是唯一的方法)。它难道不是主要用作错误处理程序的吗?我觉得 eval 比 AUTOLOAD 更好一些。此外,我想知道我的 Perl 引用理解错在哪里了。 - Francis Cagney
1
AUTOLOAD 应该只在每个方法的第一次调用时变慢。您可以在此首次调用中为所有未来的方法请求创建方法。 - mpapec
6个回答

2
每当我开始涉及类的运行时修改,我就会使用MOP和Moose。因此,如果我理解正确,您想要类似以下内容的东西。
package Tracer;
use strict;
use warnings;
use Moose;
use TraceSlave;

has 'classes' => ( is => 'ro', isa => 'ArrayRef[Str]', required => 1 );

### This is to allow ->new(\@classes) invocation instead of just
### using ->new( classes => \@classes) for invocation
around BUILDARGS => sub {
  my $orig  = shift;
  my $class = shift;

  if ( @_ == 1 && ref $_[0] eq 'ARRAY' ) {
    return $class->$orig( classes => $_[0] );
  }
  else {
    return $class->$orig(@_);
  }
};

sub BUILD {
  my $self = shift;
  for my $class (@{$self->classes}) {
    my $tracer = TraceSlave->new($class, ...);
    $self->meta->add_method( $class => sub { $tracer->trace(@_) } );
  }
}

虽然我相当确定它在内部只是做了相同的事情,最终会变成一个字符串评估。我还没有深入挖掘MOP内部。 (我也不确定这是否是正确的代码或使用Moose处理事情的最佳方法,所以买方自负。:))


1
忽略“tracer”问题的具体细节,你是否只是想能够为给定的包动态创建方法?这个怎么样?
sub new {
    my ($class, $trace_classes) = @_;

    # ...

    foreach my $tc (@$trace_classes) {
        no strict 'refs';
        *{"${class}::${tc}"} = sub {
            my $self = shift;
            # ...
        };
    }

    return $self;
}

虽然在new中这样做看起来非常奇怪!所以也许我错过了重点。


谢谢Unk,那几乎就是我想要的。我再次回答这个问题,因为我似乎只能在这些小框中输入评论。所以请往下看。 - Francis Cagney
再多说一句。你绝对是正确的,如果有多个Tracer实例,整个概念就会被打破,因为类而不是实例正在获取动态方法。然而,如果我们有多个Tracer实例,它们将共享相同的IO设备STDOUT,这也会相当混乱。OO仍然是正确的,因为Tracer实例需要一个处理跟踪状态的句柄,以便可以打开或关闭跟踪。TraceSlave还需要处理这些状态的句柄,以便可以进行跟踪或不进行跟踪。我将使用一组跟踪类来进行解析器,另一组跟踪类来进行编译器。 - Francis Cagney
好的,这是一个有价值的批评,但是我的当前系统实现了两个出色的目标:完全封装在一个引用中,并且具有清晰的客户端接口:$trace->src(...)。我认为这是无法在没有面向对象编程的情况下实现的,那么你的最佳实践是什么呢?我并不是在挖苦,我很尊重你的知识。 - Francis Cagney
此外,我上面的最终代码不起作用。*{"${class}::$tc"} = sub { $slave->trace(@[1..$#]); }。始终调用最后创建的从属。显然。eval允许我创建唯一的句柄。将左值直接分配给&$slave->trace也可以工作,但我无法使其工作。我觉得我正在快速学习Perl,但有时仍然感到文盲! - Francis Cagney
从我的角度来看...我会想要创建一个模块,可以按照以下方式使用:use Trace qw/ debug token line src match /;(在import中创建子程序),然后调用为Trace->debug(...);等。您仍然可以将状态封装在包内...但是我现在有点“不需要OO”的想法-所以请随意忽略 :) - Unk
显示剩余3条评论

0

好的,我已经完成了我的小模块,并学到了很多关于引用的知识。我正在动态定义方法,但是使用eval,我认为这非常不好。然而,似乎没有人有更好的想法,所以就这样吧。

创建动态方法的行在注释“#非常不好的动态方法创建,肯定有更好的方法吧?”之后。

所以我仍然很想听听有更好的方法。正如有人说的那样,我可以使用Moose,但是Moose也只会为我执行一个eval。

#!/usr/bin/perl
use strict;
use warnings;

package TraceSlave;
  sub new {
    my $self = { header => $_[1], states => $_[2], stateRef => $_[3] };
    bless $self, $_[0];
    return $self;
  } # new()

  sub trace {
    my $self = shift;
    if ($self->{states}->[${$self->{stateRef}}]) {  # if trace enabled for this class and state
      my @args;
      for (1..$#_) { ($args[$_-1] = $_[$_]) =~ s/\n/\\n/g; } # Build args for sprintf, and replace \n in args
      print $self->{header}.sprintf($_[0], @args)."\n";
    }
  } # trace()

package Tracer;
  sub new {
    my ($class, $classList, $stateCount, $stateRef) = @_;
    my $self = { states => {}, slaves => [], count => $stateCount };
    my @classes = split(/\s+/, $classList);
    my $maxlen = 0;

    for (@classes) { # loop over all trace classes to find longest
      my $len = length($_);
      $maxlen = $len if $len > $maxlen;
    }
    $maxlen++; # Add a space

    for (0..$#classes) { # loop over all trace classes, and eval create a slave for each class
      my $tc = $classes[$_];
      my $states = $self->{states}->{$tc} = [];
      for (0..$stateCount) { $states->[$_] = 0; }
      $self->{slaves}[$_] = TraceSlave->new( "$tc:"." "x($maxlen-length($tc)), $states, $stateRef );
# Very dirty creation of dynamic method, surely there's a better way?
      eval("sub $tc { ".'$self=shift; $self->{slaves}['.$_.']->trace(@_); }');
    } # for (0..$#classes)
    bless $self, $class;
    return $self;
  } # new()

  sub _onOff { # switch on traces
    my ($self, $onOff, $classList, $statesRef) = @_;
    my @classes = split(/\s+/, $classList);
    my $count = $self->{count};

    for (@classes) { # loop over all supplied trace classes and switch on/off for state list
      my $states = $self->{states}->{$_};
      if ($statesRef) { for (@$statesRef) { $states->[$_] = $onOff; } }
      else { for (0..$count) { $states->[$_] = 1; } } # switch on for all states is no state list
    } # for (0..$#classes)
  } # on()

  sub on {
    my $self = shift;
    $self->_onOff( 1, @_ );  
  }

  sub off {
    my $self = shift;
    $self->_onOff( 0, @_ );  
  }
1;

0

@Unk 对于Unk上面的小代码片段的回答。

  foreach my $tc (@classes) { # loop over all trace classes, and create a slave for each class
    my $states = $self->{states}->{$tc} = [];
    $slave = TraceSlave->new( "$tc:"." "x($maxlen-length($tc)), $states, $stateRef );
    no strict 'refs';
    *{"${class}::$tc"} = sub { $slave->trace(@_[1..$#_]); }
  } # foreach my $tc (@classes)
  bless $self, $class;
  return $self;
} # new()

这样做肯定更加简洁。我不再需要在 Tracer $self 中拥有从属了。

然而,我应该能够用整个子程序替换它:

*{"${class}::${tc}"} = $slave->trace;

当然这是行不通的,因为我需要一个对$slave->trace的引用,上面的代码只会调用它。不幸的是,我也不理解glob的用法,或者lvalue上的引用内容。为自己辩护,我可以在C中使用指针或在javascript中使用引用来做任何事情,但perl引用相当复杂。仍在学习中。

我认为面向对象是正确的方法,因为跟踪器有很多私有数据,特别是哪些状态下开启了跟踪类,当然还有每个跟踪类的头文件。如果我能让上面的简单赋值工作,嵌套对象也是正确的。

所有这些都是我为我的VHDL项目编写make系统的一部分。我强迫自己使用perl,因为我认为是时候好好学习这门语言了,而不是写10行的临时解决方案。

我现在意识到*{"${class}::${tc}"} = \&trace->slave可能永远不会起作用。第一个参数是Tracer实例还是TraceSlave实例?要使其工作,它必须是TraceSlave实例,但这是在Tracer类中定义的方法。当然,我可以把从属关系放回Tracer $self中,但这会使事情变得更加复杂而不是简单。

我想现在它以尽可能简单的方式实现了我想要的功能。


你可能想在for循环内使用my $slave = TraceSlave->new(...);。匿名子程序(闭包)正在关闭$slave,但是您正在每次迭代时对其进行转换。您希望每次都关闭一个新的slave。关于\&$slave->trace - 您只能引用子例程本身。您应该将$slave->trace(@args)视为TraceSlave::trace($slave, @args)。显然,后者是方法调用而不是方法本身。这里绝对需要sub { ... }; - Unk
在“my”中加入一个后缀使它起作用了。神奇!实际上,我确实理解“my”的工作原理,但其他部分对我来说就像魔法一样。我不能声称理解*{"${class}::$tc"}的工作原理,特别是为什么需要全局变量和双引号。::${tc}可以简化为::$tc,为什么我也不明白。我看到TraceSlave::trace(..)没有额外的参数,而TraceSlave->trace(..)确实得到了类名。很有趣。在JavaScript中,我会做window.onload = new Thing(...).loader。非常优雅的js为期望相同的事物分配方法引用!抱歉,这是无意义的观察。 - Francis Cagney

0
如果您修改软件包的符号表以添加类的方法,则不能具有具有不同语义的两个实例具有相同命名的方法。在这种情况下使用AUTOLOAD对我来说似乎完全没问题。
敬礼,马蒂亚斯

我不理解这个注释。如果您查看此线程的顶部,我解释了创建的方法是由传递给new()的列表命名的。这些动态创建的方法中的每一个都执行相同的操作。通过sprintf进行跟踪或不跟踪,并根据状态和跟踪类别进行某种形式的chomp。我知道要创建的所有方法的名称,因此AUTOLOAD似乎仍然是错误的。 - Francis Cagney
@FrancisCagney 我现在明白了:你知道所有方法的名称和行为。你只是想生成所有需要的方法,这样你就不必编写它们。是这样吗?那么我的评论就不合适了。现在我看到这已经在Unk的另一个评论中讨论过了。 - Matthias
有点。在编写Tracer时,它不知道它的动态方法。但是动态方法作为字符串列表传递给new()。 - Francis Cagney

0

我通常使用字符串 eval 来定义子程序:

for my $method (@classes) {
    eval "sub $method { 'TraceSlave'->new('$method', ...)->trace }";
}

如果我得不到更好的答案,那就是我要做的事情了。但是肯定有正确的方法来完成它吧???我在这方面有点龟毛,总是过于追求完美。 - Francis Cagney
@FrancisCagney:如果你知道@classes里面是什么,那么这种方法并没有任何“不对”的地方。 - choroba
我在原帖中提到了AUTOLOAD和eval都是解决方案。但我想知道如何动态定义类方法。我想我可以只定义一个方法哈希表,完全忘记面向对象。 - Francis Cagney
2
为什么要使用字符串评估?使用像Perldoc中的Glob类型会更好吗? - mpapec
@mpapec:这取决于子程序的复杂程度。 - choroba
显示剩余3条评论

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