「Nesting “If Statements” Is Bad. Do This Instead.」の「This」と、そもそも

Youtube 検索に紛れ込むノイズの一つだったのよねこれ。

なんとなくここ数日で書いてた「Youtube 検索に入ってくるノイズが困る」話をしてた流れでたまたま目に留まった「プログラミング初心者向け」と思われるコンテンツに反応してみる、の巻。いやさ、つまりこうやって反応したくなっちゃう、てことを指してノイズ言うとるわけよな。

「目に留まった」そのものはこれね:

同じ主張をしてるビデオは、改めてこれがヒットする検索を試みてみると、結構見つかる。このビデオが言う「This」は「Guard Clause」な。(typo かわからんがビデオ中盤で「Guard Cause」としとるが、間違いだろうねたぶん。)

一応言っとくと、ネガティブなツッコミをしたくてこれを取り上げたんではない。まぁ昔から良く言われてきたノウハウについて言ってるよ、これ。

まず「This」がビデオの主張と違う変種についてちょっと紹介しておきたい。この話が理解できるかどうかで、ワタシが言いたいことがわかる。

bad.c
 1 int func(const char* s)
 2 {
 3     char* sd = create_honyarara(s);
 4     /* ... */
 5     int res1 = proc1();
 6     if (res1) {
 7         int res2 = proc2();
 8         if (res2) {
 9             int res3 = proc3();
10             if (res3) {
11                /* ... */
12             } else {
13                log("error");
14                destroy_honyarara(sd);
15                return -1;
16             }
17         } else {
18            log("error");
19            destroy_honyarara(sd);
20            return -1;
21         }
22     } else {
23        log("error");
24        destroy_honyarara(sd);
25        return -1;
26     }
27     return 0;
28 }
good.c
 1 int func(const char* s)
 2 {
 3     char* sd = create_honyarara(s);
 4     /* ... */
 5     int res1 = proc1();
 6     if (res1) {
 7         int res2 = proc2();
 8         if (res2) {
 9             int res3 = proc3();
10             if (res3) {
11                /* ... */
12             } else {
13                goto err;
14             }
15         } else {
16            goto err;
17         }
18     } else {
19         goto err;
20     }
21     return 0;
22 err:
23     log("error");
24     destroy_honyarara(sd);
25     return -1;
26 }

まずこれ「Nesting “If Statements” Is Bad. 」に答えてないが、ひとまずそれは一旦忘れてほしい。ここで注目すべきなのは「複雑(でしかも忘れやすい)な終了処理を一か所にまとめる」ことについての是非である。そのためには「goto だって有用」という話になる。これは本当に C 言語文化だと良く引き合いに出される例。「goto は悪」という脊髄反射に対するカウンターとしても良く出てくる。C++ ではデストラクタのふるまいと相性が悪いのでこれはバッドノウハウではあるけれど、元の C 言語の場合は「エラー側分岐処理を都度複雑に書くくらいなら goto で共通処理に飛ばせ」はむしろ時として推奨すらされる、というのは、覚えておいてほしい。

のうえで、改めて good.c を「さらに good に」:

good2.c
 1 int func(const char* s)
 2 {
 3     char* sd = create_honyarara(s);
 4     /* ... */
 5     int res1 = proc1();
 6     if (!res1) {
 7         goto err;
 8     }
 9     int res2 = proc1();
10     if (!res2) {
11         goto err;
12     }
13     int res3 = proc1();
14     if (!res3) {
15         goto err;
16     }
17     return 0;
18 err:
19     log("error");
20     destroy_honyarara(sd);
21     return -1;
22 }

ここまでは良いかな? これも一応は「Guard Clause」のはずよ。(なお、C++ の場合はこれは例外の throw とデストラクタの二つで解決すべき問題、と考えることになる。こういうのって結局言語依存部分が結構大きいんだよ残念ながら。)

注目してほしいのが、ワタシのこの例は「Single Entry, Single Exit」パターンに原則従おうとしている、ということ。そしてこれが「Nesting “If Statements” Is Bad. Do This Instead.」の「This」と思想的に衝突していることはわかる?

「Single Entry, Single Exit」は、「後片付け」や「戻り値」が複雑なほど有用な戦略。けれどもビデオが推奨する「エラーと判明し次第即 return」原則は、時としてその保守に苦しむ原因となることがある。ワタシはよくやるよ、ほんとに。雑に始めちゃいがちなゴミツールを Python で作ってるときなんかは、タプルを返却値にするなんてことをしてよくサボるわけだけれど、その戻りの仕様を変えたいときに泣くよね。「Guard Clause」マナーそのものは本当に有用なのでワタシも多用しとる。なので一つの関数やメソッド内で何か所にも return が…あとはご察し。

本当は引用したビデオは「return せよ」と解釈させるような説明をしてるのが間違いなのよ。必ずしもいつでも即 return が御しやすいとは限らない。けれども判定を逆転させることで if のネストを避ける、というテクニックそのものは有用、というのが good.c から good2.c ね、本当はこちらだけが「Nesting “If Statements” Is Bad」の本質なのだよ。即 return とは分離して考えるべし。