内容

名称

perlobj - Perl 对象引用

说明

本文档提供了 Perl 面向对象特性的参考。如果您正在寻找有关 Perl 中面向对象编程的介绍,请参阅 perlootut

为了理解 Perl 对象,您首先需要理解 Perl 中的引用。有关详细信息,请参阅 perlreftut

本文档从头开始描述了 Perl 的所有面向对象 (OO) 特性。如果您只想编写一些您自己的面向对象代码,那么您最好使用 perlootut 中描述的 CPAN 中的一个对象系统。

如果您希望编写您自己的对象系统,或者您需要维护从头开始实现对象的代码,那么本文档将帮助您确切了解 Perl 如何进行面向对象。

有一些基本原则定义了面向对象 Perl

  1. 对象只是一个知道它属于哪个类的数据结构。

  2. 类只是一个包。类提供了期望对对象进行操作的方法。

  3. 方法只是一个子例程,它期望将对对象的引用(或对于类方法,为包名称)作为第一个参数。

让我们深入了解这些原则中的每一个。

对象只是一个数据结构

与支持面向对象的许多其他语言不同,Perl 没有提供任何用于构造对象的特殊语法。对象仅仅是已明确与特定类关联的 Perl 数据结构(哈希、数组、标量、文件句柄等)。

这种显式关联是由内置的 bless 函数创建的,该函数通常在类的 构造函数 子例程中使用。

这是一个简单的构造函数

package File;

sub new {
    my $class = shift;

    return bless {}, $class;
}

名称 new 并不特殊。我们可以将我们的构造函数命名为其他名称

package File;

sub load {
    my $class = shift;

    return bless {}, $class;
}

OO 模块的现代惯例是始终使用 new 作为构造函数的名称,但没有这样做要求。任何将数据结构祝福为类的子例程在 Perl 中都是有效的构造函数。

在前面的示例中,{} 代码创建对一个空匿名哈希的引用。然后,bless 函数获取该引用并将哈希与 $class 中的类关联起来。在最简单的情况下,$class 变量最终将包含字符串“File”。

我们还可以使用一个变量来存储对作为对象祝福的数据结构的引用

sub new {
    my $class = shift;

    my $self = {};
    bless $self, $class;

    return $self;
}

一旦我们祝福了 $self 引用的哈希,我们就可以开始调用它的方法。如果你想将对象初始化放在它自己的单独方法中,这将很有用

sub new {
    my $class = shift;

    my $self = {};
    bless $self, $class;

    $self->_initialize();

    return $self;
}

由于对象也是一个哈希,你可以将它作为一个哈希来对待,使用它来存储与对象关联的数据。通常,类内部的代码可以将哈希视为一个可访问的数据结构,而类外部的代码应该始终将对象视为不透明的。这称为封装。封装意味着对象的使用者不必知道它是如何实现的。用户只需调用对象上的已记录方法。

但是,请注意(与大多数其他 OO 语言不同),Perl 不会以任何方式确保或强制封装。如果你希望对象实际上不透明的,你需要自己安排。这可以通过多种方式完成,包括使用 "Inside-Out objects" 或来自 CPAN 的模块。

对象是祝福的;变量不是

当我们祝福某物时,我们并不是祝福包含对该事物引用的变量,我们也不是祝福变量存储的引用;我们祝福的是变量引用的事物(有时称为引用项)。这段代码最好地展示了这一点

use Scalar::Util 'blessed';

my $foo = {};
my $bar = $foo;

bless $foo, 'Class';
print blessed( $bar ) // 'not blessed';    # prints "Class"

$bar = "some other value";
print blessed( $bar ) // 'not blessed';    # prints "not blessed"

当我们对变量调用 bless 时,我们实际上祝福的是变量引用的底层数据结构。我们没有祝福引用本身,也没有祝福包含该引用的变量。这就是为什么对 blessed( $bar ) 的第二次调用返回 false 的原因。此时,$bar 不再存储对对象的引用。

有时您会看到较旧的书籍或文档中提到“引用祝福”或将对象描述为“祝福引用”,但这并不正确。作为对象被祝福的不是引用,而是引用所指的东西(即指称物)。

类仅仅是一个包

