プログラミングの基礎2011年5月15日 | |
はじめにこの文書は、これからプログラミングをはじめようと思っている人、あるいはある程度プログラミングを知っていて、おさらいをしたい人に向けたものです。プログラミングに関する一般的な知識を記述しています。特定の言語に依存しないように「疑似言語」で例を示しています。 プログラムとはプログラムとは、コンピュータが行う処理の手順を記述したものです。われわれが普段実行するプログラムは、アプリケーションあるいはソフトウエアと呼ばれます。アプリケーションとは、OS などの基本プログラムに対する応用 (application) のプログラムという意味です。ソフトウエアは、CPU やメモリなどのハードウエアに対する言葉ですが、普通はプログラム群といったぐらいの意味です。 コンピュータのしくみコンピュータは、ハードディスク、メモリ、CPU からなります。プログラムが実行されると、ハードディスクに保存されているプログラムがメモリにコピーされます。メモリから必要な分だけの命令が、CPU のキャッシュというところにコピーされます。CPU の演算装置は、キャッシュから命令を1 つずつ取り出して処理を実行していきます。 プログラミング言語とはプログラムは、プログラミング言語というもので記述されます。プログラミング言語で記述されたものをコードと呼び、コードを記述することをコーディングあるいは実装と呼びます。プログラミング言語には様々なものがあります。 機械語コンピュータの情報は、電気がオンかオフかの 2 値で表されます。したがって、コンピュータに対する命令は 2 進数で表現されます。2 進数で表されたコンピュータへの命令を機械語といいます。 アセンブリ言語機械語は数字の羅列なので、人間が読むのには適しません。機械語のそれぞれの命令を人間にわかりやすいように表した言語が、アセンブリ言語です。コンピュータはアセンブリ言語を直接理解しませんので、アセンブリ言語で書かれたコードはアセンブラというもので機械語に翻訳されます。 高級言語機械語やアセンブリ言語など、ハードウエアに密接に関連した言語を、低級言語あるいは低水準言語といいます。それに対し、より人間の言葉に近い形で表現された言語を、高級言語あるいは高水準言語といいます。普通「プログラミング言語」と言えば、高級言語を指します。 プログラミング言語の種類プログラミング言語は、動作の仕方、記述の仕方で数種類に分けられます。 動作の種類コンパイル型コンパイル型の言語で記述されたコードは、機械語に翻訳してから実行します。コードを機械語に翻訳することをコンパイルといい、コンパイルを行うプログラムをコンパイラといいます。コンパイルされるコードをソースコードと呼びます。 インタプリタ型インタプリタ型の言語で記述されたコードは、インタプリタというものによって実行されます。インタプリタ型言語は、コンパイルの必要がないのが利点です。欠点は、コンパイルされたプログラムよりも動作が遅いということです。 中間型コンパイラ型とインタプリタ型の中間に位置するような言語もあります。このような言語は、コードをバイトコードというものにコンパイルし、インタプリタのような仮想マシンによって実行します。テキストファイルを解釈するインタプリタ型よりも動作が速いという利点があります。動作時に必要な分だけ機械語に翻訳して実行する機能を持った仮想マシンもあります。 記述の種類命令型命令を並べて記述する言語を、命令型言語といいます。 関数型処理内容をすべて数学的な関数の形で記述する言語を、関数型言語といいます。 定義の種類静的プログラムの実行前にコード内のすべての定義が決定される言語を、静的言語といいます。普通、コンパイラ型として実装されます。制約が強い反面、プログラミング上の誤りに強いという利点があります。 動的コード内の定義がプログラム実行時に決定される言語を、動的言語といいます。普通、インタプリタ型として実装されます。静的言語に比べてプログラミング上の誤りに弱いですが、柔軟なプログラミングが可能です。 識別子コードは普通、英数文字と記号によって表されます。文字や記号で表された語を識別子といいます。コードは識別子と演算子、区切り記号によって記述されます。 識別子の大文字・小文字を区別する言語と、しない言語があります。 使用できないように予約されている識別子を、予約語といいます。予約語がない言語もあります。 コメントコードにはコメントを書くことができます。プログラミング言語は自然言語と比べて読みづらいものなので、内容の理解を助けるためにコメントを記述することは大切です。特に、ある処理が「なにをしているか」よりも「なぜそうしているのか」をコメントとして記述しておくことが大事です。 コメントは、実行されるコードと区別するために、{...} や /* ... */ で囲んだり、コメントの頭に # や % や // を置いたりします。 式コードは、データと、それを表す変数と、それらの演算を表した式で表されます。すべて式によって構成される言語と、式から作られる文によって構成される言語があります。 データは、数や文字などを表すものです。変数とは、データを入れる容器のようなものです。データと変数と、演算子という演算を表す記号の組み合わせで式が作られます。 変数への値の割り当てには記号 = や := が使われます。数の演算子には次のようなものが使われます。 + 加法 - 減法 * 乗法 /, div 除法 %, mod 剰余 ^, ** 累乗 多くの言語は、式をわれわれが普段書くように記述することができます。 y = 1 + 2*x^2 + 1/2*x^3 データ型データには型があります。データの型をデータ型といい、つぎのようなものがあります。 数整数型整数を表すための型です。 浮動小数点型実数を表すための型です。精度によって単精度、倍精度に分けられる言語もあります。 複素数型複素数を表すための型です。実部と虚部の二つの浮動小数点数の組み合わせとして表されます。 文字列型文字の集まりを表すための型です。 1 つの文字を表す文字型と、文字列型とを区別する言語もあります。文字列は "..." や '...' などで表されます。 論理型真か偽の 2 値をとる型です。ブール型、ブーリアンなどと呼ばれることもあります。真の値を true、偽の値を false などと表します。0 を 偽、それ以外の整数を真などとみなして、整数型と論理型の間に互換性をもたせている言語もあります。 列挙型言語のユーザーが決めた任意の値をとる型です。たとえば、お気に入りの果物を表す列挙型 FavoriteFruits はつぎのように表すことができます。 enumeration FavoriteFruits { apple orange melon } FavoriteFruits の変数は、値として apple, orange, melon をとります。 参照型データそのものではなく、データを入れている場所を保持する型です。場所を指し示すのでポインタと呼ばれることもあります。 変数変数にもデータ型があります。変数の型を指定する必要がある言語と、その必要がない言語があります。 変数の型を指定することを型宣言といいます。変数の名前によって型を想定する暗黙の型宣言を行える言語もあります。型宣言の場所が決まっている言語と、任意の場所で型宣言できる言語があります。 型宣言なしで変数が使用できる言語もあります。この場合は割り当てられたデータの型によって変数の型が決まります。 一度データを割り当てたら値を変更できない定数型の変数というものもあります。 データ構造データを表すための構造をデータ構造といいます。 コンテナ複数のデータをまとめて保持する容器のようなデータ構造を、コンテナと呼びます。コンテナ内のそれぞれのデータを、コンテナの要素とよびます。コンテナには配列や連結リストなどがあります。 配列配列は、同じデータ型の変数を一列に並べてまとめたものです。添え字 (インデックス) で値を参照します。このような参照のしかたを、ランダムアクセスといいます。 たとえば、整数が 10 個集まった配列を a とすると、1 番目の値は a(1)、5 番目の値は a(5)、というように参照します。添え字が 1 からではなく 0 から始まる言語もあります。 添え字を複数個持つ配列もあります。添え字を n 個持つ配列を n 次元配列といいます。 連結リスト連結リストは、セルという箱が数珠つなぎになった構造になっています。セルはデータを格納するための変数を持ちます。前のセルは後のセルの場所を知っています。前と後ろのセルがお互いの場所を知っている場合もあります。直接任意のセルに移動することはできません。必ず端のセルからたどっていかなければなりません。このような参照のしかたを、シーケンシャルアクセスといいます。 連結リストは、任意のセルの値を取り出すのに配列より時間がかかりますが、データの追加や挿入が簡単であるという利点があります。挿入したいセルを用意し、挿入したい場所の前のセルに新しいセルの場所をセットし、新しいセルに後のセルの場所をセットするだけです。配列の場合、挿入したい場所から最後までのデータを後にずらして、空いた場所に新しいデータをセットすることになります。 連想配列連想配列は、添え字に文字列など任意の型を使用できる配列です。2 つの型のデータの対応を作ることができます。 構造体構造体は型も名前も違う複数の変数をまとめたものです。たとえば、個人情報を管理するための構造体 Person は、次のように表すことができます。 structure Person { name address tel } Person 型の変数を p とすると、p に格納された人の名前は p.name、住所は p.address といったように参照します。 制御文プログラムの流れを制御する文を制御文といいます。「制御文」と表しましたが、言語によっては式かもしれません。いずれにしても仕組みは同じです。 条件文条件によって処理の流れを分岐させる文を、条件文といいます。if 文と呼ばれます。 if a > 0 then print "a is positive." else if a = 0 then print "a is zero." else print "a is negative." end 上の例では、変数 a が 0 より大きいとき、0 のとき、それ以外のときで表示を変えています。 条件文の条件式に使われる式を、論理式といいます。論理式の型は、論理型の場合と整数の場合があります。論理式では論理演算子と比較演算子が使われます。比較演算子にはつぎのようなものがあります。 =, == 等しい <>, != 等しくない > より大きい < より小さい >= 以上 <= 以下 論理演算子にはつぎのようなものがあります。 not 否定 or 論理和 and 論理積 論理演算子は数学の論理記号と同じ働きをします。 選択文選択文は、条件によって処理の流れを多方向に分岐させるための文です。条件文を拡張したもので、case 文などと呼ばれます。条件文でも代用できます。 select buttonNumber case 1 then print "Button 1 was pressed." case 2 then print "Button 2 was pressed." else print "Error: Unknown button number." end 上の例では、buttonNumber が 1 か 2 のとき「ボタンが押されました」と表示し、それ以外ではエラーメッセージを表示します。 反復文反復文は、処理の繰り返しを記述するものです。ループともいいます。 カウンタ型指定回数だけ反復を行うのがカウンタ型です。for 文などと呼ばれます。 x = 0 for i = 1 to n x = x + i end print x 上の例では、1 から n までの和を計算して表示します。 初期化-反復条件-増分処理型カウンタ型を一般化した型で、変数の初期化、反復条件、変数の増分処理を指定します。 for i = 1, i < n, i = i + 2 print i end 上の例では、1 から n まで 1, 3, 5, 7, ... と表示していきます。 コンテナ型コンテナの要素すべてを巡回する反復を行うのがコンテナ型です。foreach 文などと呼ばれます。 fruits = {"apple", "orange", "melon"} foreach f in fruits print f end 上の例では、コンテナ fruits の要素をすべて表示しています。 コンテナの要素巡回のためにイテレータ (反復子) というものが使われることがあります。 fruits = {"apple", "orange", "melon"} for f = fruits.begin, f <> fruits.end, f = f.next print f end 上の例は、初期化-反復条件-増分処理型反復でコンテナ fruits の要素をすべて表示する例です。イテレータはコンテナのある要素を指します。fruits.begin は fruits の最初の要素を指すイテレータで、fruits.end は最後の要素のつぎを表すイテレータ、f.next はイテレータ f が現在指している要素のつぎの要素を指すイテレータです。 条件型条件が成り立っているあいだ反復する、あるいは条件が成り立つまで反復するのが条件型です。while 文、until 文などと呼ばれます。条件を前に置くものと後に置くものがあります。 i = 1 x = 0 while i <= n x = x + i i = i + 1 end print x 上の例では、1 から n までの和を計算して表示します。同じ処理を、つぎのように書くこともできます。 i = 1 x = 0 untile i > n x = x + i i = i + 1 end print x 反復文内で使われる文反復文内で、反復を終了する文や、つぎの反復に移る文などを使える言語もあります。 for i to n if list(i) > 10 then break end end 上の例では、リストの先頭から順に 10 より大きい数を探して、見つかったら break 文というもので反復を終了しています。 for i to n if i mod 2 = 0 then continue end print i end 上の例では、i が 2 で割り切れる場合は continue 文というものでつぎの反復に移っています。結果的に 1 から n までの奇数を表示します。 goto 文goto 文は、任意の場所にジャンプします。 i = 1 x = 0 loop: x = x + i i = i + 1 if i <= n then goto loop print x 上の例では、 1 から n までの和を計算しています。loop: というのは任意のラベルで、goto loop によって処理が loop: にジャンプします。 goto 文はプログラムの流れを理解しにくくするため、使用が推奨されていません。goto 文などによって流れがこんがらがったコードは「スパゲッティコード」と呼ばれます。 手続き手続き処理を再利用可能な状態にまとめたものを手続きといいます。手続きはプロシジャ、副プログラム、サブルーチンなどと呼ばれることもあります。手続きは引数と呼ばれるいくつかのパラメータをとります。手続きを実行することを、手続きの呼び出しといいます。 手続きの中でさらに手続きを定義できる言語もあります。 プログラムの設計上、手続きを呼び出す手続きを上位、呼び出される手続きを下位などと言ったりします。上位手続きはユーザーとの対話を行い、下位手続きは計算に専念する、といったように役割を分けたりします。 引数仮引数と実引数手続きの定義の引数を仮引数、手続きの呼び出し側の引数を実引数といいます。 値渡しと参照渡し引数には値渡しと参照渡しがあります。たとえば、つぎのようなコードがあるとします。 procedure p(x) x = x + 1 end a = 1 call p(a) print a procedure p(x) ... end が手続きの定義です。値渡しは、変数の値をコピーして引数として渡します。この場合、手続き内での x の値の変更は、手続き呼び出し側の a に影響を与えません (出力は 1 となる)。参照渡しは、変数の参照を引数として渡します。したがって、手続き内での x の値の変更は、手続き呼び出し側のa に影響を与えます (出力は 2 となる)。 引数が値渡しか参照渡しかは、どちらか指定できる言語もあれば、どちらかに決まっている言語もあります。 参照渡しは手続きから値を受け取るために使われますが、大きいデータを渡す場合など、単にコピーを避けるためにも使われます。その場合、値が変更されないことを示すために定数型の参照が使われたりします。値渡しの引数でも、コピーを極力避けるために、引数が変更されない限りは参照を使い、変更されれば値をコピーする、というような方法を採用している言語もあります。 デフォルト引数引数にデフォルト値を設定できる言語もあります。 procedure p(x = 0) print x end call p(1) call p() 上の例では、引数が設定されればその値を表示し、省略されればデフォルト値の 0 を表示します。 キーワード引数引数は定義の順番で指定しますが、仮引数の名前で引数の値を設定できる言語もあります。名前を指定して設定する引数を、キーワード引数といいます。 procedure p(x, y, z) print x, y, z end call p(z = 1, y = 2, x = 3) 上の例では、引数名を指定して、任意の順番で引数を渡しています。 任意個数の引数任意個数の引数を渡すことができる言語もあります。 関数値を返し、式の一部として使用できる手続きを関数といいます。関数が返す値を返り値、戻り値などといいます。手続きを、返り値のない関数とみなす言語もあります。 function sum(*args) s = 0 foreach a in args s = s + a end return s end 上の例では、任意引数配列を受け取って、その全要素の和を返しています。return 文で返り値を指定しています。return 文の代わりに、関数名に代入することで返り値を指定する言語もあります。 手続きが return 文をもつ場合があります。その場合、値は返さず、手続きを終了するだけです。 多重定義 (オーバーロード)同じ名前の手続きを、引数の数や型で区別して定義する多重定義 (オーバーロード) が可能な言語もあります。手続き名と引数の組み合わせをシグネチャといい、シグネチャが違えば別の手続きとみなされます。 procedure f(x) print x end procedure f(x, y) print x, y end 上の例では、引数の違う手続き f を 2 つ定義しています。後の定義が前の定義を上書きしてしまう言語もありますが、多重定義をもつ言語は 2 つの手続きを区別します。 変数の型宣言が必要な言語では、引数の数が同じでも、型が違えば別の手続きと見なされます。引数の型が違うだけで処理は同じである手続きに、同じ名前をつけられるわけです。 再帰呼び出し手続きが自分自身を呼び出すことを再帰呼び出しといいます。普通は関数を使って行われます。処理が再帰的になっているものを表現するのに便利です。たとえば、1 から n の和を求める処理は、sum(n) = sum(n - 1) + n と表現できます。 function sum(n) if n = 1 then return 1 else return sum(n - 1) + n end end 再帰呼び出しを行えない言語もあります。 手続きの参照手続きの参照を手続きの引数として渡すことができる言語もあります。 たとえば、コンテナの中のある条件にあったものだけを表示する手続きを考えます。 procedure printIf(container, condition) for e in container if condition(e) then print e end end end 上の手続きは、第 1 引数にコンテナを受け取り、第 2 引数に関数を受け取ります。コンテナの全要素を参照する反復を行い、condition(e) が真のときだけコンテナの要素 e を表示します。つぎのように使います。 function even?(e) if e mod 2 = 0 then return true else return false end end container = [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ] printIf(container, even?) 上の例では偶数 2, 4, 6, 8, 10 が表示されます。 このように、手続きから呼び出してもらうように設定する関数を、コールバック関数といいます。 ライブラリ再利用可能な形で定義され、まとめられた手続き群をライブラリといいます。言語は普通、入出力操作や数学関数などのライブラリを標準で備えています。言語標準でない外部のライブラリもあります。 OS はシステムの機能を使えるようにシステム用のライブラリを提供しています。UNIX ではシステムコール、Windows では API (Application Programing Interface) と呼ばれます。 ブロックブロックコードをまとめたものをブロックといいます。条件文や反復文、手続きなどの本体はブロックです。ブロックだけを単独で定義できる言語もあります。 スコープと名前空間識別子の有効範囲をスコープといいます。スコープは普通ブロック内に限られており、たとえばブロック内で定義された変数はブロック外から参照できません。逆は可能です。変数を参照できることを「見える」、参照できないことを「見えない」と言ったりします。 x = 1 procedure f() x = 2 # 手続き外側の値を書き換えている y = 2 end y = 1 # 手続き内の変数ではなく、今新しく定義された変数 手続きのスコープが呼び出し側に依存して決まる、動的スコープを持つ言語もあります。 procedure f() print x end procedure g() x = 1 call f() end procedure h() x = 2 call f() end call g() # 1 と表示される call h() # 2 と表示される 動的でないスコープを静的スコープと呼びます。静的スコープでは、手続きから呼び出し側の変数は見えません。 手続き内にスコープをもつ変数を局所 (ローカル) 変数、プログラム全体にスコープをもつ変数を大域 (グローバル) 変数と呼びます。大域変数のような広いスコープをもつ変数は、いつ値が変更されるのかがわかりにくいため、注意が必要です。できるだけ使わないようにするか、使う場合も、それとわかるような名前にします。 定義された識別子のあつまりを名前空間といいます。同じ名前の識別子でも、属する名前空間が違えば異なる識別子として扱われます。名前空間を言語のユーザーが定義できる言語もあります。 ある変数や手続きにどの範囲から参照できるかを制御することを、アクセス制御といいます。アクセス制御は、処理のまとまりの独立性を確保するための重要な手段です。十分なアクセス制御の手段をもたない言語もあります。アクセス制御がない場合は、識別子の名前を工夫するなどして、慎重にプログラミングする必要があります。 例外処理プログラム実行中に発生するエラーを例外といいます。たとえば、「ファイルが開けない」などです。ファイルを開く関数 open を使ってファイルを開く場合、ファイルが存在しないなどの理由によってファイルが開けない事態にも対処しなければなりません。つぎのようなコードになるでしょう。 f = open("file.txt", "read") if f = null then # null は "空っぽ" の意味 print "Error: Can't open file." return end ... close(f) close 関数でファイルを閉じています。もしファイルを使っている途中で別の例外が発生した場合、その都度 close でファイルを閉じる操作を行わなければなりません。 ... read(x) # ファイルから値を読み出す if x <= 0 then print "Error: x must be positive." close(f) return end ... 例外処理はあくまで「例外の」処理なので、たくさんあると本筋のコードを読みにくくします。できれば、例外を処理するコードは本筋とは分離して一箇所にまとめておきたいものです。そういった用途のために、例外処理専用の機構を備えている言語があります。たとえば、つぎのようなものです。 try f = open("file.txt", "read") ... catch FileOpenException print "Error: Can't open file." close(f) return catch InvalidValueException print "Error: x must be positive." close(f) return end close(f) try ... catch で囲まれた部分で例外が発生すると、catch 以下でその例外を「キャッチ」します。ファイルを開くのに失敗した場合、処理がただちに catch FileOpenException 以下に移ります。 例外発生の有無に関係なく最終処理を行う機構を備える言語もあります。 try f = open("file.txt", "read") ... catch FileOpenException print "Error: Can't open file." return catch InvalidValueException print "Error: x must be positive." return finally close(f) end finally 以下は例外が発生してもしなくても実行されます。ファイルを閉じるなどの後始末をまとめて記述することができます。 例外を発生させることを投げる (throw) などといいます。自分で例外を投げることもできます。投げられた例外をキャッチするコードがない場合、例外が発生した手続きを呼び出した手続きに自動的に例外が投げられます。例外をキャッチするコードが現れるまでどんどん呼び出し元に投げられ続けます。 たとえば、手続き f が手続き g を呼び出し、g がさらに手続き h を呼び出したときに例外が発生した場合を考えます。もし h に例外処理のコードがなければ、その例外は g に投げられます。g にも例外処理のコードがなければ、その例外はf に投げられます。f に例外処理のコードがあれば、ようやく例外がキャッチされ処理されます。要するに、仕事上で問題が起こったときに、先輩、係長、課長、部長…と階層の上位に問題を報告していって、適切な人が適切な処理を行うといったような構造を実現しているわけです。 このように、例外処理機構は、ただ例外処理を本筋のコードから分離するだけでなく、例外を適切な上位手続きに処理させるための仕組みです。 メモリ管理アセンブリ言語では、値を格納するためのメモリ領域を自分で確保しなければいけません。コンパイル型の高級言語は、変数や配列の領域をコンパイル時に適当に確保してくれますが、実行時に必要なだけ領域を確保できる言語もあります。実行時にメモリを確保することをメモリの動的割り当てなどといい、そういうメモリを動的メモリといったりします。動的に確保したメモリは、使用後に言語のユーザー自身で解放する必要があります。 インタープリタ型言語では、メモリの動的割り当てはあっても解放操作が必要ないものがあります。そういう言語は、ガーベジコレクション (ごみ収集) という機構をもっています。これは、参照されていないメモリは自動的に解放する、という仕組みです。ガーベジコレクションがあれば、メモリ管理に煩わされずに済みます。 プログラミングのミスで最も厄介なものの一つは、動的メモリ管理上のミスです。できればガーベジコレクションを使うか、ミスを検出しやすいメモリ管理機構を実装するか、メモリ管理を手続き内に隠蔽して十分にテストを行うのがよいでしょう。 プログラミング手法構造化プログラミング処理を手続きの階層構造で表してプログラミングすることを、構造化プログラミングといいます。文章を書くときに、目次から先に作るような感じです。 たとえば、カレーをつくる処理を考える場合、大雑把には「ごはんを炊く」と「ルーをつくる」に分けられます。それぞれがまた細かい処理に分けられます。 カレーをつくる ごはんを炊く お米を研ぐ お米を炊飯器に入れてスタートボタンを押す ルーをつくる 野菜と肉を切る 野菜と肉を炒める 水を入れて野菜と肉をゆでる ルーを入れてゆでる このように処理を構造化しておいて、分けられたそれぞれの処理を手続きとしてコーディングしていくのが、構造化プログラミングです。 実装の方向として、下位の手続きから実装していくボトムアップの方法と、上位の手続きから (下位の手続きは存在すると仮定して) 実装していくトップダウンの方法があります。 モジュールプログラミング関連する手続き群をモジュールというかたまりにまとめておくと便利です。モジュールを組み合わせて行うプログラミングを、モジュールプログラミングといいます。 モジュールプログラミングをサポートしている言語は、アクセス制御を備えています。モジュール内の変数や手続きに対して、外部モジュールからのアクセスを許可したり、禁止したりできます。この仕組みにより、モジュールの独立性を保つことができます。各モジュールが独立していれば、コーディングやテストは各モジュールに集中して行うことができます。 データ指向プログラミングデータ指向プログラミングは、モジュールプログラミングの一種です。中心となるデータとそれを表すデータ構造があり、それに対する操作としての手続き群を定義して一つのモジュールとするという方法です。 たとえば、数学の「ベクトル演算モジュール」であれば、ベクトルを表すデータ構造を用意し、それに対して「大きさ」「和」「差」「スカラーとの積」「内積」「外積」などの演算を定義して一つのモジュールにします。つぎのようになるでしょう。 module Vector # ベクトルの作成 function new(x, y, z) return [x, y, z] end # ベクトルの削除 procedure delete(v) delete(v) end # 大きさ function norm(v) return sqrt(v(1)^2 + v(2)^2 + v(3)^2) end ... end v = Vector.new(1., 2., 3.) l = Vector.norm(v) Vector.delete(v) この方法のミソは、扱っているデータのデータ構造を知る必要がないということです。 オブジェクト指向プログラミングオブジェクト指向とは、お互いにメッセージを送りあって動きまわるオブジェクト (物) がシステムを構成する、という考え方です。オブジェクト指向プログラミングとは、オブジェクト指向によるプログラミングということです。オブジェクト指向プログラミングをサポートしている言語を、オブジェクト指向言語といいます。 オブジェクトは、その「設計図」となるクラスから作られます。クラスは、オブジェクトがもつ内部変数と振る舞いを定義します。クラスから作られるものはインスタンスとも呼ばれます。 オブジェクト指向プログラミングは、ある意味ではデータ指向プログラミングの延長とも言えます。たとえば、「データ指向プログラミング」で示したベクトルの例をオブジェクト指向プログラミングで行うと、つぎのようになります。 class Vector private _v(3) # 内部変数 # ベクトルの作成 procedure new(x, y, z) _v = [x, y, z] end # ベクトルの削除 procedure delete() delete(_v) end # 大きさ function norm() return sqrt(_v(1)^2 + _v(2)^2 + _v(3)^2) end ... end v = Vector(1., 2., 3.) l = v.norm() 上の例では、Vector がクラス、v が Vector クラスのオブジェクトです。内部変数 _v を属性などと呼びます。オブジェクトの振る舞いを定義しているクラスの手続きをメソッドなどと呼びます。 上の擬似言語上ではメソッドは手続きの呼び出しに過ぎませんが、本来のオブジェクト指向の考え方としては、メソッド呼び出しは「送られてきたメッセージに対する反応」を表しています。そういった本来のオブジェクト指向の雰囲気をもつ言語も存在します。たとえば、 l = sendMessage(v, norm) # オブジェクト v に norm メッセージを送る とでもすればよいのです。この場合、「振る舞いが未定義である」という例外が発生する可能性があります。 上のベクトルクラスの例では、手続き new, delete を呼び出すコードがありません。オブジェクトの初期化を行う手続きをコンストラクタ、イニシャライザなどといい、後始末を行う手続きをデストラクタなどといいます。たいていのオブジェクト指向言語では、オブジェクトの初期化と後始末の処理が自動的に呼び出されるようになっています。 オブジェクト指向プログラミングには、継承という操作があります。これは、クラスの定義を引き継いで新たなクラスを定義する操作です。継承されるクラスをスーパークラス、親クラスなどといい、継承によって作られるクラスをサブクラス、子クラスなどといいます。たとえば、クラス A を継承してサブクラス B を作る例を考えましょう。 class A procedure f() print "f() in A" end procedure g() print "g() in A" end end class B : A procedure g() print "g() in B" end end b = B() b.f() # "f() in A" b.g() # "g() in B" サブクラス B はスーパークラス A のメソッドや内部変数を引き継ぎます。ただし、B には A のメソッドと同じ名前のメソッド g があります。この場合、A から引き継いだ g は B で定義された g と置き換えられます。これを上書き (オーバーライド) といいます。 継承を使うと、既存のクラスの一部を置き換えるだけで新たなクラスを作ることができます。このようなプログラミングを差分プログラミングといいます。 オブジェクト指向言語のアクセス制御では、クラス内部から見えるか、外部から見えるかに加えて、サブクラスから見えるかどうかを指定できます。 ジェネリックプログラミングジェネリックプログラミングとは、データ型をパラメータにしたプログラミングです。変数の型宣言が必要な言語で値を入れ替える手続き swap を定義する場合、つぎのようになります。 procedure swapInteger(integer a, integer b) integer t t = a a = b b = t end procedure swapReal(real a, real b) real t t = a a = b b = t end procedure swapString(string a, string b) string t t = a a = b b = t end 上の 3 つの手続きは、扱うデータの型が違うだけで、処理はまったく同じです。 何度も同じことを書いていることに気がついたとき、プログラマは「気持ちの悪さ」を感じるものです。同じコードを数回書いた後で、そのコードに間違いがあることに気がついた場合、同じコードすべてを修正する必要があります。そういうとき、よく修正し忘れをして、プログラムが誤動作する原因になります。 手続きの多重定義の機能が言語にあれば、上の 3 つの手続きを swap という同じ名前にできます。呼び出すのは楽になりますが、同じ処理を 3 度書いていることには変わりありません。 こういうときに使うのが、ジェネリックプログラミングの機能です。ジェネリックプログラミングが可能な場合、つぎのように書くことができます。 procedure <T> swap(T a, T b) T t t = a a = b b = t end procedure <integer> swap procedure <real> swap procedure <string> swap 上の例では、型 T をパラメータにして処理を記述しています。T にそれぞれの型を渡してやれば、それぞれの型の swap ができあがるというわけです。 主なプログラミング言語FORTRAN古い言語で、命令型言語・静的言語です。現在でも数値計算の分野で使われています。FORTRAN で書かれた優れた数値演算ライブラリが多数存在しています。何度か仕様改訂が行われており、FORTRAN77、Fortran90 などがあります。最も使われているのは FORTRAN77 です。習慣的にラベルや goto 文が多用されがちなのが FORTRAN77 の難点です。 LISPFORTRAN と同じく古い言語で、関数型・動的言語です。S 式という括弧で表された式でプログラムを記述します。括弧だらけなので若干読みにくいのが難点です。Scheme など多くの方言があります。オブジェクト指向プログラミングに対応した LISP である CLOS というものもあります。 CUNIX という OS を記述するために開発された命令型・静的言語です。つぎの C++ とともに現在よく使われている言語です。OS を書くための言語であったため、ハードウェア寄りのプログラミングが可能です。それゆえ、高級言語ではなく中級言語と呼ばれることがあります。 C++C をオブジェクト指向プログラミングのために拡張した言語です。C をまるごと含みます。C とともによく使われている言語で、C/C++ などと並べて表記されることもあります。実行時型情報を取得できたり、ジェネリックプログラミングが可能です。習得が難しい言語です。 JavaC++ を改良して作られた言語です。バイトコードに変換され、仮想マシン上で実行されます。C++ の処理は関数からはじまりますが、Java の処理はすべてクラスのメソッドとして記述されます。無名クラスを定義できます。GUI ライブラリを含む膨大なクラスライブラリが存在します。 Python, Rubyオブジェクト指向・動的言語です。コーディングが楽で柔軟なのが利点です。C や C++、Java に比べて習得は簡単です。 その他の話題プログラミング言語を学んだだけでは、よいプログラムは作れません。以下では、プログラミングに関する言語以外の話題を取り上げます。 バグプログラマが意図しないで含めてしまったプログラムの誤りを「バグ」といいます。「バグのないプログラムはない」と言われています。失敗しない人間などいません。品質のよいプログラムを作るためには、「いかにバグを少なく抑えるか」が肝要です。 プログラムの特性コードは、どんどん複雑になっていきます。複雑さが大きくなると、コードが理解しにくくなり、バグを見つけにくくなり、バグを見つけても修正が困難になり、機能拡張もままならなくなります。ソフトウェアの開発が行き詰るのは、複雑さの増大によってです。ソフトウェア開発に欠かせない技術は、複雑さの増大を抑える技術です。 考慮すべきプログラムの特性には、以下のものがあります。
複雑さの増大を抑えることは、上のような特性を確保し続けることです。 設計プログラミング言語を扱えるからといって、製品にするようなソフトウェアを書けると思うのは間違いです。犬小屋と高層ビルでは建て方が違います。犬小屋なら趣味程度の大工技術があれば作れるでしょう。高層ビルを建てるには、大工技術はもちろんのこと、綿密な計画と設計が必要になります。同様にソフトウェアの開発においても、プログラミング言語以上の開発技術や知識が必要です。 建築物と同様に、プログラミングにおいても設計は重要です。プログラムの設計では、仕様を満たしながら保守性や拡張性などの特性を確保できるように設計を行います。 手続きやモジュール、クラスへの部品化と、それらの独立性の確保が設計において重要になります。牛一頭の丸飲みは無理でも、ステーキにすれば食べられるわけで、プログラムを人間の頭に入る大きさに部品化すれば、個々の部品は容易に理解できます。部品の独立性を確保することで、保守やテストが簡単になります。プログラムの機能を部品として「挿し込む」構造にしておけば、機能拡張が容易になります。 オブジェクト指向プログラミングでは、デザインパターンという優れた設計のパターンがあります。パターンを使うことによって、自分で下手な設計をすることを避けることができます。また、パターンを知っている人間にとって理解しやすいプログラムを作ることができます。 スタイルプログラミング言語仕様上で認められる範囲内では、自由にコーディングできます。言語の機能をどのように使うかという習慣を、スタイルといいます。スタイルの例として、つぎのようなものがあります。
「スタイル」とは「文体」のことです。スタイルはプログラマによって異なります。スタイルによって可読性が変わります。悪いスタイルは可読性を下げます。可読性が低いと、理解が困難になり、バグを混入しやすくなります。バグの発見も難しくなります。よいプログラムを作るには、よいスタイルを研究し、習慣づける必要があります。 複数のプログラマでプログラミングを行う場合は、複数のスタイルが混在することになり、コードが読みにくくなります。また、悪いスタイルを採用している人がいると、プログラム全体の品質を下げます。複数人での開発の場合は、統一されたよいスタイルを採用することになります。スタイルを統一するための取り決めを、コーディングルールといいます。 表明表明 (アサーション) は、バグを補足するための手段です。ある変数 x が必ず正である場合は、 assert(x > 0) などと表します。assert は表明のための手続きです。もし引数が偽の場合、assert はただちにプログラムを停止します。引数が真のときはなにもしません。 表明は、通らないはずの処理の経路を塞ぐのにも使えます。たとえば、変数 id が 1 か 2 の値しかとらない場合、つぎのように処理を記述するでしょう。 if id = 1 then ... else if id = 2 then ... end この場合、id が 1 か 2 以外の値をとることはないはずなので、if 文に最後の else は必要ありません。しかし、「ありえない」ことを引き起こすのがバグなので、「ありえない」経路は塞いでおく必要があります。 if id = 1 then ... else if id = 2 then ... else assert(false) end 上のようにしておけば、万が一 id が 1 か 2 以外の値になっていた場合は assert でプログラムが停止し、バグが存在することが判明します。 プログラミングにおいて最も恐ろしいのは、バグがあるのにプログラムが動き続けることです。プログラムが動いていると、プログラミングは順調に進んでいると思い込んでしまいます。そして、ずっと後になって、よりによって肝心なときに、バグは顕在化するものなのです。表明によって、バグが存在する限り動かないプログラムを作ることが重要です。 契約による設計手続きの仕様をコードに含めてしまう技法を、契約による設計といいます。契約による設計では、手続きが満たすべき条件を手続きに記述し、実行時にそれが満たされていない場合は、ただちに処理を中断します。契約にはつぎのようなものがあります。
契約条件は表明を用いて記述します。 契約による設計を用いれば、常に手続きが仕様を満たしていることを保証できます。 テスト手続きが正しく機能しているか、結果が正しいかどうかは、テストを行って確認します。 手続き本体を記述する前に、手続きが合格すべきテストを記述する技法を、テストファーストといいます。テストを書くためには、手続きの仕様が明確になっていなければなりません。テストを手続きより先に書くことによって、手続きの仕様で不明瞭な部分を洗い出すことができます。 テストは、テストを自動実行する環境を利用して行います。プログラミングの区切り (コンパイル時など) のタイミングで自動的にテストを実行させることによって、バグ混入を検知できます。 自動テスト環境として、xUnit (JUnit、CUnit、CppUnit など) があります。 リファクタリングリファクタリングとは、プログラムの動作は変えずに、コードを整理することです。コーディング前に綿密に設計を行ったとしても、コーディング後に設計の誤りに気づくことがあります。コードを追加していくうちに手続きやクラスの名前が不適切になったり、手続きやクラスを分割する必要がでてきたりします。そういったときに、リファクタリングを行い、手続きやクラスの名前や構造を整理します。リファクタリングによって定期的にコードを整理することで、コードが理解しやすくなり、バグの発見や修正、機能の追加などの手間が少なくなります。 リファクタリング前後でプログラムの動作が変わってはいけません。テストを用いて、リファクタリング後もプログラムの動作が変わっていないことを保証する必要があります。 ドキュメント生成コードは書いた本人だけが読むとは限りません。自分が書いたコードでも、長い時間をあけて見ると他人が書いたものと印象は変わりません。他人のため、明日の自分のために、コードの理解を助けるドキュメントを作成することは欠かせません。 しかし、コードは修正されます。コードの修正に合わせてドキュメントも修正しなければなりません。コードとドキュメントが別になっていると、ドキュメントの修正し忘れが発生する恐れがあります。これを避けるために、ドキュメント生成プログラムが存在します。ドキュメント生成プログラムは、コード内にドキュメントを記述し、それをもとにして整形されたドキュメントを作成します。 ドキュメント生成プログラムには Doxygen などがあります。 バージョン管理システムバージョン管理システムは、コードやそれに関連するファイルを管理するシステムです。ファイルの変更履歴を保存し、以前のバージョンのファイルの取り出しなどを行うことができます。 プログラミングを行っている途中で、以前のバージョンに戻したくなる場合があります。このシステムを使っていれば、以前のバージョンをすぐに入手できます。以前のバージョンを保存し忘れたり、なくしてしまったりすることもありません。 バージョン管理システムには CVS, Subversion, Git などがあります。 ビルド大規模なプログラムであれば、コードが複数になります。コンパイル型言語で書かれたものであれば、それぞれのソースコードをオブジェクトファイルにコンパイルし、できたオブジェクトファイルとライブラリをリンクして、実行ファイルを作成します。この一連の作業をビルドといったりします。 ビルドを行うツールとして、make があります。make は、Makefile というビルド方法を記述したファイルにしたがってビルドを実行します。コードの一部が修正されたとき、make は修正されたファイルだけコンパイルして実行ファイルを作り直します。 大規模なソフトウェアの開発には、普通、統合開発環境 (IDE) が使われています。統合開発環境は、GUI で構成されたソフトウェア開発環境で、ソースコード専用のエディタを備えており、ソースコードの管理やビルドなどを GUI で行うことができます。IDE も背景では Makefile を作成して make を実行していますが、IDE 利用者がそれを意識することはありません。 IDE には Eclipse や NetBeans などがあります。 make や IDE では、外部のプログラムを自動実行させることができます。この機能によって、ビルド前にコードチェックを行ったり、ビルド後にテストやドキュメント生成を行うことができます。 デバッグバグを取り除くことをデバッグといいます。デバッグを支援するプログラムをデバッガといいます。デバッガでは、プログラムの実行を途中で止めたり、変数の内容を表示させたり、命令を 1 つずつ実行させたりできます。 デバッグの方法として、怪しい部分の変数の値を手当たり次第に表示する方法 (デバッグプリント) がありますが、非効率です。デバッガが使える場合は、デバッガを使いましょう。 プロファイリングプログラムの性能を測定することを、プロファイリングといいます。プロファイリングを行うプログラムをプロファイラといいます。プログラムの高速化を行う場合、最も処理時間が長い部分 (ホットスポット) を最適化すると効果的です。プロファイラはホットスポットを見つけるのに役立ちます。 データ構造とアルゴリズム問題解決の手順のことをアルゴリズムといいます。プログラムは、データ構造とアルゴリズムのかたまりです。データ構造とアルゴリズムは、プログラミング言語以上に重要な知識です。これらがプログラムの構造と性能を決定づけます。一般化されたデータ構造とアルゴリズムを利用することによって、コードが理解しやすいものとなり、性能を保証することができます。 データ構造には、つぎのようなものがあります。
アルゴリズムには、つぎのようなものがあります。
一般に、自分で下手に作ったアルゴリズムよりも、よく知られたアルゴリズムを活用するほうが、性能も信頼性も高いです。 なにからはじめるべきか?C 言語でデータ構造とアルゴリズムの勉強からはじめるとよいでしょう。C 言語を学んだあとに、あるいは同時に、Python や Ruby を学ぶことをおすすめします。そのあとは、必要とあれば C++ や Java などを学べばよいでしょう。 参考文献
| |
PENGUINITIS |