Perl 的“祝福”到底是做什么的?

我理解在类的“ new”方法中使用 Perl 中的“恭喜”关键字:

sub new {
my $self = bless { };
return $self;
}

但是“祝福”到底对散列引用做了什么呢?

114014 次浏览

简而言之: 它将该散列标记为附加到当前包命名空间(以便该包提供其类实现)。

通常,bless将对象与类关联。

package MyClass;
my $object = { };
bless $object, "MyClass";

现在,当您在 $object上调用方法时,Perl 知道要搜索哪个包。

如果省略了第二个参数,就像在您的示例中一样,那么将使用当前的包/类。

为了清晰起见,您的示例可以这样写:

sub new {
my $class = shift;
my $self = { };
bless $self, $class;
}

编辑: 请参阅 Kixx的良好 回答了解更多细节。

bless将引用与包关联。

引用是什么并不重要,它可以是一个散列(最常见的情况) ,一个数组(不太常见) ,一个标量(通常这表明一个 由内而外的物体) ,一个正则表达式,子例程或 TYPEGLOB (有用的例子参见书 面向对象的 Perl: Damian Conway 编写的概念和编程技术综合指南) ,甚至一个文件或目录句柄的引用(最不常见的情况)。

bless-ing 的效果是,它允许您将特殊的语法应用于受祝福的引用。

例如,如果一个受保护的引用存储在 $obj中(由 bless与包“ Class”相关联) ,那么 $obj->foo(@args)将调用一个子例程 foo,并将引用 $obj作为第一个参数传递,然后传递其余的参数(@args)。子例程应该在包“ Class”中定义。如果包“ Class”中没有子程序 foo,那么将搜索其他包的列表(取自包“ Class”中的数组 @ISA) ,并调用找到的第一个子程序 foo

例如,如果您确信任何 Bug 对象都将是一个受祝福的散列,那么您可以(最终!)在 Bug: : print _ me 方法中填写缺少的代码:

 package Bug;
sub print_me
{
my ($self) = @_;
print "ID: $self->{id}\n";
print "$self->{descr}\n";
print "(Note: problem is fatal)\n" if $self->{type} eq "fatal";
}

现在,每当通过引用任何已经被保护到 Bug 类中的散列来调用 print _ me 方法时,$self 变量将提取作为第一个参数传递的引用,然后 print 语句将访问受保护散列的各个条目。

我遵循这个思想来指导面向对象的 Perl 的开发。

请将任何数据结构引用与类关联。考虑到 Perl 如何创建继承结构(在一种树中) ,很容易利用对象模型来为组合创建对象。

对于这种关联,我们称之为对象,要发展始终记住,对象的内部状态和类的行为是分开的。您可以保护/允许任何数据引用使用任何包/类行为。 因为包可以理解对象的“情感”状态。

这个函数告诉 REF 引用的实体,它现在是 CLASSNAME 包中的一个对象,如果省略 CLASSNAME,则是当前包。建议使用两个论点形式的祝福。

例子 :

bless REF, CLASSNAME
bless REF

返回值

此函数返回对保护为 CLASSNAME 的对象的引用。

例子 :

下面是显示其基本用法的示例代码,对象引用是通过加持对包的 class-的引用来创建的

#!/usr/bin/perl


package Person;
sub new
{
my $class = shift;
my $self = {
_firstName => shift,
_lastName  => shift,
_ssn       => shift,
};
# Print all the values just for clarification.
print "First Name is $self->{_firstName}\n";
print "Last Name is $self->{_lastName}\n";
print "SSN is $self->{_ssn}\n";
bless $self, $class;
return $self;
}

我将在这里尝试提供一个答案,因为在我最初写这篇文章的时候,这里的答案并没有完全为我点击(警告,这个答案结构相当糟糕,请随意跳过那些对你不是特别有用的部分)。

Perl 的祝福函数将指定的引用与一个包名字符串相关联,并使得祝福引用的箭头操作符在与该引用相关联的包中查找该方法,如果找不到该方法,则继续使用 @ISA数组查找(这超出了本文的范围)。

我们为什么需要这个?

让我们首先用 JavaScript 表示一个示例:

(() => {
//'use strict'; // uncomment to fix the bug mentioned below.


class Animal {
constructor(args) {
console.log(this);
this.name = args.name;
this.sound = args.sound;
}
}


/* This is left for historical reasons,
*    modern JavaScript engines no longer allow you to
*    call class constructors without using new.
*
* var animal = Animal({
*     'name': 'Jeff',
*     'sound': 'bark'
* });
* console.log(animal.name + ', ' + animal.sound); // seems good
* console.log(window.name); // my window's name is Jeff?
*/


// as of recently, Animal constructor cannot be called without using the new operator.
var animal = new Animal({
'name': 'Jeff',
'sound': 'bark'
});


console.log(animal.name + ', ' + animal.sound); // still fine.
console.log("window's name: " + window.name); // undefined
})();

现在让我们看一下这个类构造的蜜糖版本:

