FPGA開発日記

カテゴリ別記事インデックス https://msyksphinz.github.io/github_pages , English Version https://fpgadevdiary.hatenadiary.com/

「リーダブルコード」を読む (第5章「コメントすべきことを知る」第6章「コメントは正確で簡潔に」)

諸事情で週末はネットワークをあまり使えないので、RISC-Vに関連する調査は中止。 あまりやることが無いので例によって一人読書会をしている。 今回は「リーダブルコード」。名著と呼ばれており、私も一度さっくりと読んだ。 ただし忘れていることもあるし、体系的に学びたいので読み直す。

例によってJupyter Notebookに書いてまとめをブログに張っていくことにする。

今回は第5章「コメントすべきことを知る」第6章「コメントは正確で簡潔に」までをまとめた。

まとめるといっても、本書は言いたいことを非常に簡潔に要約しており、ほぼほぼそれを写経したようなものになっているが、まあ自分で手を動かさないと分からないということで。。。

リーダブルコード ―より良いコードを書くためのシンプルで実践的なテクニック (Theory in practice)

リーダブルコード ―より良いコードを書くためのシンプルで実践的なテクニック (Theory in practice)


第5章 コメントすべきことを知る

鍵となる考え: コメントの目的は、書き手の意図を読み手に知らせることである。

  • コメントすべきでは「ない」ことを知る。
  • コードを書いているときの自分の考えを記録する。
  • 読み手の立場になって何が必要になるかを考える。

5.1 コメントすべきでは「ない」こと

価値のあるコメントと価値のないコメントがある。

新しい情報を提供するわけでもなく、読み手がコードを理解しやすくなるわけでも無いコードには価値がない。

  • 鍵となる考え: コードからすぐにわかることをコメントに書かない。「すぐに」ということが大切。コードを読むのが早いか、コメントを読むのが早いか。

コメントのためのコメントをしない

// 与えられたsubtreeに含まれるnameとdepthに合致したNodeを見つける。
Node* FindNodeInSubtree (Node *subtree, string name, int depth);

関数宣言以上の情報を与えていない。下記の方が適切。

// 与えられた'name'に合致したNodeかNULLを返す。
// もし depth <= 0ならば、'subtree'だけを調べる。
// もし depth == N ならば、'subtree'とその下のN階層まで調べる。
Node* FindNodeInSubtree (Node* subtree, string name, int depth);

ひどい名前はコメントを付けずに名前を変える。

「制限を課す(enforce limit)」などという付加的情報は関数名に入れる方が適切。 優れたコメントよりも名前の方が大切。

通常は、「補助的なコメント」(コードの読みにくさを補うコメント)が必要になることはない。 プログラマはこのことを「 優れたコード > ひどいコード + 優れたコメント 」と言っている。

5.2 自分の考えを記録する

「監督のコメンタリー」を入れる

自分の考えを記録するために、コメントにはコードに対する大切な考え方を記録しなければならない。

// このデータだとハッシュテーブルよりもバイナリツリーの方が40%速かった。
// 左右の比較よりもハッシュの計算コストの方が高いようだ。

テストケースに無駄な時間を掛けることのないようなコメントなど、あとはコードが汚い理由をコメントに残してもよい。

コードの欠陥にコメントを付ける。

改善が必要な場合、

// TODO: もっと高速なアルゴリズムを使う

コードが未完成な時は

// TODO(ダスティン): JPEG以外のフォーマットに対応する

プログラマが良く使う記法のいくつか:

  • TODO: あとで手を付ける
  • FIXME: 既知の不具合があるコード
  • HACK: あまりキレイじゃない解決策
  • XXX: 危険!大きな問題がある

定数にコメントをつける

定数を定義するときは、なぜその値を定義したのか、なぜその値を持っているのかという「背景」が存在することが多い。

NUM_THREADS = 8

これに対して、以下のようにコメントを記述すれば、値の決め方がわかる。

NUM_THREADS = 8 # 値は「>= 2 * num_processors」で十分

