第40回
プログラミングの周辺事項(3)~適切なコメントとは?

必要なことだけを簡潔に

先述したように、ソースコードを読んでわかることをわざわざコメントにする必要はありません。一目瞭然のことをコメントに書くと、本質的な部分が文章に埋もれて読み落とされることがあります。

多すぎず少なすぎず……

コメントの基本は
多すぎず かつ 少なすぎず さらに ツボを押さえて
書くことです。

たとえば変数の宣言で、それがint型であることは説明するまでもありませんが、ではなぜ整数型としたのかは説明の必要があるかもしれません。

先のfor文のように『なぜ変数num-1の数だけ繰り返すのか?』など、他者(未来の自分も含めて)が読んだときに、ソースコードの前後を見返さないと判別できないようなところは、前後を読み直さなくても理解できる程度に補足するのが理想です。

他者に対する想像力

技術者の中には、『他者の気持ちを想像する』ことが苦手だという人が少なくありません。ユーザーの感覚や気持ちに立ってユーザーインターフェイスを考える場合と同じで、理論に基づいて物事を組み立てる人は、理論に基づいていれば誰にでも理解できると考えがちです。

たとえば、あなたが飼い猫(または犬でもハムスターでも)の写真を撮ってブログにアップしたとします。あなたにとってそのペットは可愛くて仕方ない存在であり、当然、名前も年齢も癖も好みも知っています。しかし、ブログを見る他者はそのことを知りません。そんなときあなたは「僕の(私の)可愛がっているミケちゃんのことを知らない方が悪い」などとは思わないでしょう。

説明していない自分の責任なのですから、ブログを見る他者に対して、このペットを自分がどれだけ可愛がっているかを文章で説明するはずです。コメントも、それと似たところがあります。『自分がなぜこの処理に対してこのようなソースコードを書いたのか」を、他者の視線に立って記述することが大切です。

想定する『他者』によって変わる

この『他者』という考え方ですが、当然のことながら『想定する他者』によってコメントの書き方は変わります。ソースコードを読む人のスキルや経験が、コメントの書き方を左右します。

将来の自分だけを想定すれば、それこそ自分が忘れてしまいそうな最低限のレベルは容易に想像できます。企業内プロジェクトのほかのメンバーを想定した場合も、自分と同程度のスキルを持っているという前提で対処すればいいでしょう。

しかし、同僚と言えども経験まで自分と同じだとは言い切れません。システム系の仕事をこなしてきた人が、事務系の仕事を担当することもあるでしょう。当然、その逆もあり得ます。そこを想定して、コメントを記述する必要があります ※2

雑誌記事や書籍の場合は、読者のレベル――初心者なのか上級者なのか――を想定して、初心者向けならあえて初歩的なところまで補足し、上級者を想定するなら初歩的なことはあえて省略して……と、考慮しなければなりません ※3

実際、システム系・科学技術系技術者と事務系技術者との齟齬(そご/解釈の食い違いや行き違い)はよくあります。事務系のプログラミングではfloatやdoubleなどの小数点数を使うことはあまりないため、丸め誤差に対する感性が異なります。逆にシステム系の人は、回転率とか利益率、損益分岐点などの整数~小数点以下2桁程度の処理にうといものです
物書きとしては、難しいところです。初心者向けの記事で丁寧なコメントを書くと上級者の読者から「説明がくどい」と言われ、上級者向けの記事で省略したコメントを書くと初心者の読者から「不親切だ」などと苦情を頂いたりします(笑)。

あとがき

サンプルのソースについて

今回のサンプルは
テキストファイルを読み込んで
タブコードを複数のスペースに
 または
複数のスペースをタブコードに
変換する
というプログラムです。

複数の入力ファイルを指定して一括変換できます。変換結果は標準出力に出力されるので、必要に応じてリダイレクトします。

リスト1:スペース/タブ変換プログラム~stconv.c
/* ------------------------------------------------------------ *
 * stconv.c     テキストファイルのスペース<-->タブ変換          *
 * 書式 : stconv option <input file> <option> <input file> ...  *
 *        option の説明                                         *
 *         -Tn : スペースを「タブコード」に変換                 *
 *               1バイトだけのスペースはタブに変換されない     *
 *         -Sn : タブコードを「スペース」に変換                 *
 *        オプションに続く'n'でタブストップの間隔を指定         *
 *        <input file> で入力ファイルを指定                     *
 * ------------------------------------------------------------ */

#include    <stdio.h>
#include	<stdlib.h>
#include    <jctype.h>    /* 日本語2バイトコードの処理用 */

#define     DATMAX          8        /* オプションの最大数 */
#define     ARGMIN          2        /* 引数の最小値 */
                                     /* これ以下はエラーとする */
#define     BUFSIZE         1024     /* ★ 文字列処理バッファのバイト数 */
#define     DEFAULT_TAB     8        /* ★ 標準のタブストップ */
#define     MIN_TAB         2        /* ★ 最小のタブストップ */
#define     MAX_TAB         16       /* ★ 最大のタブストップ */
#define     SPACE           0x20     /* ★ スペースコード */
#define     TAB             0x09     /* ★ タブコード */
#define     _FF             0x0c     /* ★ 改ページコード */
#define     TRUE            1        /* 論理値--真 */
#define     FALSE           0        /* 論理値--偽 */

/* 構造体 STCONV を定義 */
typedef struct {
          int     sw;     /* 0:TAB->SPACE 1:SPACE->TAB */
          int     tab;    /* タブストップ幅 */
          char    *fname; /* 入力ファイル名 */
          FILE    *fp;    /* ★ ファイルポインタ */
        } STCONV;

/* ----------------------------------------------------------- *
 * putusage : 使用方法を表示                                   *
 * ----------------------------------------------------------- */
void    putusage(void)
{
    puts("STCONV : タブ/スペース変換フィルタ");
    puts("¥x7¥tSTCONV option <input file> <option> <input file> ...");
    puts("¥t¥t---- <option> の説明 ----");
    puts("¥t-Tn   : スペースを「タブコード」に変換します.");
    puts("¥t        1バイトだけのスペースはタブに変換されません.");
    puts("¥t-Sn   : タブコードを「スペース」に変換します.");
    puts("¥tオプション文字に続く n でタブストップの間隔を指定します.");
    puts("¥t<input file> で入力ファイルを指定します.");
    puts("¥t入力ファイルに<option>を指定しない場合は,それ以前に指定された");
    puts("¥tオプションを引き継ぎます.");
    puts("¥t出力は標準出力になっているので,リダイレクトしてください.");
}

/* ----------------------------------------------------------- *
 * setdata : 構造体のメンバをセット                            *
 *   構造体のメンバtabの値をチェックし、妥当な値をセットする   *
 *   fnameを下にファイルをオープンし、ファイルポインタをfpに   *
 *   セットする--エラーならFALSEを返す                         *
 * ----------------------------------------------------------- */
int     setdata(STCONV *p)
{
    /* タブストップの調整 */
    if (p->tab < MIN_TAB)
        p->tab = DEFAULT_TAB;
    else if (p->tab > MAX_TAB)
        p->tab = MAX_TAB;

    /* ★ ファイルのオープンとエラー処理 */
    if ((p->fp = fopen(p->fname, "r")) == NULL) {
        fprintf(stderr, "%s がオープンできません.¥n", p->fname);
        return (FALSE);
    }
    return (TRUE);
}

/* ----------------------------------------------------------- *
 * tb2sp : タブ->スペース 変換                                 *
 *   src  : 送り側文字列   ★                                  *
 *   dest : 受け側文字列   ★                                  *
 *   tab  : タブストップ数 ★                                  *
 * ----------------------------------------------------------- */
void    tb2sp(char *src, char *dest, int tab)
{
    int     i, j, step;

    /* ★ 送り側文字列の終端まで繰り返す */
    for (i = 0; *src != '¥0'; i++) {
        /* タブコードが見付かったらスペースに変換 */
        if (*src == TAB) {
            /* 2バイト日本語への対処 */
            if ((i > 0) && (iskanji(*(src-1))))
                    *dest++ = *src++;
            else {
                /* タブコードをスペースに置き換える */
                step = tab - (i % tab);
                for (j = 0; j < step; j++)
                    *dest++ = SPACE;
                src++;
                i += (j - 1);
            }
        }
        else
            *dest++ = *src++;
    }
    *dest = '¥0';
}

/* ----------------------------------------------------------- *
 * sp2tb : スペース->タブ 変換                                 *
 *   src  : 送り側文字列   ★                                  *
 *   dest : 受け側文字列   ★                                  *
 *   tab  : タブストップ数 ★                                  *
 * ----------------------------------------------------------- */
void    sp2tb(char *src, char *dest, int tab)
{
    char    *pos;
    int     i, j;
    int     step;

    /* ★ 送り側文字列の終端まで繰り返す */
    for (i = 0; *src != '¥0'; i++) {
            step = tab - (i % tab);
        /* スペースが見付かったらタブコードに変換変換 */
        if (*src == SPACE)  {
            /* 2バイト日本語への対処 */
            if (((i > 0) && (iskanji(*(src-1)))) ||¥
                                (*(src+1) != SPACE))
                    *dest++ = *src++;
            else {
                /* スペースをタブコードに置き換える */
                pos = src;
                for (j = 0; j < step; j++, pos++)
                    if (*pos != SPACE)
                        break;
                if (j == step) {
                    *dest++ = TAB;
                    src = pos;
                    i += (j - 1);
                    /* iはfor文の中でインクリメント */
            /* されているため、このときだけ */
                }   /*  -1 しておく */
                else
                    *dest++ = *src++;
            }
        }
        else {
            *dest++ = *src++;
        }
    }
    *dest = '¥0';
}

/* ----------------------------------------------------------- *
 * convinit : 構造体メンバの初期化                             *
 * ----------------------------------------------------------- */
void    convinit(STCONV *p)
{
    p->sw = 0;
    p->tab = DEFAULT_TAB;
    p->fname = NULL;
    p->fp = NULL;
}

/* ----------------------------------------------------------- *
 * chk_opt : オプションを解析して構造体にデータをセット        *
 *   p      : 構造体                                           *
 *   argv   : コマンドライン文字列                             *
 * ----------------------------------------------------------- */
int     chk_opt(STCONV *p, char *argv[])
{
    int     i, sw, tabsize;
    char    * arg_p;

    /* 解析をDATMAX回繰り返す */
    for (i = 0; (i < DATMAX) && (*(++argv) != NULL); ) {
        arg_p = *argv;
        /* オプション文字列を処理 */
        if (*arg_p == '-') {
            arg_p++;
            switch (*arg_p) {
                case 't' :
                case 'T' :  /* SPACE -> TAB */
                    sw = 1;
                    break;
                case 's' :
                case 'S' :  /* TAB -> SPACE */
                    sw = 0;
                    break;
                default  :
                    fprintf(stderr, "-%c は無効なオプションです.¥n", *arg_p);
            }
            arg_p++;
            tabsize = atoi(arg_p);
        }
        else {
            p->sw = sw;
            p->tab = tabsize;
            p->fname = arg_p;   /* ファイル名をコピー */
            p++;
            i++;
        }
    }
    return (i);		/* 有効な構造体数を返す */
}

/* ----------------------------------------------------------- *
 * stconv : ファイルの読み込みと書き出し                       *
 *   p      : 構造体                                           *
 * ----------------------------------------------------------- */
void    stconv(STCONV *p)
{
    char    rbuf[BUFSIZE + 1];
    char    wbuf[BUFSIZE + 1];

    /* リダイレクト時でもディスプレイに
       出力されるよう stderr に送る */
    fprintf(stderr, "%s¥n", p->fname);
    fprintf(stderr, "¥tタブストップ間隔 : %d / ", p->tab);

    if (p->sw == 0) {
        fprintf(stderr, "タブ → スペース¥n");
        while (fgets(rbuf, BUFSIZE, p->fp) != NULL) {
            tb2sp(rbuf, wbuf, p->tab);
            fputs(wbuf, stdout);
        }
    }
    else {
        fprintf(stderr, "スペース → タブ¥n");
        while (fgets(rbuf, BUFSIZE, p->fp) != NULL) {
            sp2tb(rbuf, wbuf, p->tab);
            fputs(wbuf, stdout);
        }
    }
}

/*------------------------------------------------------------ *
 *                                                             *
 *                           main                              *
 *                                                             *
 *------------------------------------------------------------ */
void    main(int argc, char *argv[])
{
    int     i, count;
    STCONV  dat[DATMAX], *p;

    if (argc < ARGMIN) {
        putusage();
        exit(1);
    }
    p = dat;
    for (i = 0; i < DATMAX; i++)
        convinit(p);    /* 構造体の初期化 */

    /* コマンドラインを解析し、info[]へオプションと
       ファイル名配列のポインタをセットする */
    if ((count = chk_opt(p, argv)) == 0) {
        fprintf(stderr,"出力するファイルがありませんでした.¥n");
        exit(1);
    }
    for (i = 0; i < count; i++, p++) {
        if (setdata(p) == FALSE)
            continue;
        stconv(p);
        putchar(_FF);   /* プリンタ出力のためにページ送りする */
    }
}
/* ---- end of file ---- */

あくまでコメントの書き方に対するサンプルなので、ソースの細部に関する説明は割愛します。LSI-Cでコンパイルできるように書かれており、MS-DOSまたはコマンドプロンプトで動作します。

LinuxなどのUNIX系OSでGCCを使ってコンパイルすることもできますが、文字コードの関係で一部修正が必要です。が、これは今回のテーマとは異なるため、説明を割愛させていただきます。

なお、サンプルのソース中、★マークの付いたコメントは初心者向けのもので、Cを理解している人に対しては不要です。それらも含め、コメントを書く際の参考にしてください。