each 関数
each を使ったループを途中で抜けると反復子(iterator)が初期化されない話。
my %hash = map {$_ => 1} qw/a b c/; while( my($k, $v) = each %hash ) { print "1st loop\n"; last; } while( my($k, $v) = each %hash ) { print "2nd loop\n"; }
このコードを実行すると
予想: 1st loop 2nd loop 2nd loop 2nd loop 結果: 1st loop 2nd loop 2nd loop
予想より, 2 回目のループの結果が 1 回少ないのですが, 結論は "perldoc -f each" に記述されていました。
There is a single iterator for each hash, shared by all "each", "keys", and "values" function calls in the program; it can be reset by reading all the elements from the hash, or by evaluating "keys HASH" or "values HASH".
ハッシュごとに反復子を持つため, 途中でループを抜けかつ次に最初から繰り返したい場合は, 反復子を初期化する必要があります。
my %hash = map {$_ => 1} qw/a b c/; while( my($k, $v) = each %hash ) { print "1st loop\n"; last; } keys %hash; # 初期化が必要 while( my($k, $v) = each %hash ) { print "2nd loop\n"; }
# ドキュメントはちゃんと読めと :-)
perl 5.13.1 のソースで確認です。
==== pp.c ==== 4619 PP(pp_each) 4620 { 4621 dVAR; 4622 dSP; 4623 HV * hash = MUTABLE_HV(POPs); 4624 HE *entry; 4625 const I32 gimme = GIMME_V; 4626 4627 PUTBACK; 4628 /* might clobber stack_sp */ 4629 entry = hv_iternext(hash); 4630 SPAGAIN;
each に対応するのが pp_each なので, その中で呼ばれている hv_iternext (4629行) を見ると
==== hv.h ==== 398 #define hv_iternext(hv) hv_iternext_flags(hv, 0) ==== embed.h ==== 2717 #define hv_iternext_flags(a,b) Perl_hv_iternext_flags(aTHX_ a,b) ==== hv.c ==== 2102 HE * 2103 Perl_hv_iternext_flags(pTHX_ HV *hv, I32 flags) 2104 { 2105 dVAR; 2106 register XPVHV* xhv; 2107 register HE *entry; 2108 HE *oldentry; 2109 MAGIC* mg; 2110 struct xpvhv_aux *iter; (snip) 2125 iter = HvAUX(hv);
HvAUX (2125行) が反復子(xpvhv_aux) を取得するマクロになっており
238 #define HvARRAY(hv) ((hv)->sv_u.svu_hash) 239 #define HvFILL(hv) ((XPVHV*) SvANY(hv))->xhv_fill 240 #define HvMAX(hv) ((XPVHV*) SvANY(hv))->xhv_max 241 /* This quite intentionally does no flag checking first. That's your 242 responsibility. */ 243 #define HvAUX(hv) ((struct xpvhv_aux*)&(HvARRAY(hv)[HvMAX(hv)+1]))
その定義からするとハッシュ要素格納用領域の後ろ HvMAX(hv)+1 番目に, そのハッシュの反復子が格納されている様です。
一方, keys(pp_keys), values(pp_values) は
==== mathoms.c ==== 733 /* Ops that are calls to do_kv. */ 734 PP(pp_values) 735 { 736 return do_kv(); 737 } 738 739 PP(pp_keys) 740 { 741 return do_kv(); 742 } ==== doop.c ==== 1429 OP * 1430 Perl_do_kv(pTHX) 1431 { 1432 dVAR; 1433 dSP; 1434 HV * const hv = MUTABLE_HV(POPs); 1435 HV *keys; (snip) 1452 keys = hv; 1453 (void)hv_iterinit(keys); /* always reset iterator regardless */
その実行に際して, hv_iterinit (1453行) が呼ばれます。
==== embed.h ==== 2713 #define hv_iterinit(a) Perl_hv_iterinit(aTHX_ a) ==== hv.c ==== 1922 I32 1923 Perl_hv_iterinit(pTHX_ HV *hv) 1924 { (snip) 1932 if (SvOOK(hv)) { 1933 struct xpvhv_aux * const iter = HvAUX(hv); 1934 HE * const entry = iter->xhv_eiter; /* HvEITER(hv) */ 1935 if (entry && HvLAZYDEL(hv)) { /* was deleted earlier? */ 1936 HvLAZYDEL_off(hv); 1937 hv_free_ent(hv, entry); 1938 } 1939 iter->xhv_riter = -1; /* HvRITER(hv) = -1 */ 1940 iter->xhv_eiter = NULL; /* HvEITER(hv) = NULL */ 1941 } else { 1942 hv_auxinit(hv); 1943 }
1939,1940 行にて iter(反復子) の初期化が行われています。
勉強不足で SvOOK (1932行) が何をしているのか理解できなかったのですが, SvOOK が偽であり hv_auxinit (1942行) が呼ばれた場合でも
==== embed.h ==== 3644 #define hv_auxinit S_hv_auxinit ==== hv.c ==== 1879 static struct xpvhv_aux* 1880 S_hv_auxinit(HV *hv) { 1881 struct xpvhv_aux *iter; 1882 char *array; (snip) 1897 iter = HvAUX(hv); 1898 1899 iter->xhv_riter = -1; /* HvRITER(hv) = -1 */ 1900 iter->xhv_eiter = NULL; /* HvEITER(hv) = NULL */ 1901 iter->xhv_name = 0; 1902 iter->xhv_backreferences = 0; 1903 iter->xhv_mro_meta = NULL;
と, iter を初期化する処理 (1899 - 1903行) が実行される様です。
また, hv_iterinit を呼び出している所に戻ると
==== doop.c ==== 1429 OP * 1430 Perl_do_kv(pTHX) 1431 { (snip) 1453 (void)hv_iterinit(keys); /* always reset iterator regardless */ 1454 1455 if (gimme == G_VOID) 1456 RETURN; 1457 1458 if (gimme == G_SCALAR) { 1459 IV i;
'gimme == G_VOID' (1455行) が真の場合, 即ち 'void context' で実行された際は, 反復子の初期化を行った直後に処理が終了しているため, 配列(keys, values の戻り値)の作成が行われません。
- keys, values はハッシュの要素数分だけ配列を作ってしまうので, 要素数が多い場合にパフォーマンスが悪くなる
- その場合, each の方がパフォーマンス的に優れている
- でも, (反復子を初期化するため) keys or values を呼び出したら意味が無いのでは?
との疑問がありましたが, それも問題ありませんでした。
参考: