メンバページ: Go

プログラムイディオム  in Java and C#

Program Idiom in Java and C#


このページは、プログラムイディオムの話を掲載していきます。


- 更新記録 -
1日目 イディオムとは
2日目 イディオムの分類
3日目 定数イディオム

1日目 ---イディオムとは

イディオムとは日本語で「慣用句」という意味です。この意味ではソースプログラムにしばしば出現するコードのパターンをイディオムと呼びます。つまり慣用的に使われるコードです。

文章に出てくる慣用句

一般の文章に出てくる慣用句には、少し「見栄」的な要素もあるかも知れませんが、慣用句を使っても「違和感」のない文章になっているはずです。

慣用句を使うことによって、概念を共有することができ、筆者から読者への意味伝達を効率よくする働きがあります。

慣用句は、歴史的文化的積み重ねの結果、作られたものです。

慣用句の例を見ていきましょう。例えば、「閑話休題」と書けば、今までは本題でない横道や余計なことを書いていて、ここから本題へ戻るということを示しています。(それにしては「閑話開始」という慣用句はありませんが。)

さて、閑話休題することにしましょう。(これは間違った慣用句の使い方です。)

イディオムの例

一番小さいイディオムは i です。
これだけでは何のことか分からないと思いますが、 for (int i = 0; i < size; i++) のように、 繰り返し文で使う制御変数は i になります。

ここでは i だけのようなイディオムは扱いませんが、一つの例です。

別のイディオムの例としては、アサーションがあります。

テストに関するイディオムとして、事前条件、事後条件などをアサーションとして、関数の入口、出口に作成するというものがあります。 アサーションの中に書いた式が false になるとき(アサーションが失敗するとき=エラーのとき)は、エラーメッセージを表示することによって、エラー解析が効率よくできるものです。

Java や C# などのようにアサーション文がプログラミング言語にあるものはそれを使います。 ないときも if 文を使って手作りでアサーションを入れるようにします。 例えば、C ではマクロ _assert などがあり、それらを使って実装します。 例外処理の try-catch-finally もイディオムです。

上記に挙げた for (int i = 0; i < size; i++) もイディオムです。 スタッククイックソートもイディオムです。

フィールド変数(メンバ変数、スロット変数)を直接アクセスさせるのではなく、セッタ関数、ゲッタ関数を定義するのもイディオムになります。

C++ でコピー操作を禁止するために private にしてコピー関数を定義しないこともコードが(ほとんど)ないですがイディオムです。 Visual Basic で循環参照を削除するためのコードも残念ながら負のイディオムです。

今までに挙げた例は、それほど目新しいものではありません。当たり前のコードです。 これは、これらがイディオムと言われる所以です。

しかし、これからは「多少目新しい」イディオムを扱うようにします。 但し、目新しいものだけがイディオムであるということではないことに注意してください。

他の概念との関係

イディオムと他の概念との関係を見ていくことにします。

アルゴリズムとデータ構造

アルゴリズムやデータ構造はイディオムと密接に関係しています。 前者は概念中心であるのに対し、イディオムは概念をベースにした実装コードであるという点で、 裏表の関係にあるといえます。

例えば、スタックを考えます。スタックを配列とスタックポインタで実装することを考えます。 このとき、push, pop 操作がスタックポインタのインクリメント・デクリメントを使って実装することがイディオムに当たります。

もちろん、Java や C# にはスタッククラスがありますので、配列を使って手作りする必要はありません。

クイックソートのような真ん中を選んで O(nlogn) で実装するのもイディオムになります。

このアルゴリズムとデータ構造をベースにしたイディオムは「エレガント」な部類に入るでしょう。

Tips

プログラミングテクニックのうち、「小技 Tip」を集めた「小技集 Tips」のいくつかはイディオムです。

ここで「いくつか」と書いたのは、Tips の多くは、特定の関数の API の説明であることが多く、 イディオムというよりも、対象とする関数のサンプルコードになっています。

しかし、Tips の中のいくつかはイディオムになっています。ここでも Tips として本で紹介されている中からいくつかのものを取り上げていきます。

プログラムパターン

プログラムパターンとイディオムの違いを明確化することは難しいです。それぞれの詳細な定義に依存します。等しいものとして扱う場合さえもあります。

ここでも両者の区別をつけずに書いていくことにします。但しイメージとしてはイディオムが実際にしばしば出現するコードであるのに対して、プログラムパターンはテンプレート的なイメージです。 (このように書いても実際の差を分かるのは難しいでしょう。)

サンプルコード

やや大きいサンプルコードに現れるコードには、イディオムが現れることが多いでしょう。 しかし、サンプルコードは関数の API の使用例として使われるときはイディオムでないでしょう。

