Skip to content

Perl5 的 OOP 学习笔记 #15

Open
@jamesfancy

Description

@jamesfancy

在 Perl 排名持续下降的情况下学 Perl,似乎是有点不明智。但是,工作需要,不得不学啊。再说,Perl 现在在测试领域还是用得非常多的。Phython 虽然也在测试领域开始活跃起来,不过我始终还是不太喜欢 Phython 的语法。

在学习了 Perl 的基本语法之后,学习 Perl 的 OOP,略有心得。不知道 Perl 各个版本之间 OOP 是否有区别,但是我是学习的 Perl5,所以在标题上将版本号也写出来了。因为了解到 PHP4 和 PHP5 的 OOP 部分就有不小的差别,所以有此担心。

学习 Perl 的 OOP,最关键的两件事情就是 package 和 bless。只要把这两个东西搞清楚也就学会大一半了。

Perl 的 package

感觉 Perl 的 package 和 Java 还真有点相似。Java 的 package 是以 CLASSPATH 中的目录为根,按目录定义和搜索分级包名。Perl 也类似,是以 @INC 数组中的目录为根,按目录搜索分级包名。不过有一点不同,Perl 的 package 定义貌似不需要与目录结构对应。具体是什么样的规则我没有去研究,因为按目录结构定义 package 是个好习惯。

相较于 Java,Perl 的 package 还有一点很有意思。Java 的每层 package 对应一个目录,而最后是一个 class 文件对应到类名。Perl 却简化了,package 直接就把目录和文件名都引用了进去。比如

Java 中,name.jamesfancy.MyClass,对应的是 /name/jamesfancy/MyClass.class,源代码中则分成两句来写

package name.jamesfancy;
class MyClass {....}

Perl 中,name::jamesfancy::MyClass,应对的是 /name/jamesfancy/MyClass.pm,源代码中只有一句 package 就说明了

package name::jamesfancy::MyClass;

至于 package 中的内容,也就是变量和子程序,至于区别,稍后再说。

bless 函数

bless 是用来把一个类绑定到引用类型变量的函数。很奇怪 Perl 为什么要用这个单词,不过没关系,我们可以把它想像得形象一点:就像游戏里牧师通过祝福技能为某人加上 BUFF 一样,bless 把一个类绑定到某个引用类型的变量,从此这个变量就受到了祝福,拥有了这个类中的变量和子程序。

bless 的用法通常是:

bless($引用变量, 类名);

引用变量貌似可以是任何引用类型的变量,我尝试过 Scalar,Array 和 Hash 的引用,都能成功。在 bless 之外,这个引用变量就可以被称之为对象了,当然它仍然是个引用,是对象的引用。

有一点还需要注意,虽然这个对象拥有了类的变量和子程序,但我们应该把它拥有的类的变量和子程序都看成是静态的,换句话说,就是类的成员。在这一点上,子程序的处理会比较特殊一点,但至少类的变量,也就是包变量,是不属于对象的。因此,所有对象的数据都保存在对象引用的原始数据中。既然大家都习惯对象数据以键值对的方式保存,所以通常情况下,bless 的引用变量,都是 Hash 的引用了。

很抽象么?举个例子。如果对 OOP 的成员函数还不够了解,那就只看下面示例中每个类的 test 函数中第一句以后的内容不好。

# test.pl
package TestScalar;
sub test {
    my $this = shift();
    print("/nIn TestScalar::test()/n");
    print("Scalar:/n    ${$this}/n");
}

package TestArray;
sub test {
    my $this = shift();
    print("/nIn TestArray::test()/n");
    print("Array:/n");
    foreach my $item (@{$this}) {
        print("    $item/n");
    }
}

package TestHash;
sub test {
    my $this = shift();
    print("/nIn TestHash::test()/n");
    print("Hash:/n");
    while (my ($key, $value) = each %{$this}) {
        printf("    %-4s = %s/n", $key, $value);
    }
}

package main;

my $name = "James Fancy";
my $objScalar = /$name;
my $objArray = ['James', 'Fancy', 'Jenny'];
my $objHash = {'name' => 'James', 'age' => 30};

bless($objScalar, 'TestScalar');
bless($objArray, 'TestArray');
bless($objHash, 'TestHash');

$objScalar->test();
$objArray->test();
$objHash->test();

__END__

In TestScalar::test()
Scalar:
    James Fancy

In TestArray::test()
Array:
    James
    Fancy
    Jenny

In TestHash::test()
Hash:
    name = James
    age  = 30