(() => {
// 'use strict'; // uncomment to fix bug.


var Animal = function(args) {
this.name = args.name;
this.sound = args.sound;
return this; // implicit context hashmap
};
    

/**
*  The bug left for historical reasons,
*      should still work now in modern web developer consoles.
*
*  var animal = Animal({
*      'name': 'Jeff',
*      'sound': 'Bark'
*  });
*  console.log(animal.name + ', ' + animal.sound); // seems good
*  console.log("The window's name is: " + window.name); // my window's name is Jeff?
*/
  

// the new operator causes the "this" inside methods to refer to the animal
// rather than the global scope, so the bug mentioned above does not occur.
var animal = new Animal({
'name': 'Jeff',
'sound': 'bark'
});
console.log(animal.sound);
console.log(window.name); // the name has not been changed by the constructor.
})();

Animal 的构造函数接受一个 Object 属性并返回一个具有这些属性的 Animal,或者如果您忘记放置 new 关键字,它将返回整个全局上下文(在浏览器开发者控制台中是 window)。

Perl 没有“ this”、“ new”或“ class”,但它仍然可以有一个行为类似的函数。我们不会有一个构造器或原型,但我们将能够创造新的动物和修改他们的个人属性。

# immediatly invoked subroutine execution(IIFE).
(sub {
my $Animal = (sub {
return {
'name' => $_[0]{'name'},
'sound' => $_[0]{'sound'}
};
});


my $animal = $Animal->({
'name' => 'Jeff',
'sound' => 'bark'
});


print $animal->{sound};
})->();

现在,我们有一个问题: 如果我们想让动物自己发出声音,而不是直接打印它们的声音,那该怎么办。也就是说,我们需要一个执行声音的函数来打印动物自己的声音。

这样做的一种方法是为 Animal 的每个实例提供其自己的 PerformSound 子例程引用。

# self contained scope
(sub {
my $Animal = (sub {
$name = $_[0]{'name'};
$sound = $_[0]{'sound'};
    

return {
'name' => $name,
'sound' => $sound,
'performSound' => sub {
print $sound . "\n";
}
};
});


my $animal = $Animal->({
'name' => 'Jeff',
'sound' => 'bark'
});


$animal->{'performSound'}();
})->();

这通常不是我们想要的,因为 PerformSound 被作为构建的每个动物的一个全新的子例程引用。构建10000个动物将潜在地分配10000个 PerformSound 子例程。我们希望有一个单独的子例程 PerformSound,所有 Animal 实例都可以使用它来查找自己的声音并打印它。

(() => {
'use strict';


/* a function that creates an Animal constructor which can be used to create animals */
var Animal = (() => {
/* function is important, as fat arrow does not have "this" and will not be bound to Animal. */
var InnerAnimal = function(args) {
this.name = args.name;
this.sound = args.sound;
};
/* defined once and all animals use the same single function call */
InnerAnimal.prototype.performSound = function() {
console.log(this.name);
};
        

return InnerAnimal;
})();
 

var animal = new Animal({
'sound': 'bark',
'name': 'Jeff'
});
animal.performSound(); // Jeff
})();

这里就是 Perl 的并行停止的地方。

JavaScript 的 new 操作符不是可选的,没有它,对象方法中的“ this”会污染全局范围:

(() => {
// uncommenting this prevents unintentional
//     contamination of the global scope, and throws a TypeError instead.
// 'use strict';


var Person = function() {
this.name = "Sam";
};
//    var wrong = Person(); // oops! we have overwritten window.name or global.main.
//    console.log(window.name); // my window's name is Sam?
var correct = new Person; // person's name is actually stored in the person now.
})();

我们希望为每个动物有一个功能,查找动物自己的声音,而不是硬编码它在建设。

Blessing 让我们使用包的子程序,而不是必须为每个对象附加一个子程序引用,它还让 ref引用更有意义的包名(比如 Animal)作为对象的名称,而不是一个无聊的 HASH或任何其他你选择祝福的引用:

package Animal;
sub new {
my $packageRef = $_[0];
my $name = $_[1]->{'name'};
my $sound = $_[1]->{'sound'};


my $this = {
'name' => $name,
'sound' => $sound
};
    

bless($this, $packageRef);
return $this;
}


# all animals use the same performSound to look up their sound.
sub performSound {
my $this = shift;
my $sound = $this->{'sound'};
print $sound . "\n";
}


package main;
my $animal = Animal->new({
'name' => 'Cat',
'sound' => 'meow'
});


print("The animal's ref is: " . ref($animal) . "\n");
$animal->performSound();

摘要/TL; DR :

  1. Perl 没有“ this”、“ class”和“ new”。

  2. 对包中的对象进行祝福可以给该对象提供对包的引用。

  3. 使用箭头操作符调用受保护的引用($blessedObject->method(...arguments))的方法通常与调用 Package::method($blessedObject, ...arguments)相同,但是如果没有找到方法,它将继续使用超出本文范围的包的 @ISA进行查找。

  4. 实际上,你可以在运行时创建新的类,只要你违反了严格的“ ref”或者使用 eval,这里有一个如何实现的演示:

#!/usr/bin/perl