Perl 没有提供任何用于类定义的特殊语法。包仅仅是一个包含变量和子例程的命名空间。唯一的区别是,在类中,子例程可能将对对象的引用或类的名称作为第一个参数。这纯粹是约定问题,因此类可能包含方法和子例程,它们对对象或类进行操作。

每个包都包含一个名为 @ISA 的特殊数组。@ISA 数组包含该类的父类列表(如果存在)。当 Perl 执行方法解析时,将检查此数组,我们将在后面介绍这一点。

当然,从包中调用方法意味着它必须被加载,因此您通常希望加载模块并同时将其添加到 @ISA。您可以使用 parent 实用程序一步完成此操作。(在较旧的代码中,您可能会遇到 base 实用程序,现在不建议使用,除非您必须使用同样不建议使用的 fields 实用程序。)

但是,无论如何设置父类,包的 @ISA 变量都将包含这些父类的列表。这仅仅是一个标量列表,其中每个标量都是对应于包名称的字符串。

所有类都隐式继承自 UNIVERSAL 类。UNIVERSAL 类由 Perl 核心实现,并提供几个默认方法,例如 isa()can()VERSION()UNIVERSAL永远不会出现在包的 @ISA 变量中。

Perl 提供方法继承作为内置功能。属性继承留给类来实现。有关详细信息,请参阅 "编写访问器" 部分。

方法仅仅是一个子例程

Perl 不提供任何用于定义方法的特殊语法。方法只是一个常规子例程,并使用 sub 声明。方法的特殊之处在于它期望将对象或类名作为其第一个参数接收。

Perl 确实 提供用于方法调用的特殊语法,即 -> 运算符。我们将在后面详细介绍这一点。

您编写的大多数方法都期望对对象进行操作

sub save {
    my $self = shift;

    open my $fh, '>', $self->path() or die $!;
    print {$fh} $self->data()       or die $!;
    close $fh                       or die $!;
}

方法调用

在对象上调用方法写为 $object->method

方法调用(或箭头)运算符的左侧是对象(或类名),右侧是方法名。

my $pod = File->new( 'perlobj.pod', $data );
$pod->save();

在解除引用的引用时也使用 -> 语法。它看起来像同一个运算符,但它们是两个不同的操作。

当您调用方法时,箭头左侧的内容作为第一个参数传递给该方法。这意味着当我们调用 Critter->new() 时,new() 方法将字符串 "Critter" 作为其第一个参数接收。当我们调用 $fred->speak() 时,$fred 变量将作为第一个参数传递给 speak()

就像任何 Perl 子例程一样,@_ 中传递的所有参数都是原始参数的别名。这包括对象本身。如果您直接赋值给 $_[0],您将更改保存对该对象的引用的变量的内容。我们建议您不要这样做,除非您确切知道自己在做什么。

Perl 通过查看箭头的左侧来了解方法所在的包。如果左侧是包名,它将在该包中查找方法。如果左侧是一个对象,那么 Perl 将在对象已被祝福进入的包中查找方法。

如果左侧既不是包名也不是对象,那么方法调用将导致错误,但请参阅 "方法调用变体" 部分以了解更细微的差别。

继承

我们已经讨论了特殊的 @ISA 数组和 parent 实用程序。

当一个类从另一个类继承时,父类中定义的任何方法都可供子类使用。如果您尝试在对象上调用其自身类中未定义的方法,Perl 还会在它可能拥有的任何父类中查找该方法。

package File::MP3;
use parent 'File';    # sets @File::MP3::ISA = ('File');

my $mp3 = File::MP3->new( 'Andvari.mp3', $data );
$mp3->save();

由于我们没有在 File::MP3 类中定义 save() 方法,因此 Perl 将查看 File::MP3 类的父类以查找 save() 方法。如果 Perl 在继承层次结构中的任何地方都找不到 save() 方法,它将终止。

在这种情况下,它在 File 类中找到了一个 save() 方法。请注意,即使该方法是在 File 类中找到的,但传递给 save() 的对象仍然是 File::MP3 对象。

我们可以在子类中覆盖父类的某个方法。当我们这样做时,我们仍然可以使用 SUPER 伪类来调用父类的方法。

sub save {
    my $self = shift;

    say 'Prepare to rock';
    $self->SUPER::save();
}

SUPER 修饰符只能用于方法调用。你不能将它用于常规子例程调用或类方法

SUPER::save($thing);     # FAIL: looks for save() sub in package SUPER

