overload - Perl 運算重載套件
package SomeThing;
use overload
'+' => \&myadd,
'-' => \&mysub;
# etc
...
package main;
$a = SomeThing->new( 57 );
$b = 5 + $a;
...
if (overload::Overloaded $b) {...}
...
$strval = overload::StrVal $b;
此實用程式允許為類別重載 Perl 的運算子。若要重載內建函式,請改為參閱 "perlsub 中的「覆寫內建函式」"。
use overload
指令的引數為 (金鑰, 值) 配對。有關合法金鑰的完整清單,請參閱下方的 "可重載運算"。
運算子實作(值)可以是子常式、子常式的參照或匿名子常式 - 換句話說,在 &{ ... }
呼叫中任何合法的內容。指定為字串的值會被解釋為方法名稱。因此
package Number;
use overload
"-" => "minus",
"*=" => \&muas,
'""' => sub { ...; };
宣告減法將由 Number
類別(或其任一基底類別)中的 minus()
方法實作,而 Number::muas()
函式將用於乘法的指定形式 *=
。它也定義一個匿名子常式來實作字串化:這會在祝福進入 Number
套件的物件在字串內容中使用時呼叫(此子常式可能會以羅馬數字回傳數字,例如)。
以下的 minus()
範例實作(假設 Number
物件只是祝福為純量的參照)說明了呼叫慣例
package Number;
sub minus {
my ($self, $other, $swap) = @_;
my $result = $$self - $other; # *
$result = -$result if $swap;
ref $result ? $result : bless \$result;
}
# * may recurse once - see table below
所有在 use overload
指令中指定的子常式都會傳遞三個引數(有例外 - 請參閱下方,特別是 "nomethod")。
第一個是提供重載運算子實作的運算元 - 在這個例子中,就是呼叫其 minus()
方法的物件。
第二個參數是另一個運算元,或是一元運算子的 undef
。
第三個參數在(且僅在)兩個運算元被交換時設定為 TRUE。Perl 可能會這麼做以確保第一個參數 ($self
) 是實作重載運算的物件,符合一般的物件呼叫慣例。例如,如果 $x
和 $y
是 Number
operation | generates a call to
============|======================
$x - $y | minus($x, $y, '')
$x - 7 | minus($x, 7, '')
7 - $x | minus($x, 7, 1)
Perl 也可能使用 minus()
來實作其他未在 use overload
指令中指定的運算子,根據稍後說明的 "Magic Autogeneration" 規則。例如,上述的 use overload
並未為任何運算子 --
、neg
(一元減號的重載鍵)或 -=
宣告子程式。因此
operation | generates a call to
============|======================
-$x | minus($x, 0, 1)
$x-- | minus($x, 1, undef)
$x -= 3 | minus($x, 3, undef)
請注意 undef
:當自動產生導致標準運算子(例如 -
)的方法不會變更其任何運算元,而用於實作會變更運算元(「變異器」:在此為 --
和 -=
)的運算子時,Perl 會將 undef 傳遞為第三個參數。這仍會評估為 FALSE,與運算元未被交換的事實一致,但讓子程式有機會在這些情況下變更其行為。
在上述所有範例中,minus()
僅需要傳回減法的結果:Perl 會負責將其指派給 $x。事實上,此類方法不應變更其運算元,即使將 undef
傳遞為第三個參數(請參閱 "可重載運算")。
++
和 --
的實作並非如此:它們預期會變更其運算元。--
的適當實作可能如下所示
use overload '--' => "decr",
# ...
sub decr { --${$_[0]}; }
如果啟用「位元」功能(請參閱 feature),則會將第五個 TRUE 參數傳遞給處理 &
、|
、^
和 ~
的子程式。這表示呼叫者預期數值行為。第四個參數會是 undef
,因為該位置 ($_[3]
) 保留給 "nomethod" 使用。
術語「數學魔術」描述數學運算子的過載實作。數學魔術運算會引發問題。考慮下列程式碼
$a = $b;
--$a;
如果 $a
和 $b
是純量,則在這些陳述句後
$a == $b - 1
然而,物件是對受祝福資料的參考,所以如果 $a
和 $b
是物件,則指定 $a = $b
僅複製參考,讓 $a
和 $b
參考相同的物件資料。因此,我們可能會預期運算 --$a
會遞減 $b
和 $a
。然而,這與我們預期數學運算子運作的方式不一致。
Perl 透過在呼叫定義為實作變動器(--
、+=
等)的方法之前,透明地呼叫複製建構函數來解決此困境。在上述範例中,當 Perl 到達遞減陳述句時,它會複製 $a
中的物件資料,並指定 $a
為複製資料的參考。然後才會呼叫 decr()
,它會變更複製的資料,讓 $b
保持不變。因此,盡可能保留物件隱喻,同時數學魔術運算仍根據算術隱喻運作。
注意:前一段描述 Perl 根據純量自動產生物件的複製建構函數時會發生什麼事。對於其他情況,請參閱 "複製建構函數"。
可在 use overload
指令中指定的完整金鑰清單,以空格分隔,出現在雜湊 %overload::ops
的值中
with_assign => '+ - * / % ** << >> x .',
assign => '+= -= *= /= %= **= <<= >>= x= .=',
num_comparison => '< <= > >= == !=',
'3way_comparison' => '<=> cmp',
str_comparison => 'lt le gt ge eq ne',
binary => '& &= | |= ^ ^= &. &.= |. |.= ^. ^.=',
unary => 'neg ! ~ ~.',
mutators => '++ --',
func => 'atan2 cos sin exp abs log sqrt int',
conversion => 'bool "" 0+ qr',
iterators => '<>',
filetest => '-X',
dereferencing => '${} @{} %{} &{} *{}',
matching => '~~',
special => 'nomethod fallback =',
大部分可過載運算子會一對一對應到這些金鑰。例外,包括此雜湊中未顯現的其他可過載運算,包含在下列附註中。此清單會隨著時間而增加。
如果嘗試註冊上述未找到的運算子,會發出警告。
not
運算子 not
不是 use overload
的有效金鑰。然而,如果運算子 !
已過載,則相同的實作會用於 not
(因為兩個運算子僅在優先順序上有所不同)。
neg
neg
鍵用於一元減號,以區別於二元 -
。
++
, --
假設它們的行為類似於 Perl 的 ++
和 --
,則這些運算子的重載實作需要變異其運算元。
遞增和遞減運算子的前置和後置形式之間沒有區別:它們僅在 Perl 在評估表達式時呼叫關聯子程式時有所不同。
指定
+= -= *= /= %= **= <<= >>= x= .=
&= |= ^= &.= |.= ^.=
簡單指定不可重載('='
鍵用於 「複製建構函式」)。Perl 確實有辦法讓指定至物件執行您想要的任何動作,但這需要使用 tie(),而非重載 - 請參閱 perlfunc 中的 「tie」 和以下的 「COOKBOOK」 範例。
運算子指定變異的子程式只需要傳回運算結果。它可以變更其運算元的數值(這是安全的,因為 Perl 會先呼叫複製建構函式),但這是可選的,因為 Perl 會將傳回值指定給左手邊的運算元。
重載指定運算子的物件僅針對指定至該物件的指定進行重載。換句話說,Perl 永遠不會呼叫對應的方法,且第三個引數(「交換」引數)設定為 TRUE。例如,運算
$a *= $b
不會導致呼叫 $b
的 *=
實作,即使 $a
是純量。(然而,它可以產生呼叫 $b
的 *
方法)。
具有變異變異的非變異器
+ - * / % ** << >> x .
& | ^ &. |. ^.
如上所述,Perl 可能會在實作遺失的運算式(如 ++
、+=
和 &=
)時,呼叫運算式(如 +
和 &
)的方法。雖然這些方法可以透過測試第三個引數的定義來偵測此用法,但它們在所有情況下都應避免變更其運算元。這是因為 Perl 在呼叫這些方法之前不會呼叫複製建構函式。
int
傳統上,Perl 函式 int
會四捨五入為 0(請參閱perlfunc 中的「int」),因此對於浮點數類型的資料,應遵循相同的語意。
字串、數字、布林和正規表示式轉換
"" 0+ bool
這些轉換會根據需要依據上下文呼叫。例如,'""'
(字串化)的子常式可用於將超載物件傳遞為 print
的引數,而 'bool'
則可用於在流程控制陳述式(如 while
)或三元 ?:
運算式的條件中測試。
當然,在例如 $obj + 1
等的上下文中,Perl 會呼叫 $obj
的 +
實作,而不是(在此範例中)使用數字化方法 '0+'
將 $obj
轉換為數字(例外情況是未提供 '+'
的方法,且「fallback」設為 TRUE)。
'""'
、'0+'
和 'bool'
的子常式可以傳回任何任意的 Perl 值。如果此值的對應運算式也超載,則會再次使用此值呼叫該運算式。
如果超載傳回物件本身,則會直接使用該物件,這是一種特殊情況。傳回物件的超載轉換可能是錯誤的,因為您可能會取得類似於 YourPackage=HASH(0x8172b34)
的結果。
qr
'qr'
的子常式用於將物件內插到正規表示式或用作正規表示式的地方,包括出現在 =~
或 !~
運算式的 RHS 時。
qr
必須傳回已編譯的正規表示式,或已編譯正規表示式的參照(例如 qr//
傳回的),且將會忽略傳回值上的任何進一步超載。
反覆運算
如果 <>
被覆寫,則 讀取檔案句柄 語法 <$var>
和 glob 語法 <${var}>
都會使用相同的實作。
檔案測試
金鑰 '-X'
用於指定一個子常式來處理所有檔案測試運算子(-f
、-x
等:請參閱 perlfunc 中的「-X」 以取得完整清單);無法個別覆寫任何檔案測試運算子。為了區分它們,'-' 後面的字母會作為第二個參數傳遞(也就是在用於傳遞第二個運算元的二元運算子中的位置)。
呼叫覆寫的檔案測試運算子不會影響與特殊檔案句柄 _
關聯的 stat 值。它仍然會參考最後一次 stat
、lstat
或未覆寫的檔案測試的結果。
此覆寫是在 Perl 5.12 中引入的。
比對
金鑰 "~~"
允許您覆寫 ~~
運算子和 switch 建構(given
/when
)所使用的智慧型比對邏輯。請參閱 perlsyn 中的「Switch Statements」 和 feature。
不同尋常的是,智慧型比對運算子的覆寫實作並未完全控制智慧型比對行為。特別是在以下程式碼中
package Foo;
use overload '~~' => 'match';
my $obj = Foo->new();
$obj ~~ [ 1,2,3 ];
智慧型比對不會像這樣呼叫方法
$obj->match([1,2,3],0);
相反地,智慧型比對分配規則優先,因此 $obj 會與每個陣列元素依序進行智慧型比對,直到找到比對為止,因此您可能會看到一到三個這樣的呼叫
$obj->match(1,0);
$obj->match(2,0);
$obj->match(3,0);
有關何時呼叫重載,請參閱 perlop 中的「Smartmatch 運算子」 中的比對表。
解除參考
${} @{} %{} &{} *{}
如果這些運算子沒有明確重載,則它們會以正常方式運作,產生底層純量、陣列或儲存物件資料的任何內容 (或如果解除參考運算子與其不符,則會產生適當的錯誤訊息)。定義萬用 'nomethod'
(請參閱下方) 對此沒有影響,因為不會呼叫萬用函式來實作遺失的解除參考運算子。
如果重載解除參考運算子,則它必須傳回適當類型的參考 (例如,金鑰 '${}'
的子常式應傳回純量的參考,而不是純量),或重載運算子的另一個物件:也就是說,子常式只決定解除什麼參考,而實際的解除參考則交給 Perl。作為特殊情況,如果子常式傳回物件本身,則不會再次呼叫它,以避免無限遞迴。
特殊
nomethod fallback =
如果找不到運算的方法,則 Perl 會嘗試從已定義的運算中自動產生替代實作。
注意:可以透過將 fallback
設定為 FALSE 來停用本節中描述的行為 (請參閱"fallback")。
在下列表格中,數字表示優先順序。例如,下表指出,如果沒有定義 '!'
的實作,則 Perl 會使用 'bool'
來實作它 (也就是說,透過反轉 'bool'
方法傳回的值);如果也未實作布林轉換,則 Perl 會使用 '0+'
,或者如果失敗,則使用 '""'
。
operator | can be autogenerated from
|
| 0+ "" bool . x
=========|==========================
0+ | 1 2
"" | 1 2
bool | 1 2
int | 1 2 3
! | 2 3 1
qr | 2 1 3
. | 2 1 3
x | 2 1 3
.= | 3 2 4 1
x= | 3 2 4 1
<> | 2 1 3
-X | 2 1 3
注意:迭代器 ('<>'
) 和檔案測試 ('-X'
) 算子會正常運作:如果運算元不是受祝福的 glob 或 IO 參考,它會轉換成字串 (使用 '""'
、'0+'
或 'bool'
的方法) 來解釋為 glob 或檔案名稱。
operator | can be autogenerated from
|
| < <=> neg -= -
=========|==========================
neg | 1
-= | 1
-- | 1 2
abs | a1 a2 b1 b2 [*]
< | 1
<= | 1
> | 1
>= | 1
== | 1
!= | 1
* one from [a1, a2] and one from [b1, b2]
如同數字比較可以從 '<=>'
的方法自動產生,字串比較可以從 'cmp'
的方法自動產生
operators | can be autogenerated from
====================|===========================
lt gt le ge eq ne | cmp
類似地,金鑰 '+='
和 '++'
的自動產生類似於上述的 '-='
和 '--'
operator | can be autogenerated from
|
| += +
=========|==========================
+= | 1
++ | 1 2
其他指派變異類似於 '+='
和 '-='
(以及類似於上述的 '.='
和 'x='
)
operator || *= /= %= **= <<= >>= &= ^= |= &.= ^.= |.=
-------------------||-------------------------------------------
autogenerated from || * / % ** << >> & ^ | &. ^. |.
另請注意,複製建構函式 (金鑰 '='
) 可以自動產生,但僅限於基於純量的物件。請參閱 "複製建構函式"。
由於某些運算可以從其他運算自動產生,因此有一組最小的運算需要重載,才能一次擁有完整的重載運算組。當然,自動產生的運算可能無法完全符合使用者的預期。最小組為
+ - * / % ** << >> x
<=> cmp
& | ^ ~ &. |. ^. ~.
atan2 cos sin exp log sqrt int
"" 0+ bool
~~
在轉換中,僅需要字串、布林或數字其中一種,因為每種都可以從其他兩種產生。
use overload
的特殊金鑰nomethod
'nomethod'
金鑰用於指定一個 catch-all 函式,以呼叫任何未個別重載的運算子。指定的函式將傳遞四個參數。前三個參數與傳遞給對應方法 (如果已定義) 的參數相同。第四個參數是該遺失方法的 use overload
金鑰。如果啟用「位元」功能 (請參閱 功能),會傳遞第五個 TRUE 參數給處理 &
、|
、^
和 ~
的子常式,以表示呼叫者預期數字行為。
例如,如果 $a
是已指派給宣告套件的物件
use overload 'nomethod' => 'catch_all', # ...
則運算
3 + $a
可以(除非特別為金鑰 '+'
宣告方法)導致呼叫
catch_all($a, 3, 1, '+')
請參閱 "Perl 如何選擇運算子實作"。
fallback
指派給金鑰 'fallback'
的值會告訴 Perl 它應該多努力尋找替代方式來實作遺失的運算子。
已定義,但為 FALSE
use overload "fallback" => 0, # ... ;
這會停用 "Magic Autogeneration"。
未定義
在未明確指派值給 fallback
的預設情況下,會啟用 magic 自動產生。
TRUE
與 undef
相同,但如果無法自動產生遺失的運算子,則 Perl 會被允許回復到如果沒有 use overload
指令時會執行的動作,而不是發出錯誤訊息。
注意:在多數情況下,特別是 "複製建構函式",這不太可能是適當的行為。
請參閱 "Perl 如何選擇運算子實作"。
如 上文所述,當將變異器套用至與其他參考共用其物件的參考時,會呼叫此運算。例如,如果 $b
是數學的,且 '++'
已使用 'incr'
進行過載,而 '='
已使用 'clone'
進行過載,則程式碼
$a = $b;
# ... (other code which does not modify $a or $b) ...
++$b;
會以等同於
$a = $b;
# ...
$b = $b->clone(undef, "");
$b->incr(undef, "");
的方式執行。
注意
'='
的子常式不會過載 Perl 指派運算子:它只用於允許變異器如本文所述的方式運作。(請參閱上文的 "指派"。)
複製建構函式僅在呼叫宣告為實作變異器的函式之前呼叫,例如,如果上述程式碼中的 ++$b;
是透過宣告給金鑰 '++'
的方法(或 'nomethod',傳遞 '++'
作為第四個引數)或透過自動產生 '+='
來實作的。如果增量運算透過呼叫 '+'
的方法來實作,則不會呼叫複製建構函式,因為在等效的程式碼中,
$a = $b;
$b = $b + 1;
$a
參照的資料不會因為將新物件資料的參照指定給 $b
而改變。
如果 Perl 判斷不需要複製建構函式,因為沒有其他參照修改資料,則不會呼叫複製建構函式。
如果 'fallback'
未定義或為 TRUE,則可以自動產生複製建構函式,但僅適用於基於純量的物件。在其他情況下,需要明確定義。如果物件資料儲存在純量陣列中,以下內容可能是適當的
use overload '=' => sub { bless [ @{$_[0]} ] }, # ...
如果 'fallback'
為 TRUE 且未定義複製建構函式,則對於非基於純量的物件,Perl 可能會靜默地回退到簡單指定,也就是指定物件參照。實際上,這會停用複製建構函式機制,因為不會建立物件資料的新副本。這幾乎肯定不是您想要的。(然而,這是一致的:例如,Perl 對 ++
算子的回退是遞增參照本身。)
會先檢查哪一個,nomethod
還是 fallback
?如果算子的兩個運算元類型不同且都重載算子,會使用哪個實作?以下是優先順序規則
如果第一個運算元已宣告子常式來重載算子,則使用該實作。
否則,如果 fallback
對第一個運算元為 TRUE 或未定義,則查看自動產生規則是否允許使用其其他算子。
除非算子是指定(+=
、-=
等),否則針對第二個運算元重複步驟 (1)。
針對第二個運算元重複步驟 (2)。
如果第一個運算元有「nomethod」方法,則使用該方法。
如果第二個運算元有「nomethod」方法,則使用該方法。
如果兩個運算元的 fallback
皆為 TRUE,則執行運算子的一般運算,將運算元視為數字、字串或布林值,視運算子而定(請參閱註解)。
無效 - 停止。
如果只有一個運算元(或只有一個具備重載的運算元),則會略過針對上述另一個運算元的檢查。
上述規則有例外,針對解除參考運算(如果步驟 1 失敗,則永遠回歸到一般內建實作 - 請參閱解除參考),以及 ~~
(有其自己的規則集 - 請參閱上述「可重載運算」下的「比對」)。
步驟 7 註解:有些運算子具有不同的語意,視其運算元的類型而定。由於無法指示 Perl 將運算元視為數字而非字串等,因此此處的結果可能與您的預期不同。請參閱「錯誤和陷阱」。
比較運算的限制在於,即使例如 cmp
應傳回已祝福的參考,自動產生的 lt
函數也只會根據 cmp
結果的數值產生標準邏輯值。特別是,此情況需要運作良好的數值轉換(可能以其他轉換表示)。
類似地,如果套用字串轉換替換,.=
和 x=
運算子會失去其數學魔法屬性。
當您對數學魔法物件執行 chop() 時,它會提升為字串,並失去其數學魔法屬性。其他運算也可能發生相同情況。
重載會透過 @ISA 階層尊重繼承。繼承會以兩種方式與重載互動。
use overload
指令中的方法名稱如果
use overload key => value;
中的 value
是字串,它會被解釋為方法名稱 - 這個方法名稱可以(用一般的方式)從其他類別繼承而來。
任何從覆寫類別繼承的類別也會被覆寫,並繼承其運算子實作。如果同一個運算子在多個祖先中被覆寫,那麼實作會由一般的繼承規則決定。
例如,如果 A
從 B
和 C
繼承(順序為此),B
使用 \&D::plus_sub
覆寫 +
,而 C
使用 "plus_meth"
覆寫 +
,那麼子常式 D::plus_sub
會被呼叫來實作封裝 A
中物件的運算 +
。
請注意,在 Perl 5.18 之前的版本中,fallback
鍵的繼承不受上述規則規範。會使用第一個覆寫祖先中的 fallback
值。這在 5.18 中已修正,以遵循一般的繼承規則。
由於所有 use
指令都在編譯時期執行,因此在執行時期變更覆寫的唯一方法是
eval 'use overload "+" => \&addmethod';
您也可以使用
eval 'no overload "+", "--", "<="';
儘管在執行時期使用這些建構有待商榷。
封裝 overload.pm
提供下列公用函式
提供 arg
的字串值,就像在沒有字串化覆寫的情況下一樣。如果您使用這個來取得參考的位址(用於檢查兩個參考是否指向同一個東西),您最好使用 builtin::refaddr()
或 Scalar::Util::refaddr()
,它們比較快。
如果 arg
會受到某些運算的覆寫,則傳回 true。
傳回 undef
或實作 op
的方法參考。
此類方法總是採用三個引數,如果它是 XS 方法,將會強制執行。
對於某些應用程式,Perl 語法剖析器會過度扭曲常數。可以透過 overload::constant()
和 overload::remove_constant()
函式連結到此程序。
這些函式採用雜湊作為引數。此雜湊的已辨識金鑰為
用於重載整數常數,
用於重載浮點常數,
用於重載八進位和十六進位常數,
用於重載 q
引號字串、qq
和 qx
引號字串的常數部分,以及 here 文件,
用於重載正規表達式的常數部分。
對應的值是函式的參考,採用三個引數:第一個是常數的初始字串形式,第二個是 Perl 如何詮釋此常數,第三個是常數如何使用。請注意,初始字串形式不包含字串分隔符號,而且反斜線分隔符號組合中的反斜線已移除(因此分隔符號的值與處理此字串無關)。此函式的傳回值是 Perl 如何詮釋此常數。第三個引數未定義,除非是重載的 q
和 qr
常數,在單引號內容中為 q
(來自字串、正規表達式和單引號 HERE 文件),在 tr
/y
算子的引數中為 tr
,在 s
算子的右側為 s
,否則為 qq
。
由於運算式 "ab$cd,,"
只是 'ab' . $cd . ',,'
的捷徑,因此預期重載的常數字串會配備合理的重載串接運算子,否則會產生荒謬的結果。類似地,負數視為正數常數的否定。
請注意,除了 import() 和 unimport() 方法之外,從其他地方呼叫函式 overload::constant() 和 overload::remove_constant() 可能沒有意義。從這些方法中,它們可以呼叫為
sub import {
shift;
return unless @_;
die "unknown import: @_" unless @_ == 1 and $_[0] eq ':constant';
overload::constant integer => sub {Math::BigInt->new(shift)};
}
下列內容可能會因 RSN 而變更。
所有作業的方法表格都快取在封裝的符號表雜湊的魔法中。在處理 use overload
、no overload
、新的函式定義和 @ISA 中的變更時,快取會失效。
(每個 SVish 物件都有個 magic 佇列,而 magic 是該佇列中的項目。這表示單一變數可以同時參與多種形式的 magic。例如,環境變數通常同時具有兩種形式:它們的 %ENV magic 和 taint magic。不過,實作 overloading 的 magic 會套用至儲存區,而儲存區很少直接使用,因此不應減慢 Perl 速度。)
如果套件使用 overloading,它會攜帶特殊旗標。在定義新函式或修改 @ISA 時,也會設定此旗標。在支援 overloading 的第一個作業之後,會出現輕微速度損失,因為會更新 overloading 表格。如果沒有 overloading,旗標會關閉。因此,此後唯一的速度損失是檢查此旗標。
預期不會變更的方法引數為常數(但未強制執行)。
請新增範例至以下內容!
將此內容放入 Perl 程式庫目錄中的 two_face.pm
package two_face; # Scalars with separate string and
# numeric values.
sub new { my $p = shift; bless [@_], $p }
use overload '""' => \&str, '0+' => \&num, fallback => 1;
sub num {shift->[1]}
sub str {shift->[0]}
使用方式如下
require two_face;
my $seven = two_face->new("vii", 7);
printf "seven=$seven, seven=%d, eight=%d\n", $seven, $seven+1;
print "seven contains 'i'\n" if $seven =~ /i/;
(第二行會建立一個同時具有字串值和數字值的 scalar。)這會列印
seven=vii, seven=7, eight=8
seven contains 'i'
假設您想建立一個物件,可以同時作為陣列參考和雜湊參考存取。
package two_refs;
use overload '%{}' => \&gethash, '@{}' => sub { $ {shift()} };
sub new {
my $p = shift;
bless \ [@_], $p;
}
sub gethash {
my %h;
my $self = shift;
tie %h, ref $self, $self;
\%h;
}
sub TIEHASH { my $p = shift; bless \ shift, $p }
my %fields;
my $i = 0;
$fields{$_} = $i++ foreach qw{zero one two three};
sub STORE {
my $self = ${shift()};
my $key = $fields{shift()};
defined $key or die "Out of band access";
$$self->[$key] = shift;
}
sub FETCH {
my $self = ${shift()};
my $key = $fields{shift()};
defined $key or die "Out of band access";
$$self->[$key];
}
現在可以使用陣列和雜湊語法存取物件
my $bar = two_refs->new(3,4,5,6);
$bar->[2] = 11;
$bar->{two} == 11 or die 'bad hash fetch';
請注意此範例的幾個重要特徵。首先,$bar 的實際類型是 scalar 參考,我們不會 overloading scalar 取消參照。因此,我們可以使用 $$bar
(我們在 overloading 取消參照的函式中執行的動作)取得 $bar 的實際未 overloading 內容。類似地,TIEHASH() 方法傳回的物件是 scalar 參考。
其次,每次使用雜湊語法時,我們會建立新的繫結雜湊。這讓我們不必擔心參考迴圈的可能性,這將導致記憶體外洩。
這兩個問題都可以解決。假設我們要在實作為雜湊本身的物件參考上 overloading 雜湊取消參照,唯一必須解決的問題是如何存取這個實際雜湊(與 overloading 取消參照運算子顯示的虛擬雜湊相反)。以下是一個可能的擷取常式
sub access_hash {
my ($self, $key) = (shift, shift);
my $class = ref $self;
bless $self, 'overload::dummy'; # Disable overloading of %{}
my $out = $self->{$key};
bless $self, $class; # Restore overloading
$out;
}
若要移除在每次存取時建立的連結雜湊,可以多加一層間接層,這允許建立非循環的參考結構
package two_refs1;
use overload
'%{}' => sub { ${shift()}->[1] },
'@{}' => sub { ${shift()}->[0] };
sub new {
my $p = shift;
my $a = [@_];
my %h;
tie %h, $p, $a;
bless \ [$a, \%h], $p;
}
sub gethash {
my %h;
my $self = shift;
tie %h, ref $self, $self;
\%h;
}
sub TIEHASH { my $p = shift; bless \ shift, $p }
my %fields;
my $i = 0;
$fields{$_} = $i++ foreach qw{zero one two three};
sub STORE {
my $a = ${shift()};
my $key = $fields{shift()};
defined $key or die "Out of band access";
$a->[$key] = shift;
}
sub FETCH {
my $a = ${shift()};
my $key = $fields{shift()};
defined $key or die "Out of band access";
$a->[$key];
}
現在,如果 $baz 這樣被覆寫,則 $baz
會是對中間陣列的參考的參考,而該陣列會保留對實際陣列和存取雜湊的參考。存取雜湊的 tie() 物件會是對實際陣列的參考的參考,因此
沒有任何參考迴圈。
被祝福成 two_refs1
類別的兩個「物件」都是對陣列的參考的參考,因此是對 純量 的參考。因此,存取器運算式 $$foo->[$ind]
沒有包含任何覆寫的運算。
將此放入 Perl 函式庫目錄中的 symbolic.pm
package symbolic; # Primitive symbolic calculator
use overload nomethod => \&wrap;
sub new { shift; bless ['n', @_] }
sub wrap {
my ($obj, $other, $inv, $meth) = @_;
($obj, $other) = ($other, $obj) if $inv;
bless [$meth, $obj, $other];
}
這個模組很不同於一般的覆寫模組:它不提供任何一般的覆寫運算子,而是提供 "nomethod"
的實作。在此範例中,nomethod
子常式會傳回一個物件,用來封裝在物件上執行的運算:symbolic->new(3)
包含 ['n', 3]
,2 + symbolic->new(3)
包含 ['+', 2, ['n', 3]]
。
以下是使用上述套件「計算」外接八邊形邊長的範例指令碼
require symbolic;
my $iter = 1; # 2**($iter+2) = 8
my $side = symbolic->new(1);
my $cnt = $iter;
while ($cnt--) {
$side = (sqrt(1 + $side**2) - 1)/$side;
}
print "OK\n";
$side 的值為
['/', ['-', ['sqrt', ['+', 1, ['**', ['n', 1], 2]],
undef], 1], ['n', 1]]
請注意,雖然我們使用一個漂亮的小指令碼取得這個值,但沒有簡單的方法可以使用這個值。事實上,這個值可以在偵錯程式中檢查(請參閱 perldebug),但前提是已設定 bareStringify
O 選項,且不能透過 p
指令。
如果有人嘗試列印這個值,則會呼叫覆寫運算子 ""
,而該運算子會呼叫 nomethod
運算子。此運算子的結果會再次字串化,但這個結果的類型仍為 symbolic
,這會導致無限迴圈。
將漂亮的列印器方法新增到 symbolic.pm 模組
sub pretty {
my ($meth, $a, $b) = @{+shift};
$a = 'u' unless defined $a;
$b = 'u' unless defined $b;
$a = $a->pretty if ref $a;
$b = $b->pretty if ref $b;
"[$meth $a $b]";
}
現在,可以透過以下方式完成指令碼
print "side = ", $side->pretty, "\n";
pretty
方法會執行物件轉換為字串,因此使用這個方法覆寫運算子 ""
是很自然的。不過,在這種方法內部,不需要漂亮列印物件的組成部分 $a 和 $b。在上述子常式中,"[$meth $a $b]"
是幾個字串和組成部分 $a 和 $b 的串接。如果這些組成部分使用覆寫,則串接運算子會尋找覆寫運算子 .
;如果沒有找到,則會尋找覆寫運算子 ""
。因此,使用以下內容就足夠了
use overload nomethod => \&wrap, '""' => \&str;
sub str {
my ($meth, $a, $b) = @{+shift};
$a = 'u' unless defined $a;
$b = 'u' unless defined $b;
"[$meth $a $b]";
}
現在可以將腳本的最後一行變更為
print "side = $side\n";
其輸出為
side = [/ [- [sqrt [+ 1 [** [n 1 u] 2]] u] 1] [n 1 u]]
而且可以使用所有可能的方法在偵錯器中檢查值。
仍有一些問題:考慮腳本的迴圈變數 $cnt。它是一個數字,而不是物件。我們無法將這個值設為 symbolic
類型,因為這樣迴圈將不會終止。
的確,要終止迴圈,$cnt 應變為 false。但是,用於檢查假值的運算子 bool
已被覆寫(這次是透過覆寫 ""
),並傳回一個長字串,因此 symbolic
類型的任何物件都是 true。為了解決這個問題,我們需要一種將物件與 0 比較的方法。事實上,撰寫一個數字轉換常式較為容易。
以下是加入此類常式(並略微修改 str())的 symbolic.pm 文字
package symbolic; # Primitive symbolic calculator
use overload
nomethod => \&wrap, '""' => \&str, '0+' => \#
sub new { shift; bless ['n', @_] }
sub wrap {
my ($obj, $other, $inv, $meth) = @_;
($obj, $other) = ($other, $obj) if $inv;
bless [$meth, $obj, $other];
}
sub str {
my ($meth, $a, $b) = @{+shift};
$a = 'u' unless defined $a;
if (defined $b) {
"[$meth $a $b]";
} else {
"[$meth $a]";
}
}
my %subr = (
n => sub {$_[0]},
sqrt => sub {sqrt $_[0]},
'-' => sub {shift() - shift()},
'+' => sub {shift() + shift()},
'/' => sub {shift() / shift()},
'*' => sub {shift() * shift()},
'**' => sub {shift() ** shift()},
);
sub num {
my ($meth, $a, $b) = @{+shift};
my $subr = $subr{$meth}
or die "Do not know how to ($meth) in symbolic";
$a = $a->num if ref $a eq __PACKAGE__;
$b = $b->num if ref $b eq __PACKAGE__;
$subr->($a,$b);
}
數字轉換的所有工作都在 %subr 和 num() 中完成。當然,%subr 尚未完成,它只包含以下範例中使用的運算子。額外的問題:為什麼我們需要在 num() 中明確遞迴?(答案在本章節的結尾。)
像這樣使用這個模組
require symbolic;
my $iter = symbolic->new(2); # 16-gon
my $side = symbolic->new(1);
my $cnt = $iter;
while ($cnt) {
$cnt = $cnt - 1; # Mutator '--' not implemented
$side = (sqrt(1 + $side**2) - 1)/$side;
}
printf "%s=%f\n", $side, $side;
printf "pi=%f\n", $side*(2**($iter+2));
它會列印(不會有這麼多換行符號)
[/ [- [sqrt [+ 1 [** [/ [- [sqrt [+ 1 [** [n 1] 2]]] 1]
[n 1]] 2]]] 1]
[/ [- [sqrt [+ 1 [** [n 1] 2]]] 1] [n 1]]]=0.198912
pi=3.182598
上述模組非常原始。它未實作變異器方法(++
、-=
等),未執行深度複製(在沒有變異器的情況下不需要!),而且只實作範例中使用的那些算術運算。
實作大多數算術運算很簡單;只需使用運算表格,並將填入 %subr 的程式碼變更為
my %subr = ( 'n' => sub {$_[0]} );
foreach my $op (split " ", $overload::ops{with_assign}) {
$subr{$op} = $subr{"$op="} = eval "sub {shift() $op shift()}";
}
my @bins = qw(binary 3way_comparison num_comparison str_comparison);
foreach my $op (split " ", "@overload::ops{ @bins }") {
$subr{$op} = eval "sub {shift() $op shift()}";
}
foreach my $op (split " ", "@overload::ops{qw(unary func)}") {
print "defining '$op'\n";
$subr{$op} = eval "sub {$op shift()}";
}
由於實作賦值運算子的子常式不需要修改其運算元(請參閱上方的「可重載運算」),我們不需要任何特殊功能來讓 +=
和相關運算子運作,除了將這些運算子新增至 %subr 並定義一個複製建構函數(這是必要的,因為 Perl 無法得知 '+='
的實作不會變異引數,請參閱「複製建構函數」)。
若要實作複製建構函數,請將 '=' => \&cpy
新增至 use overload
列,以及程式碼(此程式碼假設變異器只會變更一個層級深度的內容,因此不需要遞迴複製)
sub cpy {
my $self = shift;
bless [@$self], ref $self;
}
若要讓 ++
和 --
運作,我們需要實作實際的變異器,可以透過直接方式或在 nomethod
中實作。我們會繼續在 nomethod
中執行操作,因此請在 wrap() 的第一行後新增
if ($meth eq '++' or $meth eq '--') {
@$obj = ($meth, (bless [@$obj]), 1); # Avoid circular reference
return $obj;
}
這並不是最有效的實作,可以考慮
sub inc { $_[0] = bless ['++', shift, 1]; }
作為替代方案。
最後,請注意可以透過
my %subr = ( 'n' => sub {$_[0]} );
foreach my $op (split " ", $overload::ops{with_assign}) {
$subr{$op} = $subr{"$op="} = eval "sub {shift() $op shift()}";
}
my @bins = qw(binary 3way_comparison num_comparison str_comparison);
foreach my $op (split " ", "@overload::ops{ @bins }") {
$subr{$op} = eval "sub {shift() $op shift()}";
}
foreach my $op (split " ", "@overload::ops{qw(unary func)}") {
$subr{$op} = eval "sub {$op shift()}";
}
$subr{'++'} = $subr{'+'};
$subr{'--'} = $subr{'-'};
填入 %subr。這完成在 50 行 Perl 程式碼中實作基礎符號計算器的動作。由於子表達式的數值不會快取,因此計算器運作速度非常慢。
以下是練習的解答:在 str() 的情況下,我們不需要明確的遞迴,因為重載的 .
運算子會回退至現有的重載運算子 ""
。如果未明確要求 fallback
,重載的算術運算子不會回退至數值轉換。因此,如果沒有明確的遞迴,num() 會將 ['+', $a, $b]
轉換為 $a + $b
,這只會重建 num() 的引數。
如果您好奇為什麼 str() 和 num() 的轉換預設值不同,請注意撰寫符號計算器有多麼容易。這種簡潔性是因為適當地選擇預設值。額外說明:因為明確遞迴,所以 num() 比 sym() 脆弱:我們需要明確檢查 $a 和 $b 的類型。如果元件 $a 和 $b 碰巧是某種相關類型,這可能會導致問題。
您可能會好奇為什麼我們稱呼上述計算器為符號計算器。原因在於表達式的實際值計算會延後到值使用時才進行。
要看到實際運作,請新增一個方法
sub STORE {
my $obj = shift;
$#$obj = 1;
@$obj->[0,1] = ('=', shift);
}
到套件 symbolic
。在做完這個變更之後,您可以執行
my $a = symbolic->new(3);
my $b = symbolic->new(4);
my $c = sqrt($a**2 + $b**2);
,而 $c 的數字值會變成 5。不過,在呼叫
$a->STORE(12); $b->STORE(5);
之後,$c 的數字值會變成 13。現在毫無疑問,symbolic 模組確實提供了一個符號計算器。
為了隱藏引擎蓋下的粗糙邊緣,請提供一個 tie()d 介面到套件 symbolic
。新增方法
sub TIESCALAR { my $pack = shift; $pack->new(@_) }
sub FETCH { shift }
sub nop { } # Around a bug
(錯誤已在 Perl 5.14 中修正,說明請參閱 "BUGS")。您可以使用這個新介面,如下所示
tie $a, 'symbolic', 3;
tie $b, 'symbolic', 4;
$a->nop; $b->nop; # Around a bug
my $c = sqrt($a**2 + $b**2);
現在 $c 的數字值是 5。在 $a = 12; $b = 5
之後,$c 的數字值會變成 13。為了隔離模組使用者,請新增一個方法
sub vars { my $p = shift; tie($_, $p), $_->nop foreach @_; }
現在
my ($a, $b);
symbolic->vars($a, $b);
my $c = sqrt($a**2 + $b**2);
$a = 3; $b = 4;
printf "c5 %s=%f\n", $c, $c;
$a = 12; $b = 5;
printf "c13 %s=%f\n", $c, $c;
顯示 $c 的數字值會隨著 $a 和 $b 的值變更而變更。
Ilya Zakharevich <ilya@math.mps.ohio-state.edu>。
overloading
pragma 可用於在詞彙範圍內啟用或停用重載運算子 - 參閱 overloading。
當 Perl 以 -Do 參數或等效參數執行時,重載會引發診斷訊息。
使用 Perl 除錯器的 m
指令 (參閱 perldebug),可以推論哪些運算子已重載 (以及哪個祖先觸發此重載)。例如,如果 eq
已重載,則除錯器會顯示方法 (eq
。方法 ()
對應於 fallback
鍵 (事實上,此方法的存在表示此套件已啟用重載,而這是模組 overload
的 Overloaded
函數所使用的)。
模組可能會發出下列警告
(W) 對 overload::constant 的呼叫包含奇數個引數。引數應成對出現。
(W) 您嘗試重載 overload 套件不知道的常數類型。
(W) overload::constant 的第二個 (第四個、第六個,...) 引數必須是程式碼參考。匿名子常式或子常式的參考。
(W) use overload
傳遞了一個它不識別的引數。您是否輸入錯誤的運算子?
當 fallback 為 TRUE 且 Perl 採用運算子的內建實作時,陷阱是有些運算子具有多個語義,例如 |
use overload '0+' => sub { $_[0]->{n}; },
fallback => 1;
my $x = bless { n => 4 }, "main";
my $y = bless { n => 8 }, "main";
print $x | $y, "\n";
您可能會預期這會輸出「12」。事實上,它會印出「<」:將「|」視為按位元字串運算子處理的 ASCII 結果 - 也就是說,將運算元視為字串「4」和「8」而不是數字處理的結果。numify (0+
) 已實作但 stringify (""
) 尚未實作的事實並無差別,因為後者只是從前者自動產生。
變更此設定的唯一方法是為 '|'
提供您自己的子常式。
魔術自動產生會增加無意間建立自參照結構的可能性。目前 Perl 在循環明確中斷之前,不會釋放自參照結構。例如,
use overload '+' => 'add';
sub add { bless [ \$_[0], \$_[1] ] };
會造成問題,因為
$obj += $y;
實際上會變成
$obj = add($obj, $y, undef);
與
$obj = [\$obj, \$foo];
的結果相同。即使指令碼中沒有運算子的明確指派變異,它們也可能會由最佳化器產生。例如,
"obj = $obj\n"
可能會最佳化為
my $tmp = 'obj = ' . $obj; $tmp .= "\n";
符號表會填滿看起來像線路雜訊的名稱。
此錯誤已在 Perl 5.18 中修正,但如果您使用的是舊版本,仍然可能會讓您絆倒
為了繼承的目的,每個過載套件的行為就像 fallback
存在一樣(可能未定義)。如果某些套件未過載,但繼承自兩個過載套件,這可能會產生有趣的影響。
在 Perl 5.14 之前,過載和 tie() 之間的關係中斷。過載會根據 tie()d 變數的先前類別觸發或不觸發。
這發生是因為在嘗試任何 tie()d 存取之前,太早檢查是否過載。如果從繫結變數 FETCH() 的值的類別沒有變更,則要在較舊的 Perl 版本上執行程式碼的簡單解決方法是在 tie() 之後立即存取值(透過 () = $foo
或類似方式),以便在呼叫之後,先前的類別與目前的類別一致。
裸字元不受過載字串常數涵蓋。
範圍運算子 ..
無法過載。