機能中心の逆引き系のサンプルコードは機能によってはイディオムであることがあります。 さらにサンプルコードは「きれいに」「分かりやすく」書くようにしていますので、各種イディオムの例にもなっていることが多いでしょう。

ライブラリ

プログラミング言語によっては、イディオムを、言語仕様やライブラリに入れているのもあります。

この意味では、その言語仕様やライブラリを使うことがイディオムであると言えます。 最初の例に挙げたアサーションや例外処理などがそうです。 またスタックやクイックソートもライブラリとして存在しています。

プログラミング書法

イディオムと直接は関係ありませんが、イディオムの根拠の一つになるものが書法です。 この意味ではプログラミングスタイルやコーディングガイドラインとも同じです。

例えば、1関数1機能というガイドラインがあれば、イディオムも当然そうなっているべきです。

イディオムの定義

それでは今日の最後にイディオムを定義します。

  1. イディオムは実際のプログラムコードにしばしば出現するコード
  2. イディオムは使用目的が明確であり、使いまわすことにメリットがあるコード
  3. イディオムは多くの人が多くの場面で多く使うコード
  4. イディオムは大きくはないコード
  5. イディオムは歴史的・文化的積み重ねで作られてきたコード
  6. イディオムは言語に依存するものと汎用的なものがある

イディオムがどのようなものであるか、どのように定義しているか、おぼろげながら、分かってもらえたでしょうか。

明日は、イディオムを分類していくことにします。

このページトップへ

2日目 ---イディオムの分類

今日は、イディオムの分類をすることにします。

言語共通のイディオム(共通語)と言語依存のイディオム(方言)

まずはプログラミング言語に依存するイディオムと依存しないイディオムがあります。 自然言語と違って、異なるプログラミング言語でも元々は計算機で実行させるための言語ですので、 共通的なイディオムは数多くあります。これを「共通語」のイディオムと呼びます。 一方、言語に依存するイディオムは、イディオムの「方言」と呼ばれています。

しかし多くあるイディオム系の本では、その両者をあまり明確に区別していません。 つまりは「本の題名に書いてあるプログラミング言語で動作することのみが保証されている」だけです。 このため、この手の本を読むときには、そのイディオムが共通語なのか方言なのかを意識して読む必要があります。

例えば、以下の 「n 回の繰り返し」のイディオムを見ていきましょう。

(1) for (int i = 0; i < n; i++) ...
(2) for (int i = 0; i < n; ) ... i++ ...
(3) int i = 0; while (i < n) ... i++ ...

これらの繰り返しのイディオムは、どれも共通語のイディオムのように見えます。 事実、(少し修正が必要なのもありますが) C, C++, C#, Java などの C 系言語では共通に使えるものです。 しかし、Basicや Lisp, Fortran のように n 回繰り返しの For-Next, dotimes Do 文がある言語では、 上記のものは、言語仕様に存在するためにイディオムではありません。 もう少し控えめに言っても、またはイディオムと呼ぶには憚(はばか)れるもので、原始的なものに見えます。

また iterate パターンを仕様で最初から持っている言語では、オブジェクト指向的にどのコンテナオブジェクトの 何の指標インデックスで繰り返すのかを記述して実装するのがイディオムになります。

このように言語共通のイディオムというのは、厳密に定義すると面倒になりますので、ここでは 「C系言語で共通的に使えるもの」を共通語とし、Java 特有、C# 特有などのものを方言とします。この定義によって、上記の n 回繰り返しのイディオムは共通語に分類されるものになります。

対象別イディオムの分類

次の分類としては、イディオムの対象別に分類していきます。

  1. 基本的なプログラムのイディオム
  2. 名前に関するイディオム
  3. 入出力に関するイディオム
  4. GUI に関するイディオム
  5. 文字列に関するイディオム
  6. コンテナオブジェクトに関するイディオム
  7. その他のデータ系ライブラリ使用に関するイディオム
  8. オブジェクト生成・消滅に関するイディオム
  9. オブジェクトの操作(読み書き)に関するイディオム
  10. 関数呼び出しや引数渡しに関するイディオム
  11. 繰り返しや分岐に関するイディオム
  12. その他のプログラム構造に関するイディオム
  13. 例外処理イディオム
  14. セキュリティ・イディオム
  15. 分散処理に関するイディオム
  16. その他

上記のように多岐に分類できます。上記は一次元で分類していますが、大分類・中分類・小分類などまたはクロスで分類する必要がありでしょう。

よく本にある 「1000 個の Tip を紹介」などとあるようにすべてのイディオムを紹介すると莫大な量になり、その分類も複雑なものになります。
このため、適当な量の分類数になるようにします。ここでは、小分類では上記のその他を除いたものに分類するようにします。