SUPER->save($thing);     # FAIL: looks for save() method in class
                         #       SUPER

$thing->SUPER::save();   # Okay: looks for save() method in parent
                         #       classes

SUPER 是如何解析的

SUPER 伪类是从调用该方法的包中解析出来的。它不是根据对象的类来解析的。这一点很重要,因为它允许在深层继承层次结构中的不同级别的每个方法都能正确地调用它们各自的父方法。

package A;

sub new {
    return bless {}, shift;
}

sub speak {
    my $self = shift;

    say 'A';
}

package B;

use parent -norequire, 'A';

sub speak {
    my $self = shift;

    $self->SUPER::speak();

    say 'B';
}

package C;

use parent -norequire, 'B';

sub speak {
    my $self = shift;

    $self->SUPER::speak();

    say 'C';
}

my $c = C->new();
$c->speak();

在这个示例中,我们将得到以下输出

A
B
C

这演示了 SUPER 是如何解析的。即使对象被祝福为 C 类,B 类中的 speak() 方法仍然可以调用 SUPER::speak(),并期望它正确地查找 B 的父类(即方法调用所在的类),而不是 C 的父类(即对象所属的类)。

在极少数情况下,这种基于包的解析可能会成为一个问题。如果你将一个子例程从一个包复制到另一个包,那么 SUPER 解析将基于原始包进行。

多重继承

多重继承通常表示设计问题,但如果你要求,Perl 总是会给你足够的绳子让你自缢。

要声明多个父类,你只需将多个类名传递给 use parent

package MultiChild;

use parent 'Parent1', 'Parent2';

方法解析顺序

方法解析顺序只在多重继承的情况下才有意义。在单继承的情况下,Perl 只需查找继承链即可找到一个方法

Grandparent
  |
Parent
  |
Child

如果我们在 Child 对象上调用一个方法,并且该方法未在 Child 类中定义,Perl 将在 Parent 类中查找该方法,然后在必要时在 Grandparent 类中查找。

如果 Perl 在这些类中找不到该方法,它将死掉并显示一条错误消息。

当一个类有多个父类时,方法查找顺序变得更加复杂。

默认情况下,Perl 从左到右对方法进行深度优先搜索。这意味着它从 @ISA 数组中的第一个父类开始,然后搜索它的所有父类、祖父母等。如果它找不到该方法,它将转到原始类的 @ISA 数组中的下一个父类,并从那里开始搜索。

          SharedGreatGrandParent
          /                    \
PaternalGrandparent       MaternalGrandparent
          \                    /
           Father        Mother
                 \      /
                  Child

因此,给定上面的图表,Perl 将搜索 ChildFatherPaternalGrandparentSharedGreatGrandParentMother,最后是 MaternalGrandparent。这可能是个问题,因为现在我们在检查所有派生类(即在尝试 MotherMaternalGrandparent 之前)之前就正在 SharedGreatGrandParent 中查找。

可以使用 mro 实用工具请求不同的方法解析顺序。

package Child;

use mro 'c3';
use parent 'Father', 'Mother';

此实用工具允许您切换到“C3”解析顺序。简单来说,“C3”顺序确保在子类之前绝不会搜索共享父类,因此 Perl 现在将搜索:ChildFatherPaternalGrandparentMother MaternalGrandparent,最后是 SharedGreatGrandParent。但请注意,这不是“广度优先”搜索:在考虑任何 Mother 祖先之前,将搜索所有 Father 祖先(除了公共祖先)。

C3 顺序还允许您使用 next 伪类在同级类中调用方法。有关此功能的更多详细信息,请参阅 mro 文档。

方法解析缓存

当 Perl 搜索方法时,它会缓存查找结果,以便将来对该方法的调用无需再次搜索它。更改类的父类或向类添加子例程将使该类的缓存无效。

mro 实用工具提供了一些函数,用于直接操作方法缓存。

编写构造函数

如前所述,Perl 没有提供特殊的构造函数语法。这意味着类必须实现自己的构造函数。构造函数只是一个返回对新对象的引用的类方法。

构造函数还可以接受定义对象的附加参数。让我们为我们之前使用的 File 类编写一个真正的构造函数

package File;

sub new {
    my $class = shift;
    my ( $path, $data ) = @_;

    my $self = bless {
        path => $path,
        data => $data,
    }, $class;

    return $self;
}