从上面的示例中可以看到,分别将 3 种类型的引用转变为对象。之所以要把类写成 3 个而非 1 个,主要是为了在 Test 里输出不同类型的数据。

类和对象的成员函数

成员函数就是在 package 中定义的子程序。成员函数是没有静态和非静态之分的,但我宁愿大家都把它看作是静态函数,因为虽然它即可以当作类成员函数来调用,也可以当用对象成员函数来调用,但在当作对象成员函数来调用的时候,Perl 偷偷的传入了对象引用。这也解释了为什么通常成员函数里的第一句话往往是

my $this = shift();

当然,这里的 $this 只是一个局部变量,而不是关键字,你也可以用别的名称来代替它。比如很多人就喜欢用 $self,或者 $me 等。

假如,对于一个成员函数,分别用类和对象来对它进行调用,会有什么不一样呢?再看一个示例:

# test.pl
package MyClass;

sub test {
    my ($this, @args) = @_;
    print('-' x 40, "/n");
    print("/$this is [$this], Ref of /$this is [", ref($this), "]/n");
    print("Args: [@args]/n");
}

package main;

$obj = {};
bless($obj, 'MyClass');

MyClass->test("MyClass->test(...)");
$obj->test("/$obj->test(...)");

__END__
----------------------------------------
$this is [MyClass], Ref of $this is []
Args: [MyClass->test(...)]
----------------------------------------
$this is [MyClass=HASH(0x178a44)], Ref of $this is [MyClass]
Args: [$obj->test(...)]

从结果可以看出来,不管哪种方法调用,第一个参数都是 Perl 偷偷传递进去的。如果是类调用,则第一个参数是该类。如果是对象调用,第一个参数是该对象。因此,只需要将 ref($this) 的结果和类名进行比较就清楚是哪种调用了。所以,一个容错性较好的成员函数,一开始要判断传入的第一个参数,比如

sub foo {
    my $this = shift();
    return unless ($this ne 'MyClass');
    # 其它语句
}

这里还有一个疑问:既然 package 中定义的子程序都是成员函数,那不是类的 package 和是类的 package 有啥区别?它们在结构上没有一点区别,唯一的区别在处理中。在调用子程序的时候,Perl 不会硬塞一个类或者对象在参数列表的最前面,但调用成员函数的时候会,所以区别是根据你的调用方式来区分的。

调用对象成员还好说,$obj->foo() 就好,但是调用类成员的时候,怎么知道是调用的类成员还是包中的子程序呢?那就要看是通过 -> 还是 :: 来调用的了。下面的例子可以帮助理解:

# test.pl
package MyClass;
use Data::Dumper;
sub test {
    print('-' x 40, "/n");
    print(Dumper(@_));
}

package main;

MyClass->test("MyClass->test(...)");
MyClass::test("MyClass::test(...)");

__END__
----------------------------------------
$VAR1 = 'MyClass';
$VAR2 = 'MyClass->test(...)';
----------------------------------------
$VAR1 = 'MyClass::test(...)';

很明显,通过 :: 调用的子程序没有被 Perl 塞入一个引用类的参数。

构造函数

Perl 的 OOP 没有指定专门的构造函数,所以你可以把任何一个子程序当作构造函数,当然,重要的是其中的内容。既然脚本通常不是写给自己一个人看的,所以还是按照大家的习惯,把构造函数取名为 new 吧。按照多数 OOP 语言的习惯,new 函数通常返回一个对象或其引用、指针。所以在 Perl 中,这个 new 函数要返回一个对象引用,理所当然地,把 bless 动作包含在 new 函数中是个好习惯。那么一个简单的 new 函数看起来就像这样:

sub new {
    my $this = {};
    bless($this);
}

这个 new 函数中产生了一个 Hash 引用,bless 它,并返回它。如果你疑惑为什么这里没有看到 return 语句,那么建议你去看看关于子程序中返回值的资料,顺便查一下 bless 函数的说明。来看看完整的程序了解一下是怎么使用 new 函数的。

# test.pl
package MyClass;

sub new {
    my $this = {};
    bless($this);
}

package main;

my $obj1 = MyClass::new();
my $obj2 = MyClass->new();
my $obj3 = new MyClass();

print(join("/n", ref($obj1), ref($obj2), ref($obj3)));

__END__
MyClass
MyClass
MyClass

注意上面 new MyClass() 的效果和 MyClass->new() 效果是一样的。这里 new 不是关键字,而是函数名。同理,如果有一个 foo 成员函数的话,也可以 foo MyClass(args),它实际上是 MyClass::foo(MyClass, args);