目的別イディオム

次はイディオムの目的によって、分類します。最初にも書きましたが、イディオムは目的を持っていて、かつよく使われるプログラムコードであると定義しています。ここではその目的によって、分類していくことにします。

  1. 読解性
    見やすさのためのイディオムです。このイディオムを使えば、万人がその使用方法が明らかに分かるというものです。多くのイディオムはこの読解性の目的を合わせ持っています。
    例えば、インデントは直接的なイディオムではありませんが、読解性イディオムに付随する重要な属性になります。またネーミングに関するイディオムはこの読解性のためのイディオムと言えます。
  2. 実行効率
    実行速度の高速化やメモリフットプリントを小さくするためのイディオムです。速度面では全体性能と反応時間などがあります。メモリフットプリントでは全体のメモリフットプリントや GC を考慮したメモリ使用などがあります。
    例えば、繰り返しの終了条件判定を size() のようなメモリアクセスと関数呼び出しのコストが掛かるものを使うのではなく、静的なものにするなどがあります。
  3. 変更容易性(拡張性、可搬性など)
    将来の変更または現在未定の部分を抽象化して、その部分の変更を容易にします。これから拡張することが容易になり、また他へ持っていくことも容易になります。
    例えば、インタフェースの使用はそのためのイディオムです。
  4. 開発効率(慣用句・共通語)
    イディオムで定型化(共通化)することにより、開発効率を上げる効果があります。これは主目的というよりもイディオムの持つ本来的な性質のようなものになります。ネーミング規則はこの目的になります。
    例えば、n 回繰り返しのイディオムは、毎回同じものを使うなどを行うことにより、開発効率を向上させ、また全体で統一されていることにより、慣用句として読みやすくなります。
  5. ハックコード
    究極のイディオムです。実行効率を極限まで追い詰めたイディオムです。見やすさや拡張性は無視して、兎に角、実行効率がいいものです。
    例えば、2レジスタスワップなどがあります。

イディオムの目的が共通の用語としての慣用句だけの効果であってさえも有効な場合が多くあります。逆にイディオムを使わないプログラムは正しくても「違和感」のあるプログラムになります。

大きさ別イディオム

デザインパターンの対象の大きさは、比較的小さなものから存在しています。このため、イディオムと同じようなものがある場合があります。仕様(設計)と実装のような関係のものもあります。このようにサイズによって、デザインパターンさらにアーキテクチャパターンとぶつかる可能性があります。

  1. 小規模なイディオム
    最初にイディオムを定義したように、小さなプログラム断片であるものがイディオムとしています。つまり、この小規模なイディオムがイディオムの大半を占めています。
    行数で10行以内のものがこの中に入ります。
    他のパターン、例えば、デザインパターンが複雑になって、その結果、大規模なパターンとなっても有用であるのは、パターンを抽象化することで大きくなっても可搬性が維持できることからです。一方、プログラムイディオムは大規模になると抽象化が困難で可搬性が落ちることになり、大規模なイディオムは事実上困難になります。
  2. 中規模なイディオム
    10行を越え、1画面に収まる程度のものが中規模なイディオムになります。この程度の大きさは、やや複雑な API を利用する「サンプルプログラム」となる程度のものです。
    この規模のイディオムの中には、小規模なイディオムが複数存在していることが多くあります。
  3. 大規模なイディオム
    1画面を越えるプログラム断片は、大規模な分類に入るイディオムです。これがイディオムであるためには抽象化が重要なキーワードになります。
    例えば、コンパイラのテンプレートとなるようなものが大規模なイディオムとなります。
    但し、リファレンスプログラムのような役目を持った実装例としてプログラムをイディオムに分類するのは、ここでは外すことにします。リファレンスプログラムは、一般的にきれいではありますが抽象化されていない場合が多く、再利用ということには向いていません。

今日はイディオムの分類について書いてきました。今までに出現したイディオムが、どの対象分野でどういう目的があって、それが方言なのか共通語なのかを見ていくとおもしろいでしょう。これを理解してイディオムを使っていくようにしてください。

このページトップへ

3日目 ---定数イディオム

今日は、最初のイディオムとして、定数を見ていきます。定数の表現は各プログラミング言語でイディオムとして確立されています。例えば、Java では final で表現し、C#, C++ では const, C ではマクロ、Lisp では defconst などで表現します。

定数イディオム

定数のイディオムを使わないで、書くと以下のようになります。

int getSeconds(int days) { 
  return days * 24 * 60 * 60; 
}