如您所见,我们已将路径和文件数据存储在对象本身中。请记住,在底层,此对象仍然只是一个哈希。稍后,我们将编写访问器来操作此数据。

对于我们的 File::MP3 类,我们可以检查以确保我们给定的路径以 “.mp3” 结尾

package File::MP3;

sub new {
    my $class = shift;
    my ( $path, $data ) = @_;

    die "You cannot create a File::MP3 without an mp3 extension\n"
        unless $path =~ /\.mp3\z/;

    return $class->SUPER::new(@_);
}

此构造函数允许其父类执行实际的对象构造。

属性

属性是属于特定对象的数据片段。与大多数面向对象语言不同,Perl 不提供用于声明和操作属性的特殊语法或支持。

属性通常存储在对象本身中。例如,如果对象是匿名哈希,我们可以使用属性名称作为键,将属性值存储在哈希中。

虽然可以直接在类外部引用这些哈希键,但将所有对属性的访问都包装在访问器方法中被认为是最佳实践。

这有几个优点。访问器使以后更改对象的实现变得更加容易,同时仍保留原始 API。

访问器允许您在属性访问周围添加其他代码。例如,您可以将默认值应用于未在构造函数中设置的属性,或者您可以验证属性的新值是否可接受。

最后,使用访问器使继承变得更加简单。子类可以使用访问器,而不必知道父类如何在内部实现。

编写访问器

与构造函数一样,Perl 不提供特殊的访问器声明语法,因此类必须提供明确编写的访问器方法。有两种常见的访问器类型,只读和读写。

一个简单的只读访问器只获取单个属性的值

sub path {
    my $self = shift;

    return $self->{path};
}

读写访问器将允许调用者设置值并获取值

sub path {
    my $self = shift;

    if (@_) {
        $self->{path} = shift;
    }

    return $self->{path};
}

关于更智能和更安全代码的旁注

我们的构造函数和访问器不是很智能。它们不检查是否定义了 $path,也不检查 $path 是否是有效的文件系统路径。

手动执行这些检查会很快变得乏味。手动编写一堆访问器也令人难以置信地乏味。CPAN 上有很多模块可以帮助您编写更安全、更简洁的代码,包括我们在 perlootut 中推荐的模块。

方法调用变体

除了我们到目前为止看到的 $object->method() 用法之外,Perl 还支持多种其他方法来调用方法。

具有完全限定名称的方法名称

Perl 允许您使用其完全限定名称(包和方法名称)来调用方法

my $mp3 = File::MP3->new( 'Regin.mp3', $data );
$mp3->File::save();

当您调用完全限定的方法名称(如 File::save)时,用于 save 方法的方法解析搜索将从 File 类开始,跳过 File::MP3 类可能已定义的任何 save 方法。如有必要,它仍会搜索 File 类的父类。

虽然此功能最常用于显式调用从祖先类继承的方法,但没有技术限制强制执行此操作

my $obj = Tree->new();
$obj->Dog::bark();

这将调用 Tree 类的对象上的 Dog 类的 bark 方法,即使这两个类完全不相关。请谨慎使用此功能。

前面描述的 SUPER 伪类使用完全限定名称调用方法不同。有关详细信息,请参阅前面的 "继承" 部分。

作为字符串的方法名称

Perl 允许您使用包含字符串的标量变量作为方法名称

my $file = File->new( $path, $data );

my $method = 'save';
$file->$method();

这与调用 $file->save() 完全相同。这对于编写动态代码非常有用。例如,它允许您将要调用的方法名称作为参数传递给另一个方法。

作为字符串的类名称

Perl 还允许您使用包含字符串的标量作为类名称

my $class = 'File';

my $file = $class->new( $path, $data );

同样,这允许非常动态的代码。

作为方法的子例程引用

您还可以使用子例程引用作为方法

my $sub = sub {
    my $self = shift;

    $self->save();
};

$file->$sub();

这完全等同于编写 $sub->($file)。您可能会在野外看到此惯用法与对 can 的调用结合使用

if ( my $meth = $object->can('foo') ) {
    $object->$meth();
}

取消引用方法调用

Perl 还允许您在方法调用中使用取消引用的标量引用。那是一口,让我们看看一些代码

