Perl5OOP学习笔记
2023-06-13 09:14:09 时间
在学习了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,源代码中则分成两句来写
复制代码代码如下:
学习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,源代码中则分成两句来写
packagename.jamesfancy;
classMyClass{....}
packagename.jamesfancy;
classMyClass{....}
Perl中,name::jamesfancy::MyClass,应对的是/name/jamesfancy/MyClass.pm,源代码中只有一句package就说明了
packagename::jamesfancy::MyClass;
packagename::jamesfancy::MyClass;
至于package中的内容,也就是变量和子程序,至于区别,稍后再说。
bless函数
bless是用来把一个类绑定到引用类型变量的函数。很奇怪Perl为什么要用这个单词,不过没关系,我们可以把它想像得形象一点:就像游戏里牧师通过祝福技能为某人加上BUFF一样,bless把一个类绑定到某个引用类型的变量,从此这个变量就受到了祝福,拥有了这个类中的变量和子程序。
bless的用法通常是:bless($引用变量,类名);
引用变量貌似可以是任何引用类型的变量,我尝试过Scalar,Array和Hash的引用,都能成功。在bless之外,这个引用变量就可以被称之为对象了,当然它仍然是个引用,是对象的引用。
有一点还需要注意,虽然这个对象拥有了类的变量和子程序,但我们应该把它拥有的类的变量和子程序都看成是静态的,换句话说,就是类的成员。在这一点上,子程序的处理会比较特殊一点,但至少类的变量,也就是包变量,是不属于对象的。因此,所有对象的数据都保存在对象引用的原始数据中。既然大家都习惯对象数据以键值对的方式保存,所以通常情况下,bless的引用变量,都是Hash的引用了。
很抽象么?举个例子。如果对OOP的成员函数还不够了解,那就只看下面示例中每个类的test函数中第一句以后的内容不好。
#test.pl
packageTestScalar;
subtest{
my$this=shift();
print("\nInTestScalar::test()\n");
print("Scalar:\n${$this}\n");
}
packageTestArray;
subtest{
my$this=shift();
print("\nInTestArray::test()\n");
print("Array:\n");
foreachmy$item(@{$this}){
print("$item\n");
}
}
packageTestHash;
subtest{
my$this=shift();
print("\nInTestHash::test()\n");
print("Hash:\n");
while(my($key,$value)=each%{$this}){
printf("%-4s=%s\n",$key,$value);
}
}
packagemain;
my$name="JamesFancy";
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__
InTestScalar::test()
Scalar:
JamesFancy
InTestArray::test()
Array:
James
Fancy
Jenny
InTestHash::test()
Hash:
name=James
age=30
从上面的示例中可以看到,分别将3种类型的引用转变为对象。之所以要把类写成3个而非1个,主要是为了在Test里输出不同类型的数据。
类和对象的成员函数
成员函数就是在package中定义的子程序。成员函数是没有静态和非静态之分的,但我宁愿大家都把它看作是静态函数,因为虽然它即可以当作类成员函数来调用,也可以当用对象成员函数来调用,但在当作对象成员函数来调用的时候,Perl偷偷的传入了对象引用。这也解释了为什么通常成员函数里的第一句话往往是
my$this=shift();
当然,这里的$this只是一个局部变量,而不是关键字,你也可以用别的名称来代替它。比如很多人就喜欢用$self,或者$me等。
假如,对于一个成员函数,分别用类和对象来对它进行调用,会有什么不一样呢?再看一个示例:
#test.pl
packageMyClass;
subtest{
my($this,@args)=@_;
print("-"x40,"\n");
print("\$thisis[$this],Refof\$thisis[",ref($this),"]\n");
print("Args:[@args]\n");
}
packagemain;
$obj={};
bless($obj,"MyClass");
MyClass->test("MyClass->test(...)");
$obj->test("\$obj->test(...)");
__END__
----------------------------------------
$thisis[MyClass],Refof$thisis[]
Args:[MyClass->test(...)]
----------------------------------------
$thisis[MyClass=HASH(0x178a44)],Refof$thisis[MyClass]
Args:[$obj->test(...)]
从结果可以看出来,不管哪种方法调用,第一个参数都是Perl偷偷传递进去的。如果是类调用,则第一个参数是该类。如果是对象调用,第一个参数是该对象。因此,只需要将ref($this)的结果和类名进行比较就清楚是哪种调用了。所以,一个容错性较好的成员函数,一开始要判断传入的第一个参数,比如
subfoo{
my$this=shift();
returnunless($thisne"MyClass");
#其它语句
}
这里还有一个疑问:既然package中定义的子程序都是成员函数,那不是类的package和是类的package有啥区别?它们在结构上没有一点区别,唯一的区别在处理中。在调用子程序的时候,Perl不会硬塞一个类或者对象在参数列表的最前面,但调用成员函数的时候会,所以区别是根据你的调用方式来区分的。
调用对象成员还好说,$obj->foo()就好,但是调用类成员的时候,怎么知道是调用的类成员还是包中的子程序呢?那就要看是通过“->”还是“::”来调用的了。下面的例子可以帮助理解:
#test.pl
packageMyClass;
useData::Dumper;
subtest{
print("-"x40,"\n");
print(Dumper(@_));
}
packagemain;
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函数看起来就像这样:
subnew{
my$this={};
bless($this);
}
这个new函数中产生了一个Hash引用,bless它,并返回它。如果你疑惑为什么这里没有看到return语句,那么建议你去看看关于子程序中返回值的资料,顺便查一下bless函数的说明。来看看完整的程序了解一下是怎么使用new函数的。
#test.pl
packageMyClass;
subnew{
my$this={};
bless($this);
}
packagemain;
my$obj1=MyClass::new();
my$obj2=MyClass->new();
my$obj3=newMyClass();
print(join("\n",ref($obj1),ref($obj2),ref($obj3)));
__END__
MyClass
MyClass
MyClass
注意上面newMyClass()的效果和MyClass->new()效果是一样的。这里new不是关键字,而是函数名。同理,如果有一个foo成员函数的话,也可以fooMyClass(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
packageMyClass;
subnew{
my$class=shift();
my$this=ref(@_[0])?@_[0]:{@_};
bless($this);
}
packagemain;
useData::Dumper;
my$obj1=MyClass->new("name"=>"JamesFancy","age"=>30);
my$obj2=MyClass->new({"name"=>"JamesFancy","age"=>30});
print(Dumper($obj1));
print(Dumper($obj2));
__END__
$VAR1=bless({
"name"=>"JamesFancy",
"age"=>30
},"MyClass");
$VAR1=bless({
"name"=>"JamesFancy",
"age"=>30
},"MyClass");
访问对象数据
既然通常是Hash引用被bless成对象,那就只说这种情况。
既然是Hash引用,所以访问数据最简单的办法就跟访问Hash引用一样。比如
$obj->{"name"}="YouName";
my$name=$obj->{"name"};
$obj->{"name"}="YouName";
my$name=$obj->{"name"};
如果想少写点花括号,可以通过定义setter/getter的办法来解决。因为getter和setter可以根据有没参数来区分,所以合并在一个函数中成为可能,比如下面的name函数
#test.pl
packageMyClass;
subnew{
my$class=shift();
my$this=ref(@_[0])?@_[0]:{@_};
bless($this);
}
subname{
my$this=shift();
if(@_[0]){
$this->{"name"}=@_[0];
}
return$this->{"name"};
}
packagemain;
my$obj=MyClass->new("name"=>"JamesFancy");
print($obj->name,"\n");
print($obj->name("NewName"),"\n");
__END__
JamesFancy
NewName
#test.pl
packageMyClass;
subnew{
my$class=shift();
my$this=ref(@_[0])?@_[0]:{@_};
bless($this);
}
subname{
my$this=shift();
if(@_[0]){
$this->{"name"}=@_[0];
}
return$this->{"name"};
}
packagemain;
my$obj=MyClass->new("name"=>"JamesFancy");
print($obj->name,"\n");
print($obj->name("NewName"),"\n");
__END__
JamesFancy
NewName
使用setter/getter的确可以使程序看起来简洁不少。但是对对象中的每个数据写一个getter/setter,还是很累人的,于是,AUTOLOAD函数就被抬出来了,看看下面的程序
packageMyClass;
subnew{
my$class=shift();
my$this=ref(@_[0])?@_[0]:{@_};
bless($this,$class);
}
subAUTOLOAD{
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})||@_){
nostrict"refs";
*{"${class}::$name"}=sub{
my$this=shift();
$this->{$name}=shift()if(@_);
#makeapropertyinhashreferencetypetoHashObjectobject.
if(ref($this->{$name})eq"HASH"){
bless($this->{$name},$class);
}
return$this->{$name};
};
goto&$name;
}
}
packagemain;
my$obj=MyClass->new("name"=>"JamesFancy");
$obj->more1({"key","valueofmore1->key"});
print($obj->name,"\n");
print($obj->more1->key,"\n");
print($obj->more2({})->key("valueofmore2->key"),"\n");
__END__
JamesFancy
valueofmore1->key
valueofmore2->key
这样调用起来是不是方便多了?不过AUTOLOAD写起来很累人的。如果你只需要一个数据对象,网上有个Hash::AsObject的类很好用,用法和上面的最后一个示例差不多。
继承
我的确是对继承这个方面没怎么研究。不过简单的继承大概就是用usebase语句引入基类而已,比如
packageParent;
subtest1{
print("Parnet::test1\n");
}
subtest{
print("Parent::test\n");
}
packageSub;
usebaseParent;
subtest{
print("Sub::test\n");
}
subtest2{
$_[0]->Parent::test();
}
packagemain;
my$obj=bless({},*Sub);
$obj->test();
$obj->test1();
$obj->test2();
__END__
Sub::test
Parnet::test1
Parent::test
参考资料
东南大学出版社出版,O"Reilly的《精通Perl(影印版)》,briandfoy著
Perlversion5.10.0documentation,
Hash::AsObject源码,来自
相关文章
- K8S学习笔记之在k8s中删除和添加节点的方法
- 人类高质量vue学习笔记(六)
- D2L学习笔记03:从零实现一个线性回归
- 背包问题九讲笔记_完全背包[通俗易懂]
- GPU Pro 1 笔记 - Screen-Space Directional Occlusion
- LeetCode 刷题笔记——day 2
- Python学习笔记(六)·面向对象编程
- web前端学习/工作笔记(六)
- GIT详细笔记(2)
- React学习笔记(二)—— JSX、组件与生命周期
- Vue学习笔记之ElementUI 最大值最小值
- 18.计算机科学导论之人工智能初识学习笔记
- 不背锅运维-实践笔记:Oracle表按天分区
- 区块链学习笔记之密码学原理
- 持续学习笔记
- Java的学习笔记(08)对象 三
- Rust学习笔记Day19 你真的了解集合容器吗?
- CMU 15445 学习笔记—8 Index Concurrency Control
- PortSwigger之不安全的反序列化+服务器端模板注入漏洞笔记
- Java基础学习笔记二 Java基础语法详解编程语言
- CSS学习笔记09 简单理解BFC详解编程语言
- MySQL学习笔记之时间取年月日(mysql时间取年月日)
- MSSQL UNION学习笔记:掌握数据合并的技巧(mssqlunion)
- PHP之变量、常量学习笔记
- VPS配置优化笔记
- javascript学习笔记(六)Date日期类型
- JavaScript高级程序设计阅读笔记(二十)js错误处理