5.3 読み手の立場になって考える

ほかの人にコードがどのように見えるかを想像するものだ。「ほかの人」というのは、プロジェクトのことを君のように熟知していない人のことである。

質問されそうなことを想像する

他人に質問されそうな、疑問を持ちそうなところにコメントを挿入する。

struct Recorder {
    vector <float> data;
    ...
    void Clear () {
        vector<float>().swap(data);   // えっ?どうしてdata.clear()じゃないの?
    }
};

これは、以下のようにしてコメントをしておくべきだ。

// ベクタのメモリを解放する(「STL swap 技法」で検索してみよう)
vector<float>().swap(data);

はまりそうな罠を告知する

「このコードを見てびっくりすることは何だろう?どんなふうに間違えて使う可能性があるだろう」 ということを想像してコメントを書く。

void SendEmail (string to, string subject, string body);

この関数の実装では、外部のメールサービスに接続しており、メールサービスがダウンしていると、ウェブアプリケーションが「ハング」してしまう。 このような不幸を防ぐためには、「実装の詳細」についてコメントを書くべきだ。

// メールを送信する外部サービスを呼び出している(1分でタイムアウト)
void SendEmail (string to, string subject, string body);

「全体像」のコメント

新しいチームメンバにとって、最も難しいのは「全体像」の理解である。 新しくチームに参加した人がいるとする。彼女は君の隣に座っている。彼女にはコードに慣れてもらわなければならない。

ソースコードを読んだだけでは得られない情報は、高レベルのコメントに書くべき情報な場合がある。 短い適切な文章で構わない。何もないよりはマシだ。

要約コメント

低レベルのコードをうまく要約したコメント

# 顧客が自分で購入した商品を検索する
for customer_id in all_customers:
    for sale in all_sales[customer_id].sales:
        if sale.recipient == customer_id:
           ...

コメントがないと、コードを読んでいる途中で意味が分からなくなる。

要約コメントは、関数の内部にある大きな「塊」につけてもいい。

def GenerateUserReport():
    # このユーザのロックを獲得する
    ...
    # ユーザの情報をDBから読み込む
    ...
    # 情報をファイルに書き出す
    ...
    # このユーザのロックを開放する

5.4 ライターズブロックを乗り越える

ライターズブロックとは、行き詰ってしまって、文章が書けないこと。

自分の考えていることをとりあえず書き出してしまって構わない。生煮えであってもかまわない。

// ヤバい。これはリストに重複があったら面倒なことになる。

もう少し丁寧に、詳細に書く。

// 注意: このコードはリストの重複を処理できません(実装が難しいので)

コメントを書く作業は、3つの簡単な手順に分解できる。

  • 頭の中にあるコメントをとにかく書き出す。
  • コメントを読んで(どちらかといえば)改善が必要なものを見つける。
  • 改善する。

5.5 まとめ

コメントすべきでは「ない」こと。

  • コードからすぐに抽出できること。
  • ひどいコード(例えば、ひどい名前の関数)を補う「補助的なコメント」。コメントを書くべきではなく、コードを修正する。

記録すべき自分の考え:

  • なぜコードがほかのやり方ではなくこうなっているのか(「監督コメンタリー」)
  • コードの欠陥をTODO: や XXX: などの記法を使って示す。
  • 定数の値にまつわる「背景」

読み手の立場になって考える。

  • コードを読んだ人が「えっ?」と思うところを予想してコメントをつける。
  • 平均的な読み手が驚くような動作は文書化しておく。
  • ファイルやクラスには「全体像」のコメントを書く。
  • 読み手が細部にとらわれないように、コードブロックにコメントをつけて概要をまとめる。

第6章 コメントは正確で簡潔に

鍵となる考え: コメントは領域に対する情報の比率が高くなければならない

6.1 コメントを簡潔にしておく

// int はCategoryType。
// pairの最初のfloatは'score'。
// 2つめは'weight'
typedef hash_map<int, pair<float, float>> ScoreMap;

1行で説明できるはずだ。

// CategoryType -> (score, weight)
typedef hash_map<int, pair<float, float>> ScoreMap;

6.2 あいまいな代名詞を避ける

// データをキャッシュに入れる。ただし、先にそのサイズをチェックする。

「その」とはいったい何のことかわからない。「データ」かもしれないし、「キャッシュ」かもしれない。

// データをキャッシュに入れる。ただし、データのサイズをチェックする。
// データが十分に小さければ、それをキャッシュに入れる。

6.3 歯切れの悪い文章を書く

コメントを簡潔にすることと正確にすることは両立することが多い。

# これまでにクロールしたURLかどうかによって優先度を変える。

しかし、もう少し直接的な文章の方が良いかもしれない。

# これまでにクロールしていないURLの優先度を高くする。

6.4 関数の動作を正確に記述する

// このファイルに含まれる行数を返す
int CountLines(string filename) { ... }

「行数」とは何なのか、わかりにくい。改行文字(\n)を数えるものならば、そのように記述すべきである。

// このファイルに含まれる改行文字('\n')を数える。
int CountLines(string filename) { ... }

6.5 入出力のコーナーケースに実例を使う

// 'src'の先頭や末尾にある 'chars'を除去する。
String Strip(String str, String chars) { ... }

コメントからはわからないことがある。

  • charsは、除去する文字列なのか、順序のない文字集合なのか?
  • srcの末尾に複数のcharsがあったらどうなるのか?

上記の質問に答えることのできる実例を用意する。

// ...
// 実例: Strip ("abba/a/ba", "ab") は "/a/" を返す。
String Strip (String src, String chars) { ... }

もう一つ、実例を使った関数を紹介する。

// 'v' の「要素 < pivot」が「要素 >= pivot」の前に来るように配置し直す。
// それから、「v[i] < pivot」になる最大の'i'を返す (なければ -1 を返す)
int Partition(vector<int> *v, int pivot);

実例を使ってより詳しく説明してみる。

// ...
// 実例: Partition ([8 5 9 8 2], 8)の結果は [5 2 | 8 9 8] となり、1を返す。
int Partition(vector<int> *v, int pivot);

6.6 コードの意図を書く

void DisplayProducts (list<Product> products) {
    products.sort(CompareProductsByPrice);
    
    // listを逆順にイテレートする
    for (list<Product>::reverse_iterator it = products.rbegin(); it != products.rend(); ++it) {
        DisplayPrice(it->price);
    ...
}

どちらかというと、次のコメントの方がコードの意図を正確に表現している。

    // 値段の高い順に表示する
    for (list<Product>::reverse_iterator it = products.rbegin(); it != products.rend(); ++it) {
        DisplayPrice(it->price);

6.7 「名前付き引数」のコメント

Pythonなどでは、関数の引数を名前付きで渡すことができる。

# 名前付き引数で関数を呼び出す。
Connect (timeout = 10, use_encription = False)

C++Javaで同じようなことはできないが、インラインコメントを使えば同じような効果を得ることができる。

// 引数にコメントをつけて関数を呼び出す
Connect(/* timeout_ms = */ 10, /* use_encryption = */ false);

特にブール型の引数では、値の前に /* name = */ を置くのが大切である。値の後ろにあると紛らわしい。

// これはやってはいけない!
Connect (..., false /* use_encryption */);
Connect (..., false /* = use_encryption */);

6.8 情報密度の高い言葉を使う。

パターンやイディオムを説明するための言葉を使って、コメントを簡潔にする。

6.9 まとめ

  • 複数のものを指す可能性のある「それ」や「これ」などの代名詞を避ける
  • 関数の動作はできるだけ正確に説明する。
  • コメントに含める入出力の実例を慎重に選ぶ。
  • コードの意図は、詳細レベルではなく、高レベルで記述する。
  • よくわからない引数にはインラインコメントを使う (例: Function (/ arg = / ... )
  • 多くの意味が詰め込まれた言葉や表現を使って、コメントを簡潔に保つ。