$file->${ \'save' };
$file->${ returns_scalar_ref() };
$file->${ \( returns_scalar() ) };
$file->${ returns_ref_to_sub_ref() };

如果解引用产生一个字符串一个子例程引用,则此方法有效。

文件句柄上的方法调用

在底层,Perl 文件句柄是 IO::HandleIO::File 类的实例。一旦打开文件句柄,你就可以对其调用方法。此外,你还可以对 STDINSTDOUTSTDERR 文件句柄调用方法。

open my $fh, '>', 'path/to/file';
$fh->autoflush();
$fh->print('content');

STDOUT->autoflush();

调用类方法

由于 Perl 允许你对包名称和子例程名称使用裸字,因此它有时会错误地解释裸字的含义。例如,结构 Class->new() 可以解释为 'Class'->new()Class()->new()。用英语来说,第二个解释读作“调用名为 Class() 的子例程,然后在 Class() 的返回值上调用 new() 作为方法”。如果当前命名空间中有一个名为 Class() 的子例程,Perl 总是会将 Class->new() 解释为第二个备选方案:对由调用 Class() 返回的对象调用 new()

你可以通过两种方式强制 Perl 使用第一个解释(即作为对名为“Class”的类的类方法调用)。首先,你可以在类名后附加 ::

Class::->new()

Perl 总是会将其解释为方法调用。

或者,你可以引用类名

'Class'->new()

当然,如果类名在标量中,Perl 也会做正确的事情

my $class = 'Class';
$class->new();

间接对象语法

在文件句柄情况之外,不鼓励使用此语法,因为它会混淆 Perl 解释器。有关更多详细信息,请参见下文。

Perl 支持另一种称为“间接对象”表示法的方法调用语法。此语法被称为“间接”,因为方法出现在它被调用的对象之前。

此语法可用于任何类或对象方法

my $file = new File $path, $data;
save $file;

由于以下几个原因,我们建议你避免使用此语法。

首先,它可能难以阅读。在上面的示例中,不清楚 saveFile 类提供的方法还是仅仅是一个将文件对象作为其第一个参数的子例程。

当与类方法一起使用时,问题甚至更糟。由于 Perl 允许子例程名称写为裸字,因此 Perl 必须猜测方法后的裸字是类名还是子例程名。换句话说,Perl 可以将语法解析为 File->new( $path, $data ) new( File( $path, $data ) )

为了解析此代码,Perl 使用基于它已看到的包名称、当前包中存在的子例程、它之前看到的裸字以及其他输入的启发式方法。不用说,启发式方法会产生非常令人惊讶的结果!

较早的文档(和一些 CPAN 模块)鼓励使用此语法,特别是对于构造函数,因此你仍然可以在野外找到它。但是,我们鼓励你在新代码中避免使用它。

你可以通过在裸字后附加“::”来强制 Perl 将裸字解释为类名,就像我们之前看到的

my $file = new File:: $path, $data;

间接对象语法仅在启用 "indirect" 命名功能时可用。此功能默认启用,但可根据请求禁用。此功能存在于较旧的功能版本包中,但已从 :5.36 包中移除;因此,v5.36 或更高版本的 use VERSION 声明也将禁用此功能。

use v5.36;
# indirect object syntax is no longer available

blessblessedref

如前所述,对象只是一个通过 bless 函数祝福到类中的数据结构。bless 函数可以接受一个或两个参数

my $object = bless {}, $class;
my $object = bless {};

在第一种形式中,匿名哈希被祝福到 $class 中的类中。在第二种形式中,匿名哈希被祝福到当前包中。

强烈不建议使用第二种形式,因为它破坏了子类重用父类构造函数的能力,但您仍然可以在现有代码中遇到它。

如果您想知道某个标量是否引用对象,可以使用 Perl 核心附带的 Scalar::Util 导出的 blessed 函数。

use Scalar::Util 'blessed';

if ( defined blessed($thing) ) { ... }

如果 $thing 引用对象,则此函数返回对象已被祝福到的包的名称。如果 $thing 不包含对祝福对象的引用,则 blessed 函数返回 undef

请注意,如果 $thing 已被祝福到名为“0”的类中,则 blessed($thing) 也将返回 false。这是可能的,但相当病态。除非您知道自己在做什么,否则不要创建名为“0”的类。

同样,Perl 的内置 ref 函数对对祝福对象的引用进行了特殊处理。如果您调用 ref($thing) 并且 $thing 持有对对象的引用,它将返回对象已被祝福到的类的名称。

如果您只想检查变量是否包含对象引用,我们建议您使用 defined blessed($object),因为 ref 为所有引用(不仅仅是对象)返回真值。

UNIVERSAL 类

所有类自动继承自 Perl 核心内置的 UNIVERSAL 类。此类提供许多方法,所有这些方法都可以对类或对象调用。您还可以选择在您的类中覆盖其中一些方法。如果您这样做,我们建议您遵循下面描述的内置语义。

isa($class)

如果对象是 $class 中的类的成员或 $class 的子类的成员,则 isa 方法返回 true

如果您覆盖此方法,它绝不应抛出异常。

DOES($role)

如果其对象声称执行角色 $role,则 DOES 方法返回 true。默认情况下,这等效于 isa。此方法供实现角色的对象系统扩展使用,例如 MooseRole::Tiny

您还可以在自己的类中直接覆盖 DOES。如果您覆盖此方法,它绝不应抛出异常。

can($method)

can 方法检查它被调用的类或对象是否具有名为 $method 的方法。这将在类及其所有父类中检查该方法。如果该方法存在,则返回对子例程的引用。如果不存在,则返回 undef

如果您的类通过 AUTOLOAD 响应方法调用,您可能希望重载 can 以返回您的 AUTOLOAD 方法处理的方法的子例程引用。

如果您覆盖此方法,它绝不应抛出异常。

VERSION($need)

VERSION 方法返回类的版本号(包)。

如果给出了 $need 参数,它将检查当前版本(由包中的 $VERSION 变量定义)是否大于或等于 $need;如果不是这种情况,它将终止。此方法由 useVERSION 形式自动调用。

use Package 1.2 qw(some imported subs);
# implies:
Package->VERSION(1.2);

我们建议你使用此方法来访问另一个包的版本,而不是直接查看 $Package::VERSION。你正在查看的包可能已经覆盖了 VERSION 方法。

我们还建议使用此方法来检查模块是否具有足够版本。内部实现使用 version 模块来确保正确比较不同类型的版本号。

AUTOLOAD

如果你调用一个在类中不存在的方法,Perl 将抛出一个错误。但是,如果该类或其任何父类定义了一个 AUTOLOAD 方法,则会调用 AUTOLOAD 方法。

AUTOLOAD 被调用为常规方法,调用者不会知道区别。你的 AUTOLOAD 方法返回的任何值都会返回给调用者。

$AUTOLOAD 包全局中可用于你的类的完全限定方法名称。由于这是一个全局变量,如果你想在 strict 'vars' 下引用它而不使用包名称前缀,则需要声明它。

# XXX - this is a terrible way to implement accessors, but it makes
# for a simple example.
our $AUTOLOAD;
sub AUTOLOAD {
    my $self = shift;

    # Remove qualifier from original method name...
    my $called =  $AUTOLOAD =~ s/.*:://r;

    # Is there an attribute of that name?
    die "No such attribute: $called"
        unless exists $self->{$called};

    # If so, return it...
    return $self->{$called};
}

sub DESTROY { } # see below

如果没有 our $AUTOLOAD 声明,则此代码将无法在 strict pragma 下编译。

正如注释中所说,这不是实现访问器的理想方式。它很慢,而且太聪明了。但是,你可能会将此视为在较旧的 Perl 代码中提供访问器的一种方式。有关 Perl 中面向对象编码的建议,请参阅 perlootut

如果你的类确实有一个 AUTOLOAD 方法,我们强烈建议你在类中也覆盖 can。你的覆盖 can 方法应为你的 AUTOLOAD 响应的任何方法返回一个子例程引用。

析构函数

当对对象的最后一个引用消失时,对象将被销毁。如果你只有一个引用存储在词法标量中的对象,则当该标量超出范围时,对象将被销毁。如果你将对象存储在包全局变量中,则该对象可能直到程序退出才会超出范围。

如果你想在对象被销毁时执行某些操作,可以在类中定义一个 DESTROY 方法。除非该方法为空,否则 Perl 将始终在适当的时候调用此方法。