话说回来,如果需要初始化对象数据又该如何呢?前面说过,对象数据保存在引用的数据自身,所以我们通常是把一个 Hash 引用 bless 成对象。所以我们经常会看到这样调用 new

my $obj = MyClass->new('key1' => 'value1', 'key2' => 'value2');

或者

my $obj = MyClass->new({'key1' => 'value1', 'key2' => 'value2'});

两种调用方式的区别在于 new 函数中的处理不同,因为前者传入的是一个 Hash 实体,而后者传入的是一个 Hash 引用。为了兼容这两种情况,new 函数通常会像下面程序中的写法:

# test.pl
package MyClass;

sub new {
    my $class = shift();
    my $this = ref(@_[0]) ? @_[0] : {@_};
    bless($this);
}

package main;
use Data::Dumper;

my $obj1 = MyClass->new('name' => 'James Fancy', 'age' => 30);
my $obj2 = MyClass->new({'name' => 'James Fancy', 'age' => 30});

print(Dumper($obj1));
print(Dumper($obj2));

__END__
$VAR1 = bless( {
                 'name' => 'James Fancy',
                 'age' => 30
               }, 'MyClass' );
$VAR1 = bless( {
                 'name' => 'James Fancy',
                 'age' => 30
               }, 'MyClass' );

访问对象数据

既然通常是 Hash 引用被 bless 成对象,那就只说这种情况。

既然是 Hash 引用,所以访问数据最简单的办法就跟访问 Hash 引用一样。比如

$obj->{'name'} = "You Name";
my $name = $obj->{'name'};

如果想少写点花括号,可以通过定义 setter/getter 的办法来解决。因为 getter 和 setter 可以根据有没参数来区分,所以合并在一个函数中成为可能,比如下面的 name 函数

# test.pl
package MyClass;

sub new {
    my $class = shift();
    my $this = ref(@_[0]) ? @_[0] : {@_};
    bless($this);
}

sub name {
    my $this = shift();
    if (@_[0]) {
        $this->{'name'} = @_[0];
    }
    return $this->{'name'};
}

package main;

my $obj = MyClass->new('name' => 'James Fancy');
print($obj->name, "/n");
print($obj->name("New Name"), "/n");

__END__
James Fancy
New Name

使用 setter/getter 的确可以使程序看起来简洁不少。但是对对象中的每个数据写一个 getter/setter,还是很累人的,于是,AUTOLOAD 函数就被抬出来了,看看下面的程序

package MyClass;

sub new {
    my $class = shift();
    my $this = ref(@_[0]) ? @_[0] : {@_};
    bless($this, $class);
}

sub AUTOLOAD {
    my $this = $_[0];
    if (!ref($this)) {
        return;
    }

    my $name = $AUTOLOAD;

    if (defined($name)) {
        $name =~ s/.*:://;
    } else {
        return;
    }

    my $class = ref($this);
    if (defined($this->{$name}) || @_) {
        no strict 'refs';
        *{"${class}::$name"} = sub {
            my $this = shift();
            $this->{$name} = shift() if (@_);

            # make a property in hash reference type to HashObject object.
            if (ref($this->{$name}) eq 'HASH') {
                bless($this->{$name}, $class);
            }

            return $this->{$name};
        };

        goto &$name;
    }
}

package main;

my $obj = MyClass->new('name' => 'James Fancy');
$obj->more1({'key', 'value of more1->key'});
print($obj->name, "/n");
print($obj->more1->key, "/n");
print($obj->more2({})->key("value of more2->key"), "/n");

__END__
James Fancy
value of more1->key
value of more2->key

这样调用起来是不是方便多了?不过 AUTOLOAD 写起来很累人的。如果你只需要一个数据对象,网上有个 Hash::AsObject 的类很好用,用法和上面的最后一个示例差不多。

继承

我的确是对继承这个方面没怎么研究。不过简单的继承大概就是用 use base 语句引入基类而已,比如

package Parent;

sub test1 {
    print("Parnet::test1/n");
}

sub test {
    print("Parent::test/n");
}

package Sub;
use base Parent;

sub test {
    print("Sub::test/n");
}

sub test2 {
    $_[0]->Parent::test();
}

package main;

my $obj = bless({}, *Sub);
$obj->test();
$obj->test1();
$obj->test2();

__END__
Sub::test
Parnet::test1
Parent::test

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions