Perl的類別物件利器 bless

URL Link //n.sfs.tw/16303

2024-01-04 10:28:25 By 張○○

perl本身具有參照這類的物件,但是參照適用簡單類型的物件,例如統一格式、規律變化的變數。若需要複雜一點,類似類別這種類型的物件時,就得借助 bless 函式的幫助。

簡單來說,bless幫助大家建立perl的類別。

 

一般程式的類別

現代的程式語言多半都封裝在類別中,不像perl是沒有類別的語言,一般程式語一般程式的類別大概是長這個樣子:

class MyClass {
   var variable1;
   function dosomething(){
       ...
   }
}

類別裡面有變數 variable1和方法 dosomething(),我們藉由NEW實體化後就能叫用這個類別:

obj = new MyClass();
obj->dosomething();
print obj->variable1;

這就是類別方便的地方,當然類別不會這麼簡單,今天同樣的事希望perl這古老的語言也做得到。

 

PERL的類別寫法

perl可以藉由包裹package來完成類別,package一般我們會寫成獨立的檔案,再用 use來引入。這部分可以參考[PERL] 18-套件及模組@新精讚

這次的範例我寫一個數學小函式Math,他的功能只是算代入的兩個數的算數和幾何平均數。

為了後面描述方便及避免混淆,後文所有的類別(class)、包裹(package)、套件(package)、模組(pm),都稱為「類別」;副程式(sub)、方法(method)、函數、函式(function)全稱為「函式」。

Math.pm

package Math;

my $a;
my $b;

sub setAB {
  $a=shift;
  $b=shift;
}

sub ari{
   return ($a+$b)/2;
}

sub geo {
   return sqrt($a*$b);
}

1;

上面的類別Math很簡單,只有三個函式 setAB、ari、geo。

接下來使用 01.pl 這個程式去執行

01.pl

#!/usr/bin/perl -w
use Cwd qw(abs_path);
use lib abs_path;
use Math;

&Math::setAB(3,4);
print &Math::ari;
print "\n";
print &Math::geo;
print "\n";

第2行和第3行 為了讓自寫的Math.pm和 01.pl 放在一起能被呼叫到,採用[4]的作法把@INC引入目前的目錄。

第4行 使用Math包裹。

執行結果

% ./01.pl
3.5
3.46410161513775

 

上面的寫法完全沒有問題,也運作的很好,但嚴格說來並不是很好的方式,因為他採用的是靜態函式的方式叫用,對於複雜一點的程式會有變數混雜的情況。

 

引入BLESS函式

接下來說今天得重頭戲,bless函式,這是perl內建的函式,不必use其餘套件。

BLESS函式有三種寫法

1

bless $ref, $package;

2

bless $ref, "";

3

bless $ref;

$ref 稱為 referred-to item,中文不知怎麼翻,就叫他被參考物件吧,一般都是放入hash變數。

以上三種以第1種最常見,也是推薦用法;第2種是建立main的參考物件;第3種是建立目前所在位置的參考物件,相當於this。

以上程式改成 bless函式的寫法,改成如下∶

 

02.pl

#!/usr/bin/perl -w
use Cwd qw(abs_path);
use lib abs_path;
use Math;

my $obj={};
bless($obj, "Math");

$obj->setAB(3,4);
print $obj->ari;
print "\n";
print $obj->geo;
print "\n";

第6行和第7行,簡單理解就是將Math這個類別指給 $obj 當參照。

後面就使用物件的方式呼叫,感覺是不是先進多了?

引用的物件 Math 也得進行修改:

Math.pm (修正)

package Math;

my $a;
my $b;

sub setAB {
  $class=shift;
  $a=shift;
  $b=shift;
}

sub ari{
   return ($a+$b)/2;
}

sub geo {
   return sqrt($a*$b);
}

1;

以上只加入第7行,有傳入參數的話,第一個shift取到的變數是類別本身的位址參照。

執行結果

% ./02.pl   
3.5
3.46410161513775

 

 

bless的建構子

預設perl就沒有建構子的概念,也就是實體化類別後一定會執行的函式,但那不打緊,我們自寫一個new方法來當作建構子[3]

 

Math.pm (+加入new這個副程式)

sub new
{
    my $class = shift;
    my $self = {
      val1 => shift || 0,
    };
    bless $self, $class;
    return $self;
}

上面有提過,傳參第一項是類別本身,第4~6行是在new裡面定義一個參照,並取得實體化後傳進來的一個變數,第5行 || 是給預設值0的方法,如果要取多筆傳進來的值,可自行增加。

02.pl 改成 03.pl 內容如下:

03.pl

#!/usr/bin/perl -w
use Cwd qw(abs_path);
use lib abs_path;
use Math;

#my $obj={};
my $obj= Math->new(8,9,10);

$obj->setAB(3,4);
print $obj->ari;
print "\n";
print $obj->geo;
print "\n";

第7行在new呼叫時傳入3個數值,但只有第1個數值(8)被類別取用。第6行就作廢不必使用了,是不是方便很多?

 

如何使用類別中的自己

類別中如果想叫用類別自己的函式或變數,一般的程式語言都會有this或self的保留字,在perl的類別中,我們得自己定義出來。

只要借用上面的new函式,再把self定義為類別變數即可:

Math.pm (部分)

my $self;

sub new
{
    my $class = shift;
    $self = {
      val1 => shift || 0,
    };
    bless $self, $class;
    return $self;
}

sub printVal1 {
  print $self->{'val1'};
}

第1行 把$self的定義移到函式外面。
第9行 把$self傳給叫用new的參照變數。
第13-15行 新增函式printVal1印出傳入的數值val1的方法。

以上的Math.pm類別中的兩個值也希望透過new的時候帶入的話,就得在new的時候把類別變數 $a, $b 指定,所以完整的 Math.pm 也要修改如下:

Math.pm

package Math;

my $a;
my $b;
my $self;

sub new
{
    my $class = shift;
    $self = {
      a => shift || 0,
      b => shift || 0
    };
    $a = $self->{a};
    $b = $self->{b};
    bless $self, $class;
    return $self;
}

sub setAB {
  shift;
  $a=shift;
  $b=shift;
}

sub ari{
   return ($a+$b)/2;
}

sub geo {
   return sqrt($a*$b);
}

1;

類別基本沒變,但要注意的是第14、15行的$a和$b是類別的變數,而$self中的是參照中的變數,換言之,是雜湊參照。你也可以寫成這樣:

   $a = $self->{'a'};
   $b = $self->{'b'};

 

03.pl 改成 04.pl 如下:

04.pl

#!/usr/bin/perl -w
use Cwd qw(abs_path);
use lib abs_path;
use Math;

my $obj= Math->new(3,7);
print $obj->ari;
print "\n";
print $obj->geo;
print "\n";

$obj->setAB(3,4);
print $obj->ari;
print "\n";
print $obj->geo;
print "\n";

第6-10行 使用new函數帶入$a, $b 值
第12-16行 使用setAB函數帶入$a, $b 值

執行結果

$ ./04.pl
5
4.58257569495584
3.5
3.46410161513775

 

能不能叫用類別中的變數?

04.pl中的函數透過new之後的參照變數 $obj就能叫用類別中的函式,例如第7,9行的叫用方式:

print $obj->ari;
print $obj->geo;

會呼叫類別中的函式。

但如果想要取用類別中的變數 $a 和$b,寫成下面可不可行?

print $obj->a;    # Can't locate object method "a" via package "Math"

print $obj->$a;  # Use of uninitialized value $a in method lookup

print $obj->'a';  # syntax error

print $obj->{a};  # 雖然這個有取到,但並不是類別的變數

print $obj::a;  # Use of uninitialized value $obj::a

這裡的確有點麻煩,bless 有把類別封裝成一個物件,並把函式也包進來。但是類別中的變數是隱藏的傳導物質,你能透過函式來取用這些變數,但不能像某些語言一樣能直接取得,換句話說,你得設計 get/set 的函式。

為此我使用 dumper來觀察封裝後的$obj物件,並拿掉類別變數 $a $b改成全部使用雜湊參照$self中的變數

Math.pm

package Math;

my $self;

sub new
{
    shift;
    $self = {
      a => shift || 0,
      b => shift || 0
    };
    bless $self;
    return $self;
}

sub setAB {
  shift;
  $self->{'a'}=shift;
  $self->{'b'}=shift;
}

sub ari{
   return ($self->{'a'}+$self->{'b'})/2;
}

sub geo {
   return sqrt($self->{'a'}*$self->{'b'});
}

1;

第7和12行 可以讓程式再精簡,畢竟$class是多餘的,就讓我們採用 bless的第3種寫法吧,我在前面有提到,第3種寫法就是自己。

因為拿掉了類別變數,所有的函式都採用雜湊參照變數,即$self->{'變數名'}

執行程式改成05.pl:

05.pl

#!/usr/bin/perl -w
use Cwd qw(abs_path);
use lib abs_path;
use Math;
use Data::Dumper qw(Dumper);

my $obj= Math->new(2,5);
print Dumper($obj);
print $obj->ari;
print "\n";
print $obj->geo;

第5和第8行可以印出$obj的內容,其餘不變。

 

執行結果

% ./05.pl
$VAR1 = bless( {
                 'b' => 5,
                 'a' => 2
               }, 'Math' );
3.5
3.16227766016838

以上就是我對bless的理解。

 

結論

bless函式是perl寫類別的利器。

bless初始化的物件沒辦法直接叫用類別中的變數,所以建議捨棄不用,全部採用雜湊參照變數,如果覺得麻煩,也可以設定函式變數。

bless可以讓你的程式更簡潔,很適合寫大型的perl程式。

[3] 有提到解構子的寫法,這是我以前不知道的,只要在類別中加入DESTROY這個函式即可:

sub DESTROY  
{  
    # DEFINE Destructors 
    my $self = shift; 
    print "Constructor Destroyed :P";  
} 

 

參考資料

[1] https://perldoc.perl.org/functions/bless

[2] https://www.cnblogs.com/yanzibuaa/p/7700832.html

[3] https://www.geeksforgeeks.org/perl-constructors-and-destructors/

[4] https://perlmaven.com/how-to-create-a-perl-module-for-code-reuse