この章ではメソッドの探索と起動について話す。
この章ではメソッド呼び出しとメソッド定義の話を両方やる関係で、 実に様々な「引数」が出てくる。そこで紛らわしくならないように ここで厳密に用語を決めてしまうことにしよう。
m(a) # aは「通常の引数」 m(*list) # listは「配列引数」 m(&block) # blockは「ブロック引数」 def m(a) # aは「通常のパラメータ」 def m(a=nil) # aは「オプションパラメータ」、nilは「そのデフォルト値」 def m(*rest) # restは「restパラメータ」 def m(&block) # blockは「ブロックパラメータ」
ようするに渡すときは全部「引数」、受けるほうは全部「パラメータ」で 種類によってそれぞれ形容詞を付ける、ということだ。
ただし、ここで挙げたうち「ブロック引数」と「ブロックパラメータ」に ついては次章『ブロック』で扱うことになる。
obj.method(7,8)
NODE_CALL nd_mid = 9049 (method) nd_recv: NODE_VCALL nd_mid = 9617 (obj) nd_args: NODE_ARRAY [ 0: NODE_LIT nd_lit = 7:Fixnum 1: NODE_LIT nd_lit = 8:Fixnum ]
メソッド呼び出しのノードはNODE_CALL
だ。
nd_args
にはNODE_ARRAY
のリストとして引数が格納されている。
それとこの他にメソッド呼び出しノードとしてはNODE_FCALL
と
NODE_VCALL
とい
うのもある。NODE_FCALL
が「method(args)
」の形式で、NODE_VCALL
はローカル
変数と同じ「method
」という形式の呼び出しに対応している。実際には
FCALL
とVCALL
は一つにまとめることもできるが、VCALL
のときには引数を準備
するためのコードが必要ないので、その分のメモリと時間を節約するためだけに
区別されている。
ではrb_eval()
でのNODE_CALL
のハンドラを見てみよう。
2745 case NODE_CALL: 2746 { 2747 VALUE recv; 2748 int argc; VALUE *argv; /* used in SETUP_ARGS */ 2749 TMP_PROTECT; 2750 2751 BEGIN_CALLARGS; 2752 recv = rb_eval(self, node->nd_recv); 2753 SETUP_ARGS(node->nd_args); 2754 END_CALLARGS; 2755 2756 SET_CURRENT_SOURCE(); 2757 result = rb_call(CLASS_OF(recv),recv,node->nd_mid,argc,argv,0); 2758 } 2759 break; (eval.c)
問題は三つのマクロ、BEGIN_CALLARGS SETUP_ARGS() END_CALLARGS
だろう。
rb_eval()
がレシーバの評価でrb_call()
がメソッド起動らしいので、この三つ
のマクロでは引数の評価をしているんだろうなあ、とはなんとなく想像できる
が、実際のところ何をしているのだろうか。BEGIN_CALLARGS
とEND_CALLARGS
は
イテレータの話をしてからでないとわかりづらいので次章『ブロック』で
改めて説明する。ここではSETUP_ARGS()
についてだけ調査しよう。
SETUP_ARGS()
SETUP_ARGS()
はメソッドの引数部分を評価するマクロである。このマクロ内
では、元プログラムのコメントにもあるように、argc
とargv
という変数を
使うのでそれをあらかじめ定義しておかなければならない。また
TMP_ALLOC()
を使うのでTMP_PROTECT
も使っておかなければならない。
だから以下のようにするのが定型である。
int argc; VALUE *argv; /* used in SETUP_ARGS */ TMP_PROTECT; SETUP_ARGS(args_node);
args_node
がメソッドの引数(を表現するノード)で、それを評価した
値の配列に変え、argv
に格納する。では見てみよう。
1780 #define SETUP_ARGS(anode) do {\ 1781 NODE *n = anode;\ 1782 if (!n) {\ 引数なし 1783 argc = 0;\ 1784 argv = 0;\ 1785 }\ 1786 else if (nd_type(n) == NODE_ARRAY) {\ 通常引数のみ 1787 argc=n->nd_alen;\ 1788 if (argc > 0) {\ 引数あり 1789 int i;\ 1790 n = anode;\ 1791 argv = TMP_ALLOC(argc);\ 1792 for (i=0;i<argc;i++) {\ 1793 argv[i] = rb_eval(self,n->nd_head);\ 1794 n=n->nd_next;\ 1795 }\ 1796 }\ 1797 else {\ 引数なし 1798 argc = 0;\ 1799 argv = 0;\ 1800 }\ 1801 }\ 1802 else {\ 配列引数や 1803 VALUE args = rb_eval(self,n);\ ブロック引数がある 1804 if (TYPE(args) != T_ARRAY)\ 1805 args = rb_ary_to_ary(args);\ 1806 argc = RARRAY(args)->len;\ 1807 argv = ALLOCA_N(VALUE, argc);\ 1808 MEMCPY(argv, RARRAY(args)->ptr, VALUE, argc);\ 1809 }\ 1810 } while (0) (eval.c)
ちょっと長いが、キッパリと三つに分岐しているので実はたいして恐くない。 それぞれの枝の意味はコメントに入れておいた通りだ。
引数なしのときはどうでもいいとして、残りの二つの枝では似たようなことを やっている。おおまかに言うとやっていることは三段階で、
である。コードに書き込んでみるとこうだ (ついでにちょっと整形しておいた)。
/***** else if節、argc!=0 *****/ int i; n = anode; argv = TMP_ALLOC(argc); /* 1 */ for (i = 0; i < argc; i++) { argv[i] = rb_eval(self, n->nd_head); /* 2,3 */ n = n->nd_next; } /***** else節 *****/ VALUE args = rb_eval(self, n); /* 2 */ if (TYPE(args) != T_ARRAY) args = rb_ary_to_ary(args); argc = RARRAY(args)->len; argv = ALLOCA_N(VALUE, argc); /* 1 */ MEMCPY(argv, RARRAY(args)->ptr, VALUE, argc); /* 3 */
else if
側ではTMP_ALLOC()
を使っているのにelse
側では
ALLOCA_N()
、つまり普通のalloca()
を使っているのはどうしてだろう。
C_ALLOCA
な環境ではalloca()
はmalloc()
に等しいのだから、危険では
ないだろうか。
この点は「else
側では引数の値がargs
にも入っている」ことが
ポイントである。図にすれば図1のようになる。
図1: ヒープにあっても大丈夫
一つでもVALUE
がスタック上にあればそこを経由して連鎖的にマークされる。
そのようなVALUE
は他のVALUE
をスタックに繁ぎ止める錨(anchor)のような
役割を果たす、即ち「anchor VALUE
」となる。
else
側ではargs
がanchor VALUE
である。
ちなみにanchor VALUE
というのは今作った造語だ。
rb_call()
SETUP_ARGS()
はどちらかといえば横道だ。ここからは本筋に戻ろう。メソッド
を起動する関数rb_call()
である。本物には見付からなかった場合に例外を上
げたりするコードがあるのだが、例によって全部省略する。
static VALUE rb_call(klass, recv, mid, argc, argv, scope) VALUE klass, recv; ID mid; int argc; const VALUE *argv; int scope; { NODE *body; int noex; ID id = mid; struct cache_entry *ent; /* メソッドキャッシュを検索 */ ent = cache + EXPR1(klass, mid); if (ent->mid == mid && ent->klass == klass) { /* キャッシュにヒットした */ klass = ent->origin; id = ent->mid0; noex = ent->noex; body = ent->method; } else { /* キャッシュミス。地道に検索 */ body = rb_get_method_body(&klass, &id, &noex); } /* ……可視性チェックをする…… */ return rb_call0(klass, recv, mid, id, argc, argv, body, noex & NOEX_UNDEF); }
基本的なメソッド探索の方法については第2章『オブジェクト』で話した。スーパー
クラスをたどりながらm_tbl
を検索すればいい。それをやるのが
search_method()
であった。
原理はその通りなのだがしかし実際に実行する段になるとメソッド呼び出しの
たびに何回もハッシュを引いて検索していたのでは速度が遅すぎる。これを改
善するためruby
では一回呼び出したメソッドはキャッシュされるようになって
いる。一回呼ばれたメソッドはすぐにまた呼び出されることが多い、ということ
が経験上の事実として知られており、このキャッシュのヒット率は高い。
そのキャッシュを索いているのがrb_call()
の前半である。この
ent = cache + EXPR1(klass, mid);
の一行だけでキャッシュが検索されている。仕組みはあとで詳しく見よう。
キャッシュが外れたときはその次のrb_get_method_body()
で地道にクラスツリー
を検索して、ついでにその結果をキャッシュしておくようになっている。
検索全体の流れを図にすれば図2のような感じだ。
図2: メソッド探索
次にメソッドキャッシュの構造を詳しく見てみよう。
180 #define CACHE_SIZE 0x800 181 #define CACHE_MASK 0x7ff 182 #define EXPR1(c,m) ((((c)>>3)^(m))&CACHE_MASK) 183 184 struct cache_entry { /* method hash table. */ 185 ID mid; /* method's id */ 186 ID mid0; /* method's original id */ 187 VALUE klass; /* receiver's class */ 188 VALUE origin; /* where method defined */ 189 NODE *method; 190 int noex; 191 }; 192 193 static struct cache_entry cache[CACHE_SIZE]; (eval.c)
仕組みを一言で言えばハッシュテーブルである。ハッシュテーブルの原理とは ようするにテーブル検索を配列のインデクシングに変換することであった。そ の時に必要になるものは三つ。データを格納する配列、キー、そしてハッシュ 関数だ。
まず配列はここではstruct cache_entry
の配列である。そしてメソッドはクラ
スとメソッド名だけで一意に決まるから、この二つがハッシュ計算のキーとな
る。あとはそのキーからキャッシュ配列のインデックス(0x000
〜0x7ff
)を生
成するハッシュ関数を作ればよい。それがEXPR1()
だ。引数のc
はクラスオブ
ジェクトで、m
はメソッド名(のID
)である(図3)。
図3: メソッドキャッシュ
ただしEXPR1()
は完全ハッシュ関数でもなんでもないので違うメソッドが偶然
同じインデックスを生成してしまうこともありうる。だがこれはあくまでキャッ
シュなので衝突しても問題はない。ただ動作が少し遅くなるだけである。
ところでメソッドキャッシュは実際どのくらいの効果があるのだろうか。 「……ことが知られている」と言われても納得できない。自分で計測だ。
種類 | プログラム | ヒット率 | |||
LALR(1)パーサ生成 | racc ruby.y | 99.9% | |||
メールスレッド生成 | とあるメーラ | 99.1% | |||
ドキュメント生成 | rd2html rubyrefm.rd | 97.8% |
なんと、実験した三つの例すべてでヒット率95%以上を記録した。 これは凄い。どうやら「……ことが知られている」の効果は 抜群のようだ。
rb_call0()
いろいろあってようやくメソッド起動に辿りついたわけだが、このrb_call0()
が
またデカい。200行以上ということはページ上では5、6ページになるだろうか。
ビューアならともかく、紙で一気に並べると悲惨なことになるので細かく分割
しながら見ていくことにしよう。まずは概形から。
4482 static VALUE 4483 rb_call0(klass, recv, id, oid, argc, argv, body, nosuper) 4484 VALUE klass, recv; 4485 ID id; 4486 ID oid; 4487 int argc; /* OK */ 4488 VALUE *argv; /* OK */ 4489 NODE *body; /* OK */ 4490 int nosuper; 4491 { 4492 NODE *b2; /* OK */ 4493 volatile VALUE result = Qnil; 4494 int itr; 4495 static int tick; 4496 TMP_PROTECT; 4497 4498 switch (ruby_iter->iter) { 4499 case ITER_PRE: 4500 itr = ITER_CUR; 4501 break; 4502 case ITER_CUR: 4503 default: 4504 itr = ITER_NOT; 4505 break; 4506 } 4507 4508 if ((++tick & 0xff) == 0) { 4509 CHECK_INTS; /* better than nothing */ 4510 stack_check(); 4511 } 4512 PUSH_ITER(itr); 4513 PUSH_FRAME(); 4514 4515 ruby_frame->last_func = id; 4516 ruby_frame->orig_func = oid; 4517 ruby_frame->last_class = nosuper?0:klass; 4518 ruby_frame->self = recv; 4519 ruby_frame->argc = argc; 4520 ruby_frame->argv = argv; 4521 4522 switch (nd_type(body)) { /* ……本処理…… */ 4698 4699 default: 4700 rb_bug("unknown node type %d", nd_type(body)); 4701 break; 4702 } 4703 POP_FRAME(); 4704 POP_ITER(); 4705 return result; 4706 } (eval.c)
まずITER
を積んでこのメソッドがイテレータかどうかを最終的に決定する。そ
の後のPUSH_FRAME()
でその値がすぐに使われるのでPUSH_ITER()
はその前にな
ければいけない。PUSH_FRAME()
はすぐあとで見る。
そして先に「……本処理……」のところの話をすると、ここには 以下のようなノード別に分かれてそれぞれ起動処理をする。
NODE_CFUNC | Cで定義されたメソッド | ||
NODE_IVAR | attr_reader | ||
NODE_ATTRSET | attr_writer | ||
NODE_SUPER | super | ||
NODE_ZSUPER | 引数なしのsuper | ||
NODE_DMETHOD | UnboundMethod の起動 | ||
NODE_BMETHOD | Method の起動 | ||
NODE_SCOPE | Rubyで定義されたメソッド |
本書では説明していないものもあるが、どれもさして重要でないので無視して
いい。重要なのはNODE_CFUNC
とNODE_SCOPE
、それにNODE_ZSUPER
だけだ。
PUSH_FRAME()
536 #define PUSH_FRAME() do { \ 537 struct FRAME _frame; \ 538 _frame.prev = ruby_frame; \ 539 _frame.tmp = 0; \ 540 _frame.node = ruby_current_node; \ 541 _frame.iter = ruby_iter->iter; \ 542 _frame.cbase = ruby_frame->cbase; \ 543 _frame.argc = 0; \ 544 _frame.argv = 0; \ 545 _frame.flags = FRAME_ALLOCA; \ 546 ruby_frame = &_frame 548 #define POP_FRAME() \ 549 ruby_current_node = _frame.node; \ 550 ruby_frame = _frame.prev; \ 551 } while (0) (eval.c)
まずFRAME
がスタックベタ置き確保であることを確認したい。ここは
module_setup()
と同じだ。あとは基本的に普通の初期化をしているだけである。
一つだけ付け加えるなら、FRAME_ALLOCA
というフラグがFRAME
の
割り当てかたを示していること、くらいだろうか。FRAME_ALLOCA
とは
もちろん「スタック上にある」ことを示している。
rb_call0()
−NODE_CFUNC
ここのコードには本物を見るといろいろ書いてあるのだが、ほとんどが
trace_func
関係なので実質的なコードは以下の一行で終わりだ。
case NODE_CFUNC: result = call_cfunc(body->nd_cfnc, recv, len, argc, argv); break;
ではcall_cfunc()
はと言うと……
4394 static VALUE 4395 call_cfunc(func, recv, len, argc, argv) 4396 VALUE (*func)(); 4397 VALUE recv; 4398 int len, argc; 4399 VALUE *argv; 4400 { 4401 if (len >= 0 && argc != len) { 4402 rb_raise(rb_eArgError, "wrong number of arguments(%d for %d)", 4403 argc, len); 4404 } 4405 4406 switch (len) { 4407 case -2: 4408 return (*func)(recv, rb_ary_new4(argc, argv)); 4409 break; 4410 case -1: 4411 return (*func)(argc, argv, recv); 4412 break; 4413 case 0: 4414 return (*func)(recv); 4415 break; 4416 case 1: 4417 return (*func)(recv, argv[0]); 4418 break; 4419 case 2: 4420 return (*func)(recv, argv[0], argv[1]); 4421 break; : : 4475 default: 4476 rb_raise(rb_eArgError, "too many arguments(%d)", len); 4477 break; 4478 } 4479 return Qnil; /* not reached */ 4480 } (eval.c)
このように、引数の数に応じて分岐するだけだ。 ちなみに引数の最大個数は15である。
一つ注意してほしいのは、NODE_CFUNC
のときにはSCOPE
やVARS
を積んでいない
ことだ。メソッドがC
で定義されていればRubyのローカル変数は使わないので
当然と言えば当然である。しかしそれは同時にC
から「現在の」ローカル変数
にアクセスすると一つ前のFRAME
のローカル変数が見えてしまうということを
意味している。そして実際にそれをやっているところもある。例えば
rb_svar
(eval.c
)など。
rb_call0()
−NODE_SCOPE
NODE_SCOPE
即ちRubyで定義したメソッドの起動である。
Rubyの基礎をなす部分だ。
4568 case NODE_SCOPE: 4569 { 4570 int state; 4571 VALUE *local_vars; /* OK */ 4572 NODE *saved_cref = 0; 4573 4574 PUSH_SCOPE(); 4575 /* (A)CREFの伝達 */ 4576 if (body->nd_rval) { 4577 saved_cref = ruby_cref; 4578 ruby_cref = (NODE*)body->nd_rval; 4579 ruby_frame->cbase = body->nd_rval; 4580 } /* (B)ruby_scope->local_varsの初期化 */ 4581 if (body->nd_tbl) { 4582 local_vars = TMP_ALLOC(body->nd_tbl[0]+1); 4583 *local_vars++ = (VALUE)body; 4584 rb_mem_clear(local_vars, body->nd_tbl[0]); 4585 ruby_scope->local_tbl = body->nd_tbl; 4586 ruby_scope->local_vars = local_vars; 4587 } 4588 else { 4589 local_vars = ruby_scope->local_vars = 0; 4590 ruby_scope->local_tbl = 0; 4591 } 4592 b2 = body = body->nd_next; 4593 4594 PUSH_VARS(); 4595 PUSH_TAG(PROT_FUNC); 4596 4597 if ((state = EXEC_TAG()) == 0) { 4598 NODE *node = 0; 4599 int i; /* ……(C)引数をローカル変数に代入…… */ 4666 if (trace_func) { 4667 call_trace_func("call", b2, recv, id, klass); 4668 } 4669 ruby_last_node = b2; /* (D)メソッド本体 */ 4670 result = rb_eval(recv, body); 4671 } 4672 else if (state == TAG_RETURN) { /* returnで戻った */ 4673 result = prot_tag->retval; 4674 state = 0; 4675 } 4676 POP_TAG(); 4677 POP_VARS(); 4678 POP_SCOPE(); 4679 ruby_cref = saved_cref; 4680 if (trace_func) { 4681 call_trace_func("return", ruby_last_node, recv, id, klass); 4682 } 4683 switch (state) { 4684 case 0: 4685 break; 4686 4687 case TAG_RETRY: 4688 if (rb_block_given_p()) { 4689 JUMP_TAG(state); 4690 } 4691 /* fall through */ 4692 default: 4693 jump_tag_but_local_jump(state); 4694 break; 4695 } 4696 } 4697 break; (eval.c)
(A)前章の定数のところで話したCREF
の伝達を行う。
つまりメソッドエントリからFRAME
にcbase
を移植する。
(B)ここの内容はmodule_setup()
でやったのと全く同じである。
SCOPE
のlocal_vars
に配列を割り当ててやる。これとPUSH_SCOPE()
、
PUSH_VARS()
でローカル変数のスコープ生成が完了した。これ以後は
メソッド内部と全く同じ環境でrb_eval()
することができる。
(C)受け取った引数をメソッドのパラメータ変数にセットしていく。
パラメータ変数とはようするにローカル変数と同じものだ。引数の
数などがNODE_ARGS
で指定されているから地道にセットしてやればいい。
詳しいことはこのあとすぐに説明しよう。そして、
(D)メソッド本体を実行する。当然ながらレシーバ(recv
)をself
にする。
つまりrb_eval()
の第一引数にする。これでメソッドは完全に起動した。
ではすっとばした引数セット部分を詳しく見ていくが、 その前にまずメソッドの構文木をもう一度見てほしい。
% ruby -rnodedump -e 'def m(a) nil end' NODE_SCOPE nd_rval = (null) nd_tbl = 3 [ _ ~ a ] nd_next: NODE_BLOCK nd_head: NODE_ARGS nd_cnt = 1 nd_rest = -1 nd_opt = (null) nd_next: NODE_BLOCK nd_head: NODE_NEWLINE nd_file = "-e" nd_nth = 1 nd_next: NODE_NIL nd_next = (null)
NODE_ARGS
がメソッドのパラメータを指定するノードだ。
いくつかダンプしまくってみたところ、そのメンバは以下のように
使われているようだ。
nd_cnt | 通常のパラメータの数。 | ||
nd_rest | rest パラメータの変数ID 。rest パラメータがなければ-1 。 | ||
nd_opt | オプションパラメータのデフォルト値を表現する構文木が入っている。NODE_BLOCK のリスト。 |
これだけの情報があれば各パラメータ変数に対応するローカル変数IDが一意に
決まる。まず0と1は常に$_
と$~
だった。その次の2から通常のパラメータ
がその数だけ並ぶ。そのまた次にオプションパラメータが並ぶ。オプションパ
ラメータの数はNODE_BLOCK
の長さでわかる。そのまた後にrestパラメータが来
る。
例えば次のような定義をすると、
def m(a, b, c = nil, *rest) lvar1 = nil end
ローカル変数IDは次のように割り当てられる。
0 1 2 3 4 5 6 $_ $~ a b c rest lvar1
いいだろうか。ではこれを踏まえてコードを見てみよう。
4601 if (nd_type(body) == NODE_ARGS) { /* 本体なし */ 4602 node = body; /* NODE_ARGS */ 4603 body = 0; /* メソッド本体 */ 4604 } 4605 else if (nd_type(body) == NODE_BLOCK) { /* 本体がある */ 4606 node = body->nd_head; /* NODE_ARGS */ 4607 body = body->nd_next; /* メソッド本体 */ 4608 } 4609 if (node) { /* なんらかのパラメータがある */ 4610 if (nd_type(node) != NODE_ARGS) { 4611 rb_bug("no argument-node"); 4612 } 4613 4614 i = node->nd_cnt; 4615 if (i > argc) { 4616 rb_raise(rb_eArgError, "wrong number of arguments(%d for %d)", 4617 argc, i); 4618 } 4619 if (node->nd_rest == -1) { /* restパラメータがない */ /* パラメータの数を数える */ 4620 int opt = i; /* パラメータの数(iはnd_cnt) */ 4621 NODE *optnode = node->nd_opt; 4622 4623 while (optnode) { 4624 opt++; 4625 optnode = optnode->nd_next; 4626 } 4627 if (opt < argc) { 4628 rb_raise(rb_eArgError, 4629 "wrong number of arguments(%d for %d)", argc, opt); 4630 } /* rb_call0では二回目の代入 */ 4631 ruby_frame->argc = opt; 4632 ruby_frame->argv = local_vars+2; 4633 } 4634 4635 if (local_vars) { /* パラメータがある */ 4636 if (i > 0) { /* 通常のパラメータがある */ 4637 /* $_と$~の領域をよけるために+2 */ 4638 MEMCPY(local_vars+2, argv, VALUE, i); 4639 } 4640 argv += i; argc -= i; 4641 if (node->nd_opt) { /* オプションパラメータがある */ 4642 NODE *opt = node->nd_opt; 4643 4644 while (opt && argc) { 4645 assign(recv, opt->nd_head, *argv, 1); 4646 argv++; argc--; 4647 opt = opt->nd_next; 4648 } 4649 if (opt) { 4650 rb_eval(recv, opt); 4651 } 4652 } 4653 local_vars = ruby_scope->local_vars; 4654 if (node->nd_rest >= 0) { /* restパラメータがある */ 4655 VALUE v; 4656 /* 余っている引数を配列化して変数に代入 */ 4657 if (argc > 0) 4658 v = rb_ary_new4(argc,argv); 4659 else 4660 v = rb_ary_new2(0); 4661 ruby_scope->local_vars[node->nd_rest] = v; 4662 } 4663 } 4664 } (eval.c)
今までより多めにコメントを入れておいたのでそれを見ながら地道に追っても らえればやっていることはわかるだろう。
一つ気になったのはruby_frame
のargc
とargv
のことである。restパラメータが
ないときだけ更新しているようなのだが、どうしてrestパラメータがないとき
だけでいいのだろうか。
この点はargc
とargv
の使い道を考えるとわかる。このメンバは実は
引数を省略したsuper
のためにあるのだ。つまり以下のような形式だ。
super
こちらのsuper
には現在実行中のメソッドのパラメータをそのまま引き渡す働
きがある。そのときに渡せるようにruby_frame->argv
に引数を保存しておくわ
けだ。
ここで元の話に戻ると、restパラメータがあるときにはsuper
では元の引数リ
ストを渡すほうがなんとなく都合がよさそうな気がする。ないときには、オプ
ションパラメータが代入されたあとのもののほうがよさそうだ。
def m(a, b, *rest) super # たぶん5, 6, 7, 8を渡すべき end m(5, 6, 7, 8) def m(a, b = 6) super # たぶん5, 6を渡すべき end m(5)
これは「そうでなければならない」というよりは仕様としてどちらがいいか、 という問題である。メソッドにrestパラメータがあるならそのスーパークラス でも同じくrestパラメータがあると考えられるから、まとめた後の値を渡すと 不便になる可能性が非常に高いのだ。
さて、いろいろ言ったがメソッドの起動の話はこれで全ておしまいだ。
あとはこの章の締めくくりとしていま話題にしたsuper
の実装を見ておく
ことにしよう。
super
super
に対応するのはNODE_SUPER
とNODE_ZSUPER
である。
NODE_SUPER
が普通のsuper
で、
NODE_ZSUPER
は引数指定なしのsuper
だ。
2780 case NODE_SUPER: 2781 case NODE_ZSUPER: 2782 { 2783 int argc; VALUE *argv; /* used in SETUP_ARGS */ 2784 TMP_PROTECT; 2785 /*(A)superが禁止されている場合 */ 2786 if (ruby_frame->last_class == 0) { 2787 if (ruby_frame->orig_func) { 2788 rb_name_error(ruby_frame->last_func, 2789 "superclass method `%s' disabled", 2790 rb_id2name(ruby_frame->orig_func)); 2791 } 2792 else { 2793 rb_raise(rb_eNoMethodError, "super called outside of method"); 2794 } 2795 } /*(B)引数の準備または評価 */ 2796 if (nd_type(node) == NODE_ZSUPER) { 2797 argc = ruby_frame->argc; 2798 argv = ruby_frame->argv; 2799 } 2800 else { 2801 BEGIN_CALLARGS; 2802 SETUP_ARGS(node->nd_args); 2803 END_CALLARGS; 2804 } 2805 /*(C)まだ謎のPUSH_ITER() */ 2806 PUSH_ITER(ruby_iter->iter?ITER_PRE:ITER_NOT); 2807 SET_CURRENT_SOURCE(); 2808 result = rb_call(RCLASS(ruby_frame->last_class)->super, 2809 ruby_frame->self, ruby_frame->orig_func, 2810 argc, argv, 3); 2811 POP_ITER(); 2812 } 2813 break; (eval.c)
引数指定なしのsuper
ではruby_frame->argv
をそのまま引数にすると
言ったが、それが(B)にそのまま出ている。
(C)rb_call()
を呼ぶ直前にPUSH_ITER()
している。これもまた詳しくは説
明できないのだが、こうしておくと現在のメソッドに渡されたブロックをそっ
くりそのまま次のメソッド(つまりこれから呼ぶスーパークラスのメソッド)
に委譲できる。
そして最後に(A)ruby_frame->last_class
が0であるときはsuper
の
呼び出しを禁止しているようだ。エラーメッセージには
「must be enabled by rb_enable_super()
」
とあるので、rb_enable_super()
を呼べば無事呼べるようになるらしい。
どうしてだろう。
まずlast_class
が0になるときはどういうときか調べてみると、どうも実体が
Cで定義されているメソッド(NODE_CFUNC
)を実行中だとそうなるようだ。
さらにそういうメソッドをalias
したり置き換えたりしたときも同じだ。
そこまではわかったのだが、ソースコードを読んでみてもどうしてもその続き
がわからない。わからないので仕方なくruby
のメーリングリストのログを
「rb_enable_super
」で検索して発見した。そのメールによると、次のような
ことらしい。
例えばString.new
というメソッドがある。もちろん、文字列を生成するメソッ
ドである。String.new
はT_STRING
な構造体を作る。従ってString
の
インスタンスメソッドはレシーバが常にT_STRING
であると期待して書いていい。
さて、String.new
のsuper
はObject.new
である。Object.new
は
T_OBJECT
な構造体を作る。ではString.new
を置き換え定義してsuper
して
しまったらどうなるだろう。
def String.new super end
結果として、構造体はT_OBJECT
なのにクラスがString
、というオブジェクトが
できる。しかしString
のメソッドはT_STRING
の構造体を期待して書いているの
で当然落ちる。
それを避けるにはどうしたらいいかと言うと、違う構造体型を期待しているメ
ソッドは呼べないようにすればよい。しかしメソッドには「期待する構造体型」
なんて情報は付いていないし、クラスにもない。例えばString
クラスから
T_STRING
を取る方法があれば呼ぶ前にチェックできそうだが、そういうこと
は現時点ではできない。だから次善の策として「Cで定義したメソッドからの
super
は禁止」ということにしてあるのだ。それなら、Cレベルのメソッド階層
をきっちり作っておけばとりあえず落とすことはできない。そうして、「絶対
に大丈夫だからsuper
させてほしい」という場合はrb_enable_super()
を
呼んでおけばsuper
できるようになる。
ようするに問題の本質は構造体型ミスマッチである。 アロケーションフレームワークで起こった問題と同じだ。
それで解決策としてはどうすればいいかと言うと、問題の根源である「クラ スがインスタンスの構造体型を知らない」という点を解決すればよい。だがそ のためには最低でも新しいAPIが必要になるし、もっと徹底的にやるなら互換性 がなくなる。そういうわけでいまのところはまだ最終的な解決策は決まってい ない。
御意見・御感想・誤殖の指摘などは 青木峰郎 <aamine@loveruby.net> までお願いします。
『Rubyソースコード完全解説』 はインプレスダイレクトで御予約・御購入いただけます (書籍紹介ページへ飛びます)。
Copyright (c) 2002-2004 Minero Aoki, All rights reserved.