Like Share Discussion Bookmark Smile

J.J. Huang   2021-06-12   Perl   瀏覽次數:

Perl - 第二十二章 | Perl 面向對象編程

Perl 中有兩種不同的面向對象編程的實現:

  • 一是基於匿名哈希表的方式,每個對象範例的實質就是一個指向匿名哈希表的引用。在這個匿名哈希表中,存儲來所有的範例屬性。
  • 二是基於數組的方式,在定義一個類的時候,我們將為每一個範例屬性創建一個數組,而每一個對象範例的實質就是一個指向這些數組中某一行索引的引用。在這些數組中,存儲著所有的範例屬性。

面向對象基礎概念

面向對像有很多基礎概念,這裡我們接收三個:對象、類和方法。

  • 對象:對像是對類中資料項的引用。 .
  • 類:類是個Perl包,其中含提供對象方法的類。
  • 方法:方法是個Perl子程序,類名是其第一個參數。

Perl 提供了 bless() 函數,bless 是用來構造對象的, 通過 bless 把一個引用和這個類名相關聯,返回這個引用就構造出一個對象。

類的定義

一個類只是一個簡單的包。
可以把一個包當作一個類用,並且把包裡的函數當作類的方法來用。
Perl 的包提供了獨立的命名空間,所以不同包的方法與變量名不會衝突。
Perl 類的文件後綴為 .pm。
創建一個 Person 類:

1
package Person;

類的程式碼範圍到腳本文件的最後一行,或者到下一個 package 關鍵字前。

創建和使用對象

創建一個類的範例 (對象) 我們需要定義一個構造函數,大多數程序使用類名作為構造函數,Perl 中可以使用任何名字。
你可以使用多種 Perl 的變量作為 Perl 的對象。大多數情況下我們會使用引用數組或哈希。
接下來我們為 Person 類創建一個構造函數,使用了 Perl 的哈希引用。
在創建對象時,你需要提供一個構造函數,它是一個子程序,返回對象的引用。

範例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package Person;
sub new
{
my $class = shift;
my $self = {
_firstName => shift,
_lastName => shift,
_ssn => shift,
};
# 輸出用戶信息
print "名字:$self->{_firstName}\n";
print "姓氏:$self->{_lastName}\n";
print "編號:$self->{_ssn}\n";
bless $self, $class;
return $self;
}

接下來我們創建一個對象:

1
$object = new Person( "摩洛", "王", 3345678);

定義方法

Perl類的方法只但是是個Perl子程序而已,也即通常所說的成員函數。
Perl面向對像中Perl的方法定義不提供任何特別語法,但規定方法的第一個參數為對像或其被引用的包。
Perl 沒有提供私有變量,但我們可以通過輔助的方式來管理對像資料。

接下來我們定義一個獲取名字的方法:

1
2
3
sub getFirstName {
return $self->{_firstName};
}

同樣也可以這麼寫:

1
2
3
4
5
sub setFirstName {
my ( $self, $firstName ) = @_;
$self->{_firstName} = $firstName if defined($firstName);
return $self->{_firstName};
}

接下來我們修改 Person.pm 文件的程式碼,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#!/usr/bin/perl

package Person;

sub new
{
my $class = shift;
my $self = {
_firstName => shift,
_lastName => shift,
_ssn => shift,
};
# 輸出用戶信息
print "名字:$self->{_firstName}\n";
print "姓氏:$self->{_lastName}\n";
print "編號:$self->{_ssn}\n";
bless $self, $class;
return $self;
}

sub setFirstName {
my ( $self, $firstName ) = @_;
$self->{_firstName} = $firstName if defined($firstName);
return $self->{_firstName};
}

sub getFirstName {
my( $self ) = @_;
return $self->{_firstName};
}
1;

註:結尾為1;是因為 Perl 模塊需要返回一個值來require指示指令是否必須成功(返回真值)或失敗(返回假值;如果模塊由於某種原因未能初始化,這可能是有意義的)。參考

employee.pl 腳本程式碼如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#!/usr/bin/perl

use Person;

$object = new Person( "摩洛", "王", 3345678);
# 獲取姓名
$firstName = $object->getFirstName();

print "設置前姓名為 : $firstName\n";

# 使用輔助函數設置姓名
$object->setFirstName( "哥哥" );

# 通過輔助函數獲取姓名
$firstName = $object->getFirstName();
print "設置後姓名為 : $firstName\n";

執行以上程序,輸出結果為:

1
2
3
4
5
6
$ perl employee.pl
名字:摩洛
姓氏:王
編號:3345678
設置前姓名為 : 摩洛
設置後姓名為 : 哥哥

繼承

Perl 裡 類方法通過@ISA數組繼承,這個數組裡面包含其他包(類)的名字,變量的繼承必須明確設定。
多繼承就是這個@ISA數組包含多個類(包)名字。
通過@ISA只能繼承方法,不能繼承資料。
接下來我們創建一個 Employee 類繼承 Person 類。

Employee.pm 文件程式碼如下所示:

1
2
3
4
5
6
#!/usr/bin/perl

package Employee;
use Person;
use strict;
our @ISA = qw(Person); # 從 Person 繼承

現在 Employee 類包含了 Person 類的所有方法和屬性,我們在 main.pl 文件中輸入以下程式碼,並執行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#!/usr/bin/perl

use Employee;

$object = new Employee( "摩洛", "王", 23234345);
# 獲取姓名
$firstName = $object->getFirstName();

print "設置前姓名為 : $firstName\n";

# 使用輔助函數設置姓名
$object->setFirstName( "哥哥" );

# 通過輔助函數獲取姓名
$firstName = $object->getFirstName();
print "設置後姓名為 : $firstName\n";

執行以上程序,輸出結果為:

1
2
3
4
5
6
$ perl main.pl
名字:摩洛
姓氏:王
編號:3345678
設置前姓名為 : 摩洛
設置後姓名為 : 哥哥

方法重寫

上面範例中,Employee 類繼承了 Person 類,但如果 Person 類的方法無法滿足需求,就需要對其方法進行重寫。

接下來我們在 Employee 類中添加一些新方法,並重寫了 Person 類的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#!/usr/bin/perl

package Employee;
use Person;
use strict;
our @ISA = qw(Person); # 從 Person 繼承

# 重寫構造函數
sub new {
my ($class) = @_;

# 調用父類的構造函數
my $self = $class->SUPER::new( $_[1], $_[2], $_[3] );
# 添加更多屬性
$self->{_id} = undef;
$self->{_title} = undef;
bless $self, $class;
return $self;
}

# 重寫方法
sub getFirstName {
my( $self ) = @_;
# 這是子類函數
print "這是子類函數\n";
return $self->{_firstName};
}

# 添加方法
sub setLastName{
my ( $self, $lastName ) = @_;
$self->{_lastName} = $lastName if defined($lastName);
return $self->{_lastName};
}

sub getLastName {
my( $self ) = @_;
return $self->{_lastName};
}

1;

執行以上程序,輸出結果為:

1
2
3
4
5
6
7
8
$ perl main.pl
名字:摩洛
姓氏:王
編號:23234345
這是子類函數
設置前姓名為 : 摩洛
這是子類函數
設置後姓名為 : 哥哥

預設載入

如果在當前類、當前類所有的基類、還有 UNIVERSAL 類中都找不到請求的方法, 這時會再次查找名為 AUTOLOAD() 的一個方法。如果找到了 AUTOLOAD,那麼就會 調用,同時設定全局變量 $AUTOLOAD 的值為缺失的方法的全限定名稱。

如果還不行,那麼 Perl 就宣告失敗並出錯。

如果你不想繼承基類的 AUTOLOAD,很簡單,只需要一句:

1
sub AUTOLOAD;

預設自動加載功能對於錯誤處理非常有用。

註:這邊提供 AUTOLOAD 的一些使用範例連結

析構函數和垃圾收集

如果你之前使用過面向對象編程,那麼你就會意識到需要創建一個析構函數,以便在使用完對像後釋放分配給該對象的內存。一旦對象超出範圍,Perl 就會自動為你執行此操作。

如果你想實現你的析構函數,它應該負責關閉文件或做一些額外的處理,那麼你需要定義一個名為DESTROY的特殊方法。這個方法將在 Perl 釋放分配給它的內存之前在對像上調用。在所有其他方面,DESTROY 方法就像任何其他方法一樣,你可以在此方法中實現你想要的任何邏輯。

析構函數方法只是一個名為 DESTROY 的成員函數(子例程),它將在以下情況下自動調用 -

  • 當對象引用的變量超出範圍時。
  • 當對象引用的變量未定義時。
  • 當腳本終止時
  • 當 perl 解釋器終止時

例如,你可以簡單地將以下方法 DESTROY 放在你的類中:

1
2
3
4
5
6
package MyClass;
...
sub DESTROY
{
print "MyClass::DESTROY called\n";
}

Perl 會把對象的引用作為唯一的參數傳遞給 DESTROY。注意這個引用是只讀的,也就是說你不能通過訪問$[0] 來修改它。
(註:參見perlsub)但是對象自身(比如”${$
[0]” 或者”@{$[0]}” 還有”%{$[0]}” 等等)還是可寫的。

如果你在析構器返回之前重新 bless 了對象引用,那麼 Perl 會在析構器返回之後接著調用你重新 bless 的那個對象的 DESTROY 方法。這可以讓你有機會調用基類或者你指定的其它類的析構器。需要說明的是,DESTROY 也可以手工調用,但是通常沒有必要這麼做。

在當前對象釋放後,包含在當前對像中的其它對象會自動釋放。

Perl 面向對象範例

我們可以通過以下範例來進一步理解Perl面向對象的應用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
#!/usr/bin/perl

# 下面是簡單的類實現
package MyClass;

sub new
{
print "MyClass::new called\n";
my $type = shift; # 包名
my $self = {}; # 引用空哈希
return bless $self, $type;
}

sub DESTROY
{
print "MyClass::DESTROY called\n";
}

sub MyMethod
{
print "MyClass::MyMethod called!\n";
}


# 繼承實現
package MySubClass;

@ISA = qw( MyClass );

sub new
{
print "MySubClass::new called\n";
my $type = shift; # 包名
my $self = MyClass->new; # 引用空哈希
return bless $self, $type;
}

sub DESTROY
{
print "MySubClass::DESTROY called\n";
}

sub MyMethod
{
my $self = shift;
$self->SUPER::MyMethod();
print " MySubClass::MyMethod called!\n";
}

# 調用以上類的主程序
package main;

print "調用 MyClass 方法\n";

$myObject = MyClass->new();
$myObject->MyMethod();

print "調用 MySubClass 方法\n";

$myObject2 = MySubClass->new();
$myObject2->MyMethod();

print "創建一個作用域對象\n";
{
my $myObject2 = MyClass->new();
}
# 自動調用析構函數

print "創建對象\n";
$myObject3 = MyClass->new();
undef $myObject3;

print "腳本執行結束...\n";
# 自動執行析構函數

執行以上程序,輸出結果為:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
調用 MyClass 方法
MyClass::new called
MyClass::MyMethod called!
調用 MySubClass 方法
MySubClass::new called
MyClass::new called
MyClass::MyMethod called!
MySubClass::MyMethod called!
創建一個作用域對象
MyClass::new called
MyClass::DESTROY called
創建對象
MyClass::new called
MyClass::DESTROY called
腳本執行結束...
MyClass::DESTROY called
MySubClass::DESTROY called

註:以上參考了
Tutorialspoint, Object Oriented Programming in PERL