staticおじさんという言葉があるらしい。 元記事を読んでると、ネタなのか本気なのか分からないので、とりあえず元記事は伏せておく。 それに、いろいろなところで評論されているからもう十分だろう。
そもそも、静的にするかどうかは言語がオブジェクト指向かどうかとはあまり関係なくて、クラスを作っていれば自然とstaticにすべきものとそうでないものが分かってくる。 それが分かっていてstaticが増えるなら問題ない。 それが分かっていなくてstaticが増えるのはマズい。 それだけだと思う。
前回渡された数値に引数で指定した値を足して返す簡単な関数を考える。 渡された値は次回の呼び出しで使うのでどこかにとっておく必要がある。 とりあえずプロトタイプの段階では
int foo(int x) { static int prev = 0; int result = prev + x; prev = x; return result; }
でいいだろう。
が、XとYで別々にprev
を持ちたいと思ったらこれではダメなのは明白だし、マルチスレッドになったらprev
をスレッドごとに持っておかなければならない。
スレッドローカル記憶域を使う手もあるが、この用途にはかえって面倒だ。
素直にint
のポインタを引数として渡した方が楽だ。
int foo(int x, int *prev) { int result = *prev + x; *prev = x; return result; }
そして、こういう「状態変数」が増えていくと引数で渡すのが面倒になるので構造体にまとめて渡すようになる。
C言語では特に区別してないが、この構造体のポインタが必要なものがいわゆるインスタンスメソッドで、いらないものが静的メソッド(クラスメソッド)に対応する。
ご想像の通り、これがオブジェクト指向言語のthis
である。
ポインタを渡したけど実は使いませんでしたてへぺろ(・ω<)という場合は静的メソッドが正解ということになる。
C言語の場合、どちらになるかは関数宣言を書く時点で自然に決まってくるだろう。
C#やJavaではグローバルな関数は作れないので、static
と書かれた関数を作ることになり、そしてこれで正解である。
C++の場合は静的メンバ関数でもグローバル関数でもどちらも実装可能だが、名前空間との兼ね合いで決めることが多いのではないだろうか。
ここから静的メンバー・静的メソッドのできること・できないことも自然に決まってくる。 構造体のポインタがないのだから構造体メンバーは使えないし、ポインタが必要な関数も呼び出せない。 当たり前だ。 プロトタイピングしていて最初はログ出力しかしないけど、本実装でメンバーを使う予定があるなら、最初からポインタを受け取っておかないと後で面倒なことになる。
構造体のメンバーが増えると初期化が面倒なので初期化関数を書くようになる。 かくして「構造体を初期化」→「構造体のポインタを関数として渡す」というコンボができあがるが、だいたいのアルゴリズムで同じようなことを毎回書くハメになるので、言語としてサポートしたのがいわゆる「クラス」だ。 今までの話は全て言語構造の中に隠蔽されるわけだが、内部的な実装はあまり変わらないので、やはり静的にするかどうかは自然に決まってくるものだ。
クラスが必要な場面、というか、クラスの方が楽に書ける場面というのも場数を踏めば分かってくる。 例えば、ちょっとだけ違う構造体に対して同じような処理をしなければならない場合に、関数を丸ごと全部書くのは面倒なので、差分だけ分岐して書くようになる。 Aという処理は共通で、最後にオブジェクトによってBかCの処理をする場合、例えばこんな風に書くようになる。
void foo(struct Something *obj, int proc, ...) { // A: 100 lines of common operation here switch (proc) { case 1: // operation B break; case 2: // operation C break; } }
マジックナンバーを書くなとか色々あるけど、そこは今は本質的ではないので目をつぶってもらおう。
switch
の分岐が増えてくると関数がどんどん大きくなっていくので、差分だけ別の関数にして指定するようになる。
C言語ではここで関数ポインタが出てきて多くの人が千尋の谷に突き落とされるわけだ。
JavaScriptならこんな感じ。
function foo(obj, bar) { // A: 100 lines of common operation here bar(obj); // operation for each object }
これをCではこう書くわけだ。
void foo(struct Something *obj, void (*bar)(void *), ...) { // A: 100 lines of common operation here bar(obj); // operation for each object }
もちろん、switch
で書いても、関数ポインタで書いても動く。
が、両者には決定的な差があって、前者はオブジェクトの種類が増えることに関数の中身を書き換える必要がある。
これは関数がライブラリの中にあったらほぼ不可能だ。
ソースがあればまだいいが、.a
・.so
・DLLで渡されたらほぼ絶望的だ。
一方、後者は差分は呼び出し側で自由に書ける。
後からオブジェクトの種類が増えても、増やした人が責任を持って書けばいいわけだ。
C言語ではあるがオブジェクト指向になっている。
そしてこの類の処理が増えてくると、引数として関数ポインタをたくさん渡すのは面倒なので構造体で渡すようになり、どうせオブジェクト本体も構造体で渡しているのだから関数ポインタを納めた構造体もオブジェクト本体にぶら下げてしまい、ついでに初期化関数で設定してしまえば、使う側は関数ポインタのことは忘れていいから便利だ。 こうして生まれたのが仮想関数テーブルで、仮想関数テーブルをいちいち書くのが面倒だから生まれてきたのがC++だ。 C++で書けばこうなる。
class Something { void foo(); virtual void bar(); } void Something::foo() { // 100 lines of common operation here bar(); }
明らかにC++のほうが面倒に見える。 これがクラスなんか書かなくても、という理由のひとつかもしれない。 C言語でオブジェクト指向のプログラムを書くのはホネが折れるので、本当に必要なときにしか使わないだろう。 C言語でオブジェクト指向で書く必要がない場面なのに、C++ではクラスを作らなければならない、というのもおかしな話なので、そういう場面ではC++でもクラスを使わないという方針は十分に考えられるわけだ。
そしてその究極の行き着く先がPHPかもしれない。 PHPでコードを書いてるとクラスを書く気分になれない。 というか、C++では絶対にクラスを書くような場面でもクラスなしで済んでしまう場合が多い。 なぜだろうと考えると、PHPは配列がすごく便利だからだと思う。
PHPの配列は数値インデックスの配列と連想配列の間に垣根がなく、PHPにもオブジェクトはあるのだが、オブジェクトを作るなら配列を使った方が楽なケースが多い。 関数に配列を渡す必要はあるが、配列としてもらってしまえばこっちのもの、なのである。 時々、PHPの配列は値渡しだからメモリや速度が・・・と言う人がいるが、そのあたりはZendエンジンが善きに計らってくれるので、今時のぺちぱーはあまり考えなくていいらしい。
こういうスクリプト系の言語では何通りかの方法で実装できるが、クラスにしないなら例えばこんなんでもいいわけだ。
function foo($obj) { // 100 lines of common operation here $obj["bar"]($obj); // operation for each object }
ここに書いた$obj
はPHPのオブジェクトではなく、連想配列である。
もちろん、引数として渡すこともできて、
function foo($obj, $bar) { // 100 lines of common operation here $bar($obj); // operation for each object }
でもいい。 両者の差は呼び出しごとに関数を指定するか、しないかである。 それによってどちらの方が楽かが決まってくる。
そして、気づいてみれば最後までクラスを書かずに完走してるわけだ。 だから「ぺちぱー」って揶揄されるんだろうな。
JavaScriptでも似たような話だろう。 JavaScriptの場合はアロー関数が便利すぎてクラスを書かずに済ませてしまうことが多い。 C++やPHPはクロージャー(ラムダ関数)がちょっと面倒くさいのだ。 クラスのプライベートメンバーはPHPでは配列に入れることになるが、JavaScriptでは外側の関数のローカル変数で済む場合が多々ある。 これも場数をこなしていればクラスにした方がいいのか、クロージャーのままの方が楽なのかが分かってくるし、拡張性を検討してもなおクロージャーで済ませた方がいいという場面もあるだろう。
コンストラクタだって動機は似たようなものだと思う。
構造体一万個を要素を持つ配列を初期化するときに、構造体のメンバを全部0すればよいのなら、C言語的には普通はmemset
するだろう。
ところが、構造体の中に1ビットのビットフィールド(いわゆるbool値型)があって、そこだけtrue
にしなければならないのなら、C言語なら愚直に一万回のループを書く必要がある。
C++ならコンストラクタにちゃらっと1行書けば言語側でやってくれるわけだ。
初期化しなければならないものが一万個あるなら、コンストラクタも一万回呼ばなきゃダメだ。
結局、オブジェクト指向言語なんて「同じことを書くのが面倒だから」産まれてきたものだと思う。
C言語でもオブジェクト指向なプログラムは書けて、その有名な産物がcontainer_of
だと思う。
アセンブラでもある程度の規模のプログラムを書くと結局は同じ道をたどり、大規模なアセンブラのプログラムを書いたことがあるなら、誰でも一度や二度は関数テーブルを作ったことがあるはずだ。 もっとも、今どきはブートしたらなるべく早くCに渡して楽に書くのが主流だし、8ビットCPUはアセンブラが有利なことも多いけどそこまでの規模にはならないので、大規模なアセンブラのプログラムを書くこと自体がほぼないだろうけど。
オブジェクト指向という言葉を初めて知った時、「へぇ、今まで(Cやアセンブラで)やってきたことはオブジェクト指向っていうのか」と思った。 色々な試行錯誤の結果、必要だったからたどり着いた場所であって、善し悪しは関係ないと思う。 それを無理やりになんにでも適用しようとするのが問題なのだと思う。
同じような話で、仮想関数にするのかテンプレートにするのかも、両方の本質を知っていればだいたい自動的に決まってくる。 ただ、仮想関数の場合、基底クラスのポインタしか情報がない場合はダブルポインタを解決して必ず実行時に実関数の呼び出しが起きるのに対して、テンプレートの場合はコンパイル時に関数を複製して直接その関数を呼びだすことができるばかりか、場合によってはインライン展開されて関数呼び出し自体がなくなる場合もある。 呼び出し回数が多く、C言語でちゃらっとマクロで書いていたようなものはテンプレートで書いたほうが性能が出ることが多いだろう。 そしてやりすぎて後で自分でコードを見た時にさっぱり分からなくなるわけだ。
仮想関数テーブルが増えすぎると全部の関数を定義するのが現実的ではなくなってくる。 そうやって産まれてきたのがメッセージ(イベント)システムじゃなかろうか。 イベントハンドラテーブルは持つことになるのだが、連想配列にしておけば仮想関数テーブルよりも小さくできるし、仮想関数テーブルの内容がコンパイル時に決まるのに対し、イベントハンドラテーブルは実行時に書き換え可能なのが大きな違いだろう。
あと、どうでもいいことだけど、PostScript言語はオブジェクト指向言語ではないと思ってますが、+
では数値演算できません。
Z80のアセンブラなんかもレジスタに1を足すような場合、add
と書かないといけません。
それが面倒でa += 1
と書けるように作られたのがC言語です、ターゲットはZ80じゃないけど。
逆に、PostScript言語でadd
をオーバーロードして、元のオペレーターの動作はそのままに、独自に複素数対応させることだってできちゃう(やり方はJavaScriptと似てます)。
このあたりの話も本来はオブジェクト指向とは関係ない。
結局、全ては言語仕様設計者の思想から来るのであって、設計の参考にした言語がたまたまこういう書き方をしていただけ、という話。 気に入らなければ大々的に「static言語」なるものを設計すればいいし、自分で実装できないのなら実装は他の人がやればいい。 もっとも、このあたりのことがきちんと分かっている人は実装もできるわけで、どう実装すればいいか分からないという人はやはり本質が分かっていないのだと思う。
ちょっとObjective PostScriptでも設計してみようかしら、なんて思った今日この頃。
初稿: 09 Aug 2024
Copyright (C) 2024 akamoz.jp