0 テ / ーックが ひなた先生が教える ・まとめ くなるテクニック ループが 1 秒間に何回まわせるのかというのは特に重要で、アルゴリズムを考える時 に、 O ( n2 ) のアルゴリズムにするべきか、 0 ( n4 ) のアルゴリズムにしても良いのかなど事 前に知ることができます。 他にも「 SQL 文の実行速度は ? 」だとか、「ネットワークのレスポンスは ? 」だとか、 知っておいたほうがいいことはたくさんあります。 簡単なプログラムを書いて、こまめに実行時間を測定する習慣をつけることから始めま ー今回のまとめ ~ ー ケンイチの講義ノート しよう。 122 のはとても難しいとする。だから、バグが露呈したことは目視で確認しなけれはならない。 は画面上の文字が化けるバグて、「画面上の文字が化けているか」を判定する関数を書く プログラムを実行してからバグが露呈するまでに 1 時間くらいかかるとする。そのバグ こういうケースはどうだろう。 ー Sparce ーー Mat 「ⅸを使うロ - ギングは本当に役に立つのか ? ー 今回出てきたような「 Sparse Matrix 」の簡易的な実装が利用できる ) であるならそのための配列は「 Sparse Matrix 」だと考えることができる。 ( ならば、 ・ロギングのためバラメータを配列に書き出すとき、大半の呼ひ出しバラメータが同し 容量的に許されない場合は、メモリ上の配列等にその情報を書き出していくと良い ・ロギングするときにファイルなどに書き出すことが速度的に、もしくはストレージの ( 記録 ) することが有効なデバッグの手がかりになりうる ・デバッグのときに関数がどのようなバラメータで呼び出されているのかをロギング ・ほとんどの要素が 0 てある行列のことを「 Sparse Matrix 」と呼ふ
0 2 も第デつくなるテクニック テ / ーッグが ・ 2 回目以降の呼び出しでは関数から復帰するまでに 1 秒しかかからない このとき、 5 回呼び出された直後は、時間的に見ると真ん中ではないのです」 「じゃあ、 5 回目呼び出された直後にブレークさせて調べるのは間違っているという ひなた先生が教える一 0 0 0 こと ? 」 「いえ。そうは思いません。リスト 1 のプログラムの場合、時間的な中間地点で、デバ ッガによって強制的にブレークさせれば hoge ー function の 1 回目の初期化中に止ま るでしよう。そこで止まったところで正当性のチェックができないかもしれません」 「どうして ? 」 「 hoge ー function の初期化中に停止するということは、まだ hoge-function で使 われている変数などの初期化が終わっていないということであり、そういった不完 全な状態にある変数に対して ( その変数に対する ) 正当性のチェックを正しく行え るとは思えないからです」 「なるほど」 「そう考えるなら、むしろ時間的な中間で正確に止めて、そこで調べようとする行 為自体に無理があるのではないかと思えてきます。 コード上にブレークポイントを仕掛ける場合ならば、やはり「コード的に見て、真 ん中ぐらいだろう』という地点にブレークポイントを配置するしかないのではない ケンイチはひなた先生がうっとりと自分のことを見つめているような気がした。 「うんうん」 かと思えるのです」 呼び出し履歴の巻 0 64 という順番で、この 4 つの関数が繰り返し実行されるプログラムがあるとするね」 「ケンイチ君、いま、図 2 のように、「わんわん、にゃんこ、ゲコゲコ、にゃんこ』
テパッグが ひなた先生が教える 。 , 電 1 ー - ーー丿くなるテクニック 「 t. get く 2 > とか書かないといけないんですね。案外不便ですね」 「だろう ? そもそも、複数の出力を返したければ、引数 " から " 返せば良いんじゃな いか ? 」 「ええと、つまり、こういうことですかね ( リスト 7 ) 」 int& add ′ int& sub ′ long& mul,doubleg div i n t a ′ i n t b ′ void add_sub—mul—div マリスト 7 引数から返す例 add sub mul div a + b; a—b; (long)a*(long)b; (double)a/(double)b; 「どうして ? 」 「ああ、そうだ」 「これは、個人的には、良くないプログラムだと思うんです」 「引数というのは、本来入力があるべきところじゃないですか。そこに出力を書く だなんて、どこか間違ってると思うんです。こういう使い方で関数の出力を得るな ら、返り値はつねに void でも構わないことになってしまいます」 関数の返り値不要論の巻 「ケンイチ君は、今『返り値はつねに void でもかまわないことになる』と言った。 それはたしかにそうなのだけれども、現実問題として、次の 2 つのプログラムは大 差だ ( リスト 8 , 9 ) 」 v リスト 8 1 つの出力を引数から返す場合 vo i d Ge t I nt ( i n t & n ) { n マリスト 9 1 つの出力を返り値として返す場合 int GetInt() { return 1 冫 } 248
0 テパックが 2 らも貰 = 一 , くなるテクニック ひなた先生か教える一 0 を 「はい」 「このプログラムには、仮引数 count とローカル変数 c 、それからメンバ変数 count_ だ。この 3 つが使われている」 「ええ、そうですね」 「ケンイチ君は、ローカル変数は、どう命名していたんだ ? 」 「当時そこは、あまり深く考えてませんでしたね。現代の視点で見れば、このケー スは、仮引数名は c とだけ書くと、インテリセンスにメソッドシグネチャが出てき たときに何を意味する c なのかわからなくなってしまいそうなので、仮引数名を count とするほうが適切だと思いますが」 「だろうなあ。つまり、こういうことだな ( リスト 2 ) ? 」 マリスト 2 仮引数とローカル変数について考えるプログラム 2 void incrementCount(int count){ i nt c count + 1冫 if ( c く 100 ) count , を臼 0 「ええ、そういうことです。ただ、ローカル変数の c というのも、当時の視点として も、やや違和感がありますね」 「どういうこと ? 」 「ある程度、メソッド本体が長い行数になってくると、この手の短い変数名をロー カル変数に割り当ててしまうとバッティングすることがあると思うのです。ですの で、 tmpCount のようなバッティングしなさそうな名前のほうが好ましいかな、と 思うのですが」 「ああ。ローカル変数には、 tmp のようなものを付けるということか」 「はい。ローカル変数は結局、このメソッドを抜けると失われるものなので、この 意味において、すべてが tmp (tempora 「 y : 一時的な ) ですから、こういう命名 230
第 11 回例外処理の考え方 「そのメソッドの内部で呼び出しているすべてのメソッドの投げる例外を手作業で 調べていくということ ? 」 「はい、そうです。すべてのメソッドの投げる例外をリストアップしていきます。メ ソッド内部で catch している例外はリストには含めません。例外のリストアップが 終われば、そのリストをそのメソッドが投げうる例外としてクラスのドキュメント に書いておきます」 「気が遠くなるような作業ね ? 」 「ええ。関数 f から関数 g を呼び出していて、関数 g から h を呼び出しているとすれば、 関数 h が投げる例外が変更になれば、関数 g と関数 f の投げる例外も変更になり、そ れらを都度ドキュメントに反映する必要が出てきます」 マリスト 5 例外を使った Java のプログラム そう言うと、ケンイチは自分のノートに例外を使った Java のプログラム書いた ( リスト 5 ) 。 Exception HandIing) を採用していますよね」 「あまり Java は使ってはいないのですが、 Java では宣言型例外処理 (Decla 「 ative 「ケンイチ君は Java の例外はどうなっているか知ってる ? 」 Java vs. C # の例外処理の巻 class KO extends Oya System. out . println("Oya::func"); public void func() class Oya 209 class MyException extends Exception throw new MyException(); System. out . println("Ko::func"); public VOid func() throws MyException
第 10 回プログラミングのイディオム ( 後編 ) 「左辺は ( A Ⅱ B) と (A & & B ) の否定形ですから、これらの否定がとりたくなった ときに、この法則を使うと良いと思います」 「ケニチ君は、一体、いつ、そんな否定がとりたくなるんだ ? 」 「ええと・・・」 ケンイチは困った。否定がとりたくなる状況というのはどういう状況なのだ ? 「ケニチ君。たとえば、あるアルゴリズムを実装するのにループ構造が必要だと気 付いたとする。そのとき、まずは wh ⅱ e ( t 「 ue ) とタイプしないだろうか ? 」 「タイプすることがありますね。そのあと、ループの脱出条件を書きます。たとえ ば、リスト 1 のようなプログラムになります」 v リスト 1 ループ構造を含むプログラム w h i [ e ( t rue ) { do—somethingl(); if ( 条件 ) break; d0—something2(); 「うん。そのとき書く条件は、ループ脱出条件だ。人間の思考として、ループが破綻 するときの条件、すなわち、ループ脱出条件を考えるほうが直観的だ。しかし、 fo 「 も wh ⅱ e もそうはなっていないよな ? 」 「あっ ! そうですね ! fo 「も、 wh ⅱ e もループを " 抜けない " ための条件であって、 ループ脱出条件ではないですね ! ということは、ループ脱出条件が思い付いた場 合、それの否定形が必要になる。 たとえば、「 (A & & B) 」がループ脱出条件だとすれば、ループするための条件は、 この否定形である「 !(A & & B) 」です。ここで、ド・モルガン則が使えて、「 !A Ⅱ 旧」と単純化される。つまり、早井先輩は、そういう風にド・モルガン則を活用し ろと言いたいわけですね ? 」 ーん ! ! 誰がそんなことを言うか ! ! こら ! ! 」 「ば、、馬鹿も一 183
0 テパッグが 2 らる危うをくなるテクニック す。たとえば、次のようなコードです」 マリスト 3 ループ変数が 2 ずつ増える場合 for(int i = 0 戸 ! = 5 戸 + = 2 ) do—something(); ひなた先生か教える 「この場合、 i は O , 2 , 4 , 6 , ・・・と増加していくのですが、 5 になることはないためこの ループを抜けません。これらの意味から、『 i!=n 』という表現はあまり現実的では ないと思います」 : 「うん、そうね。ところで、このプログラムを次のように書かないのは、なぜかな ? 」 マリスト 4 1 から始める n 回繰り返し for(int i = 1 冫 i く = n + + i ) do—something( ) 冫 「これは初心者にはありそうな書き方ですけれど、実際はめったにお目にかからな いですね。まず i に 1 を代入していますが、配列の添え字など 0 0 「 igin ( O から始ま る ) であることが多いので、ループカウンタが 1 から始まると使いにくいと感じます」 と言いながら、ケンイチは、ノートに追記した。 ▽リスト 5 配列を初期化する int aCnum]; for(int i=O;i く num; + + i ) aCi] = 0 「じゃあ、このコードは ? 」 は、あまり良いコードとは呼べないと思います」 が簡単なコードで表現できることが多いので、実行効率の観点からも i = 1 というの 「それにアセンブリ言語ではレジスタ ( 変数 ) に 1 を代入するより O を代入するほう 「うんうん」 るんですよね」 して合法なのは、 O から num-l ですから、ちょうどこのスタイルの fo 「とマッチす 「こういう配列を初期化するプログラムを書くことになった場合、配列の添え字と 164
の ) し ) ! くなるテクニ ック テパッグが マ図 1 消しこむとは・・・ まず、数の表があって 2 3 4 5 6 1 0 1 1 1 2 1 3 1 4 1 5 7 8 9 2 の倍数 ( 4 , 6 , 8 , ・・・ ) を消しこむ 11 15 15 2 5 第 5 す 7 9 次に 3 の倍数 ( 6 , 9 , 12 , ・・・ ) を消しこむ 2 5 を 5 第 7 を 1•0 11 15 ひなた先生が教える 「いわゆる『エラトステネスのふるい注 1 』だな」 「ええ、そうです」 「知ってるなら最初に言わんか ! 」 「すみませんすみません」 まさか入社 10 分以内に 2 度も怒られるとは思っていなかったケンイチは心臓をぶち抜か 「まあ、いい。ケンイチ君は、このプログラムに対して、良いとか悪いとか、そう いう意見はないのかい ? 」 「素数を求めるアルゴリズムはエラトステネスのふるいを使う以外にもいろいろ方法 がありますが、ここからアルゴリズム的な変更は加えないと仮定しますと、まず気 になるのはプログラムの実行時のメモリ消費量です。ここで確保している配列が大 きすぎるので、 bo 型の配列の代わりに std : : vector<bool> を用いたいところで す。こうすれば、この配列による使用メモリはおおよそ 1 / 8 で済みます。速度はい くぶん遅くなりますが」 「うん。そうだね、ケンイチ君 ! 」 注 1 工ラトステネスのふるい 工ラトステネスは、ギリシアの博物学者。素数を見つける方法 ( 工ラトステネスの篩 ) を案出。また地球の形を球体と 考え、その全周を推定した ( 紀元前 276 ごろ ~ 195 年ごろ ) 。 12
第 8 回ケアレスミスはなぜ起きるのか ? マリスト 3 日本語による解答プログラム 3 W(x ′ y) の値とは、 ①もし a ( x ′ y ) が 1 ( 壁 ) ならば 0 ②さもなくば、飄 y ト 1 にして、周辺 4 近傍における W の値の合計 + 1 「ええと、 C で書くと ? 」 「こうなります」 マリスト 4 C による解答プログラム i n t W ( i n t x ′ i nt y ) { if (aCx]CyJ) return 0 冫 aCxJCyJ=1; return W ( x ′ y ー 1 ) + W ( x ′ y + 1 ) + W ( x ー 1 ′ y ) + W ( x + 1 ′ y ) + 1 「ケンイチ君、さすが一 ! 」 「正解ですか ? よかった。今度間違えたら立つ瀬がないところでした」 「どう ? W = 2 の場合をテストする意義がわかったかな ? 」 「確かに、 W = 2 の場合をテストすることにより、無限再帰に陥るバグを発見するこ とができました。しかしなぜ、このテストが必要だったのでしよう ? 」 「 W = 25 の場合をやれば、このバグは発見できたとも言えるけれど、 W = 25 の場合 を頭のなかでテストするのはなかなか大変よね。 だから、その作業はコンピュータに任せることになるのだけど、バグがあって無限再 帰になるということは、スタックオーバーフローが起きて実際は正しく求まらないよ ね ? そのときデバッガで追いかければ無限再帰になっていることを発見できて、バ グ自体は修正できるとは思うけれど、デバッガに頼らなければならないというのはあ る意味敗北よね ? 」 「敗北ですかあ・・・」 ケンイチはどちらかと言えばデバッガに頼る機会が多いので、「デバッガに頼ることが 敗北である」というひなた先生の言葉が新鮮だった。 巧 1
第 13 回 関数の返り値はどうあるべきか これが本当に意味のある設計なのかどうかは意見の分かれるところた。 なにもかも method chain て書くとかえってプログラムは読みづらくなる。だから method chain で書けるようにするためたけに意味もなく this を返すスタイルて書くの は馬鹿げている。何でも method chain で書けはいいというものでもない。 1 つのオプジェクトに対して連続してメソッドを呼び出す記法を用意している言語も ある。たとえば、 SmaIITaIkT は、がそれだ ( C / C + + て言うところのは Sma Ⅱ Ta ⅸ ではリを使う ) 。 window position: 100a20 extent: 520a480 冫 backcolor: Co [ 0 「 red; caption: ー Red Window' このスタイルは cascading message と呼ばれる ( 「 cascade 」とは「滝」のこと ) 。 C # 風に上のプログラムを書き直すと、 Window . SetPosition(100 ′ 20)冫 Window. SetExtent(320 ′ 480)冫 Window . SetBackCoLor(Color. Red); Window . SetCaption("Red Window"); となって、変数名が長いと少し冗長な印象は受ける。 変数名が長い場合、 C # 3.0 以降ならば、 va 「ていったんテンボラリ変数に受けるとい いかも知れない。 ( / / 変数 w の scope をこの範囲に限定するための一 0 とー var W = Window; w. SetPosition(100 ′ 20)第 w.SetExtent(320 ′ 480)冫 w. SetBackColor(Color. Red); w. SetCaption("Red Window"); 別の観点として閉包性 (closure property) というのがある。閉包性とは、入力と 出力が同し型を返すという性質のことを言う。 閉包性があると関数を組み合わせて使うことができる。たとえは C + + の STL で入力と 261