これだけでは 24や60の意味が不明で、これが複数の箇所にあるときに変更(*)が生じたときに、すべての箇所を変更しなくてはいけません。これは、プログラミングスタイルとしても、「ハードコーディングをするな」という教えに反します。

(*) 地球上ではない別の星で、このプログラムを動作させるときに変更する必要が出てきます。

次にこれを定数のイディオムを使って書いてみましょう。ここでは Java で書いてみます。

/** 1日当たりの時間 */
public final static int HOUR = 24;
/** 1時間当たりの分 */   
public final static int MINUTE = 60; 
/** 1分当たりの秒 */
public final static int SECOND = 60; 

int getSeconds(int days) {
  return days * HOUR * MINITUE * SECOND;
}

これで 24 には HOUR という名前が付いたことにより、読解性が少し向上しました。また、別の星で実行させるときに、HOUR の値に変更が生じても1ヶ所の変更で済むようになります。

さらにいいことには、final はコンパイル時に、価型と文字列型のときは、実際の値と置き換えられます。このため、余分なメモリアクセスが発生しません。それらをハードコーディングしたときと同等な実行効率が得られます。

しかし、このイディオムには、注意することがあります。定数の変更を行ったときの、再コンパイル問題です。価型と文字列型は置き換えになりますので、変更を行ったときは再コンパイルを必要とします。もし public なアクセス属性であれば、すべてのパッケージのファイルを再コンパイルしなくてはいけないかも知れません。文字列型以外の参照型は置き換えをしませんので、再コンパイルの必要はありません。

定数項イディオム

定数項とは、すべての項が定数である式または関数のことです。12 * 24 * 36 や foo(1, 2)、MONTH * DAY (但し、MONTH と DAY は定数)は、定数項になります。

定数項イディオムとは、「定数項は事前計算をせずにそのままにする」というイディオムです。このままにすることにより、プログラムの読解性は高いです。

事前計算はコンパイラがコンパイル時に行い、実行時には行いません。このため、実行効率は悪くありません。

public final static int SECOND_FOR_DAY = 86400 // 24 * 60 * 60

int getSeconds(int days) {
  return days * SECOND_FOR_DAY; 
}

上記のように事前に計算するのではなく、前述したように days * HOUR * MINITUE * SECOND とします。

コンパイルディレクティブとしての定数イディオム

条件付コンパイルをするときにも、そのディレクティブとして、定数を使います。

例えば、定数 debug をコンパイルディレクティブとして使います。

public final static boolean debug;

int program() {
  if (debug) Log.debug("デバッグ中");
}

このプログラムは debug が true のときにのみ、Log.debug("デバッグ中"); がコンパイルされ、実行されます。 debug が false のときは、if (false) Log.debug("デバッグ中"); の条件文が定数で false になりますので、 (真面目なコンパイラであれば)コンパイルされません。

これは Log4j の isDEbugEnabled() メソッドを使う場合よりも効率のいいものになります。 また、ロギングレベルの切り替えによるログ出力制御よりも、コストは小さくなります。 このために、Log4j を使うときにも、このコンパイルディレクティブとしての定数の使用は、定数イディオムとなっています。

今回の例は boolean でしたが、他の価型や文字列型の場合でも同様なことができます。

C のマクロによる定数イディオム

Java などでは使用できませんが、C などではマクロによって、定数を表現するイディオムがあります。目的や利点は、ここの定数イディオムとして述べたものと同じですが、これは良くないイディオムの一つです。

C言語プログラムの構文とマクロ構文は、文化が異なる独立した構文です。つまり、2種類の構文規則があり、それを一つのプログラムで、ごちゃまぜになっている状態です。これは読みやすいものではありません。例えば、HTML の中に JavaScript が突然現れるぐらい見にくいものです。

一方、ここで紹介した定数イディオムはその言語の完全な一部として、表記します。そのため、一つの構文規則に則って統一された読みやすいプログラムになります。C でもこのような定数イディオムを使った方がいいでしょう。

きょうのまとめ

定数イディオムの利点

  1. ネーミングによる読解性の向上
  2. 価が1ヶ所になることによる変更の容易性
  3. 置き換えによる実行効率の良さ
  4. 定数項はそのまま(定数項はコンパイル時に計算されるので高速)

定数イディオムの注意点

  1. 値型と文字列型の場合は、変更に対して、再コンパイルが必要
  2. その他の参照型のときは、再コンパイルは不要

コンパイルディレクティブとしての定数イディオム

  1. 条件付きコンパイルを行うためのイディオム
  2. Log4j などと組み合わせて使う

C のマクロによる定数イディオム

  1. C のマクロは使用しない
  2. 変数を使った定数イディオムを使う

このページトップへ

Copyright (c) 2005-2006 OKI Software Co., Ltd.