use warnings;
use strict;


print('Enter the name for the class(eg Greeter): $ ');
my $class_name = <>;
chomp $class_name;


print('Enter the name of the method(eg greet): $ ');
my $method_name = <>;
chomp $method_name;


no strict 'refs';
# The line below violates strict 'refs'.
*{$class_name . '::new'} = sub {
my $self = bless {}, $_[0];
return $self;
};
use strict 'refs';


no strict 'refs';
# The line below violates strict 'refs'
*{$class_name . '::' . $method_name} = sub {
print("Hello, World!\n");
};
use strict 'refs';


my $instance = ($class_name)->new();
$instance->$method_name();

为什么这么混乱? :

祝福令人困惑的一个原因是,有三种有效的方式来称呼一个包裹

  1. 通过 A::a()作为一个包子例程。
  2. 通过 A->a(),作为一个包子例程,但是这使得 __PACKAGE__作为第一个参数隐式地传递到其他参数之前。
  3. 通过 $a->a()与受祝福的 $a进入 A

下面的代码说明了这一点:

# | Illustrates catching 3 distinct ways of calling a package's member.
package Test;


sub new {
return bless {}, __PACKAGE__;
}


sub runTest {
if (ref($_[0]) eq __PACKAGE__) {
# | $_[0] is the blessed reference.
# | Despite being called with "->", $_[1] is NOT "Test"(__PACKAGE__).
print("Test::runTest was called through a blessed reference call(\$instance->runTest().\n");
} elsif ($_[0] eq __PACKAGE__) {
# | $_[0] is "Test"(__PACKAGE__), but we can't determine for sure whether it was -> or ::.
print("Test::runTest was called through Test->runTest() or through Test::runTest() with 'Test' as the first argument.\n");
} else {
# | $_[0] is neither a blessed reference nor "Test"(__PACKAGE__), so it can't be an arrow call.
print "Test::runTest was called through Test::runTest()\n";
}
}


package main;


my $test = Test->new();
$test->runTest();
Test->runTest();
Test::runTest();
Test::runTest('Test'); # <- Same as "Test->runTest();"
Test::runTest($test); # <- Same as "$test->runTest();"

另一个原因是,与 JavaScript 不同的是,Python 可以有多个名称不同但定义/方法/属性不同的类,Perl 类具有唯一性(ref $obj) ,因为在任何给定时间只能有一个具有特定名称的包,而@ISA 需要一点时间来适应。

我的最后一个原因是,与其他脚本语言中的类相比,包不那么具体,在这些脚本语言中,你可以通过赋值操作符在变量中存储对类本身的引用,而在 Perl 中,你不仅不能存储对类的引用(你只能通过它们的名称字符串来引用包) ,而且如果不违反严格的“ ref”或使用 eval,试图通过存储在变量中的名称来引用包(例如 String [ $method ])似乎是不可能的。

不管怎样,希望有人会觉得这篇文章有用。


注意: 这是一个相当老的尝试来回答这个问题,我已经尝试清除大量幼稚和令人尴尬/无意义/分散注意力的陈述,并添加更多有用的例子来帮助理解这个概念,但它仍然远远不是我想要的(重读仍然相当令人沮丧)。 我留下它,因为我仍然相信它可能对某人有用。

请稍安勿躁地接受这个问题,我为答案结构设计不当所引起的任何头疼表示歉意。

bless-ed 引用在内部的特殊区别在于,用于引用的 SV(存储在标量中)获取一个额外的 FLAGS值(OBJECT) ,以及一个携带包名称的 STASH(还有一些其他差异)

perl -MDevel::Peek -wE'
package Pack  { sub func { return { a=>1 } } };
package Class { sub new  { return bless { A=>10 } } };
$vp  = Pack::func(); print Dump $vp;   say"---";
$obj = Class->new;   print Dump $obj'

印刷品,与相同的(和无关的)部分被抑制

SV = IV(0x12d5530) at 0x12d5540
REFCNT = 1
FLAGS = (ROK)
RV = 0x12a5a68
SV = PVHV(0x12ab980) at 0x12a5a68
REFCNT = 1
FLAGS = (SHAREKEYS)
...
SV = IV(0x12a5ce0) at 0x12a5cf0
REFCNT = 1
FLAGS = (IOK,pIOK)
IV = 1
---
SV = IV(0x12cb8b8) at 0x12cb8c8
REFCNT = 1
FLAGS = (PADMY,ROK)
RV = 0x12c26b0
SV = PVHV(0x12aba00) at 0x12c26b0
REFCNT = 1
FLAGS = (OBJECT,SHAREKEYS)         <--
STASH = 0x12d5300   "Class"        <--
...
SV = IV(0x12c26b8) at 0x12c26c8
REFCNT = 1
FLAGS = (IOK,pIOK)
IV = 10

翻译员知道

  • 这是一个物体

  • 它属于哪个包裹

这说明了它的用途。

例如,当遇到对该变量的解引用($obj->name)时,在包(或层次结构)中寻找具有该名称的子节点,将该对象作为第一个参数传递,等等。