这就像任何其他方法一样被调用,对象作为第一个参数。它不会接收任何其他参数。但是,$_[0] 变量在析构函数中将是只读的,因此你不能为它赋值。

如果你的 DESTROY 方法抛出一个异常,这不会导致超出退出该方法的任何控制转移。该异常将作为警告报告给 STDERR,标记为 “(在清理中)”,并且 Perl 将继续执行它之前正在执行的操作。

因为 DESTROY 方法可以在任何时候被调用,所以你应该将任何可能被你的 DESTROY 方法中的任何操作设置的全局状态变量本地化。如果你对某个特定状态变量有疑问,本地化它不会有什么坏处。有五个全局状态变量,最安全的方法是将它们全部本地化

sub DESTROY {
    local($., $@, $!, $^E, $?);
    my $self = shift;
    ...;
}

如果你在你的类中定义了一个 AUTOLOAD,那么 Perl 将调用你的 AUTOLOAD 来处理 DESTROY 方法。你可以通过定义一个空的 DESTROY 来防止这种情况,就像我们在自动加载示例中所做的那样。你还可以检查 $AUTOLOAD 的值,并在被调用来处理 DESTROY 时不执行任何操作而返回。

全局销毁

在程序退出之前的全局销毁过程中销毁对象时的顺序是不可预测的。这意味着你的对象包含的任何对象可能已经被销毁。你应该检查一个包含的对象是否已定义,然后再调用它的方法

sub DESTROY {
    my $self = shift;

    $self->{handle}->close() if $self->{handle};
}

你可以使用 ${^GLOBAL_PHASE} 变量来检测你当前是否处于全局销毁阶段

sub DESTROY {
    my $self = shift;

    return if ${^GLOBAL_PHASE} eq 'DESTRUCT';

    $self->{handle}->close();
}

请注意,此变量是在 Perl 5.14.0 中添加的。如果你想在较旧版本的 Perl 中检测全局销毁阶段,则可以在 CPAN 上使用 Devel::GlobalDestruction 模块。

如果你的 DESTROY 方法在全局销毁期间发出警告,Perl 解释器将在警告中附加字符串“在全局销毁期间”。

在全局销毁期间,Perl 将始终在取消祝福引用之前对对象进行垃圾回收。有关全局销毁的更多信息,请参阅 perlhacktips 中的“PERL_DESTRUCT_LEVEL”

非哈希对象

到目前为止,所有示例都显示了基于祝福哈希的对象。但是,可以祝福任何类型的数据结构或引用,包括标量、全局变量和子例程。在查看代码时,你可能会看到这种情况。

以下是一个作为祝福标量的模块示例

package Time;

use v5.36;

sub new {
    my $class = shift;

    my $time = time;
    return bless \$time, $class;
}

sub epoch {
    my $self = shift;
    return $$self;
}

my $time = Time->new();
print $time->epoch();

反向对象

过去,Perl 社区尝试过一种称为“内向对象”的技术。内向对象将其数据存储在对象引用外部,根据对象的唯一属性(例如其内存地址)进行索引,而不是存储在对象本身中。这具有强制封装对象属性的优点,因为其数据未存储在对象本身中。

这种技术曾一度流行(并且在 Damian Conway 的《Perl 最佳实践》中推荐),但从未得到普遍采用。Object::InsideOut CPAN 模块提供了此技术的全面实现,您可能会在实际中看到它或其他内向模块。

下面是使用 Hash::Util::FieldHash 核心模块的该技术的简单示例。此模块已添加到核心以支持内向对象实现。

package Time;

use v5.36;

use Hash::Util::FieldHash 'fieldhash';

fieldhash my %time_for;

sub new {
    my $class = shift;

    my $self = bless \( my $object ), $class;

    $time_for{$self} = time;

    return $self;
}

sub epoch {
    my $self = shift;

    return $time_for{$self};
}

my $time = Time->new;
print $time->epoch;

伪哈希

伪哈希功能是 Perl 早期版本中引入的实验性功能,并在 5.10.0 中删除。伪哈希是一个数组引用,可以使用命名键像哈希一样访问。您可能会在实际中遇到使用它的某些代码。有关更多信息,请参阅 fields 实用程序。

另请参阅

可以在 perlootut 中找到有关 Perl 中面向对象编程的更友好的教程。您还应该查看 perlmodlib 以获取有关构建模块和类的某些样式指南。