第39回
プログラミングの周辺事項(2)~きれいでわかりやすいソースとは?

2.同じレベルの処理は字下げ位置を揃える

処理のレベル――階層の深さを揃えることは重要です。ifやfor、whileなど複数の処理を{}で囲んでまとめる構造がネスト(入れ子)になる場合、書き出し位置――字下げ(インデント)の先頭がばらついていると読み取るときに混乱します。

構造を視覚的に把握できる

字下げの位置によって処理構造の深さを明確にすれば、構造の大まかなイメージを視覚的にも把握できるようになります。このことは、ソースを他者が読む場合だけではなく、自分自身が後から読み返す場合にも役に立ちます。

『三日も経てば自分も他人』という業界の格言(?)は本当で、たとえ自分の書いたコードであっても、後日字下げ位置の揃っていないソースコードを目にすると、頭の中が爆発しそうな気がするものです。

以下のようなソースを読んでみると、一体どの処理とどの処理がレベルの深さとして釣り合っているのか/いないのか、もちろん努力すればわからないことはないのですが、そのためには非常に大きな労力を要します。

int search(const char *s, const char *t)
{
  int i;

for (i=0; i<strlen(s); i++) {
if (strncmp(s, t, strlen(t)) == 0) {
printf("¥t%s in %s¥n", t, s);
count++;
  return (i+1);
  }
    s++;
      if (strlen(t) > strlen(s)) {
        break;
  }
    }
  return (-1);
}

以下のように、1つのforと2つのifが構成する処理単位について、それぞれの書き出し位置とその終端を示す}の位置とを揃えれば、処理構造が一目瞭然となります((a)と(a')、(b)と(b')、(c)と(c')が対になります)。

int search(const char *s, const char *t)
{
  int i;

  for (i=0; i<strlen(s); i++) { -------------------- (a)
    if (strncmp(s, t, strlen(t)) == 0) { ----------- (b)
      printf("¥t%s in %s¥n", t, s);
      count++;
      return (i+1);
    } ---------------------------------------------- (b')
    s++;
    if (strlen(t) > strlen(s)) { ------------------- (c)
      break;
    } ---------------------------------------------- (c')
  } ------------------------------------------------ (a')
  return (-1);
}

if~else if~elseの構造

このような構造で最も問題となるのは、if~else if~elseの構造でしょう。以下のような書き方をすると、 { と } の間に何も書かれていなくてもうんざりしてしまいます。「そんなバカな書き方をする訳はないよ」と思う人もいるでしょう。でも、それはあなたが恵まれていたからです。

長らくプログラミングに携わっていると、こういった一見常識外れな書き方にお目にかかることがしばしばあります。十分な設計をしないまま、いきなりエディタでソースコードを書き、「後から清書すればいいや」などと思ったもののそのままになってしまった……といったことは案外あるものです。

if (...) {

   } else if {

 } else if {

} else {

  }

こういったコードは、以下のようにすればわかりやすくなります。単純に、書き出し位置を揃えるだけのことです。

実のところ、手慣れた短いソースコードなら、思い付きでいきなりエディタを起動して……ということはよくあります。そして、そのような場合、大抵はわずかな修正だけでコンパイルが成功し、動作もOK……ということになります。すると、「うまくできた」という安心感からソースコードの清書を忘れてしまうのです。

if (...) {
    :
} else if {
    :
} else if {
    :
} else {
    :
}

また、以下のような書き方もあります。

if (...) {
    :
}
else if {
    :
}
else if {
    :
}
else {
    :
}

} に続けてelseを書くか否か?

上記の両者の違いは、else ifやelseを

1つ前の終端を示す } に続いて記述するか
一旦改行してから記述するか

というところです。どちらも正しい書き方で、一方が間違っているというものではありません。ただ、1つのソースファイル中で両方の書き方を混ぜてしまうのはいただけません。

ifとelse if、elseそれぞれの処理単位を

終端の } を基準に揃えるのか
処理単位開始のelse ifやelseの命令語で揃えるのか

という書き方の基準について統一しておくべきです。

for、while、switch caseのネスト

ifのほか、forやwhileのネストやさらにその中にswitch case構文が含まれる場合などには、こういった適当な字下げが後々の解読に対して大きな壁となることがあります。

while ((buf = getstr()) != NULL) {
    :
 while ((c = *buf++) != EOF) {
      :
switch (c) {
case '.' : ...
break;
case '!' : ...
break;
case '?' : ...
break;
default  :
}
  }
}

当然ですが、処理レベルの深さに応じた字下げをすればわかりやすいソースになります。

while ((buf = getstr()) != NULL) {
    :
  while ((c = *buf++) != EOF) {
      :
    switch (c) {
      case '.' : ...
                break;
      case '!' : ...
                break;
      case '?' : ...
                break;
      default  :
    }
  }
}

こういった { と } の字下げ位置については、括弧の対応関係を太字や文字色などで示してくれる機能を持ったエディタを使えば、ある程度判断しやすくなります。

字下げはタブ?それともスペース?

多くの場合、字下げにはタブコードを使います。1回の打鍵で4または8バイト分の字下げができるので迅速に作業できますし、字下げレベルの深さも見てすぐにわかります。エディタが自動インデント機能 ※2 を持っていれば、改行するだけで直前の字下げが反映されるので効率的です。

しかし、改行したところから直前の字下げレベルの1つ上の字下げレベルに戻る場合―― { に対応する } で処理単位を締めくくった次の処理のような場合――に[Backspace]キーでタブを削除するのを忘れてそのままソースを入力してしまうと、そこから途端に字下げレベルは混乱をきたします。

僕の場合、タブで字下げするとレベルが深くなった場合にソースが80桁を超えて長くなることがあるため、1バイトのスペースを使ってスペース2個を1レベルとした字下げをしています ※3

タブに比べて打鍵数が増えるため効率は悪いのですが、字下げのレベルが深くなった場合でも全体の構造を見渡しやすくなるため、後々のメンテナンスでソースを見直すときに効力を発揮します。

改行すると、次の行の行頭に直前の行の字下げレベルが反映される仕組み
雑誌の原稿ではソースの字下げレベルを明確にするため、タブの代わりに1バイトのスペースを使っています。そのため通常のソースコードでもタブを使わなくなった……というだけのことなのですが……^^;)ゞ

改行は人間のためのもの

先にも書いたように、Cでは処理の区切りは「;」であり、ソースファイル中の改行コードはコンパイラではスペースやタブと同じ扱いとなります。つまり、ソースコードの改行は人間の都合でわかりやすい位置に挿入すればよい、ということです。

引数の多い関数の宣言行やprintf文でたくさんのメッセージを表示するような場合、1行がやたらと長くなってしまうことがあります。そのような場合には、引数名など意味の切れ目にしたがって改行を挿入し、見た目を整えるのがよいでしょう。

以下は、Windowsアプリケーションの関数宣言です。引数の型修飾子が長く引数の数も多いため、1行で記述すると読みづらくなります。

long FAR PASCAL _export WndProc(HWND hWnd, WORD iMessage, WORD wParam, LONG lParam)
/* ウィンドウハンドル, 送られてきたメッセージ, メッセージのパラメータ */
{
        :
 

以下のように引数ごとに区切って改行すれば読み取りやすくなり、引数ごとにコメントを記述できるようになります。

long FAR PASCAL _export WndProc(
                        HWND hWnd,     /* ウィンドウハンドル */
                        WORD iMessage, /* 送られてきたメッセージ */
                        WORD wParam,   /* メッセージのパラメータ */
                        LONG lParam
                        )
{
        :