プログラミング言語を学ぶには
- 比較して、相違点と共通点を知る
- 歴史を学んで、今存在する機能は何を解決するために発明されたのかを知る
プログラミング言語の誕生
目次
歴史
- プログラミング言語の歴史は「何かを楽をするため」の歴史
- 1946、原初のプログラミングは、ケーブルを繋ぎかえることだった
- 1949、紙テープに穴を開けてデータを表現した
- 1954、プログラミング言語のFORTRANで数式をそのまま書けるようになった
- FORTRANは決して実行効率の良い方法ではなかったが、それ以上に書きやすく、読みやすかったので広く受け入れられた。
目的
- 「怠惰・短気・傲慢」がプログラマの三大美德とされる
- 言語によって異なるのは「何を書くのが楽になるのか」
文法
- こう書いたら、こういう意味に解釈するというルールのこと
- 演算子の優先順位などが最たる例で、1+2*3の計算の順番は言語により異なる
変遷
スタックマシンとFORTH
- FORTHは演算子を演算対象の後ろに置く「後置記法」を取る
- スタックと呼ばれる、値を積んで置くを使う
- 左の2つの値が演算対象になる
- さまざまな順番での四則演算をシンプルな文法、記法で表現できる
構文木とLISP
- LISPは前置記法を取る
- 括弧によって、演算の順番を表現する
- FORTHもLISPも構文木によって構造化できる
- 括弧の階層だけ構文木の階層も深く表現する(コードと構文木がわかりやすく対応している)
- 現代の言語でも構文木は使わている
FORTRAN
- FORTRANは中置記法を取る
- 普段の四則演算のルールで書けるようになった
- 構文木解析器(パーサ)はソースコードを解析し、構文木を作る
- 構文解析の方法によって、文法は変わる
- 新しく作る機能が、既存のルールと競合することがままある
まとめ
- パーサによってソースコードを解析する
- 矛盾なく解析できる文法を組むのは難しい
- 結果としてとっつきにくい書き方が残ってしまうこともある
構文
- コードの構造を分かりやすくするために生まれた
If
ない時
- 直前の式が正しくなければ〇〇へジャンプという命令をしていた
- 条件を満たしていればジャンプは行われず、直下の命令が実行される
- 「もし〇〇でないならどこどこへ移動しろ」と条件を裏返しに表現する必要がある
- If elseを表現しようとすると、さらにややこしくなる
- 例えばc言語では、gotoを使えばif elseを表現できる
ある時
- 「もし〇〇なら××しろ」とストレートに書ける
- 分岐ごとにまとまりができて読みやすい
- なくても分岐処理は書けるが、あると読み書きが楽!
While
- 条件を満たしている間、ブロックの中身を繰り返し処理する
- ifの繰り返しバージョン
- こちらもgotoがあれば表現できてしまう
- gotoのようなパワフルな概念はかえって扱いづらいことがある
- Whileやif elseは制限付きgotoを作る営み
For
- ループ条件は「最初の値、増やす値、終わりの値」で表現する
- for文は↑の3つをセットで表現する(While文では散らばっていた)
- 与えられた対象(配列など)の個数だけループするforeachが登場
まとめ
- 構造化によって得られるのは新しい機能ではなく、読み書きの楽さ
- 繰り返しの制御方法は、条件式のwhile、回数のfor、処理対象のforeachの3つである
関数
関数の役割
- 関数の役割は、理解と再利用を楽にすること
- ソースコードに「かたまり」を作ることで、理解を助ける
- 機能に基づいた名前を「かたまり」付けることで、再利用を助ける
変遷
ジャンプ先の書き換え
- コードを再利用するには、「元の位置に戻る」命令が必要
- 前章で出てきたgotoで出来ないことは、元の位置に戻ってくること
- 元の位置に戻ってくるには、「ジャンプ元」を覚える必要がある
- 命令とデータが同じようにメモリに記録されていた頃、プログラム自身の書き換えができた
- ジャンプ命令のジャンプ先自体を書き換えて「戻る」を実現していた
✖️関数を呼び出す人が「呼び出し先」と「呼び出し元」を把握して置く必要があった
✖️コードの修正によって「位置」がずれると、関数を呼び出すコード全ての修正が必要だった
戻り先記録用メモリ(プログラムを書き換え→メモリを書き換え)
- 「戻り先メモリに書いてある番地にジャンプする命令」とセットで使う
- 呼び出し先が呼び出し元を管理しなくて良くなった
- 戻り先メモリはレジスタが使われる
✖️戻り先メモリの値を使う前に、別の関数が呼び出されると、戻り先メモリが上書きされてしまう
スタック
- スタックによって、戻り先メモリの問題を解決できる
- LIFOのデータ構造(最後に入れたものを最初に取り出す)
- スタックの先頭を記録する
- 新規データを読み込む時は、先頭の次の番地に値を入れる
- スタックの先頭を指す値に1を足す
- 新しいデータを読み込むたびに2と3を繰り返す
- データを取り出す際は、先頭の値を取り出す
- スタックの先頭を指す値から1を引く
- データを取り出すたびに5と6を繰り返す
- これらの操作によって、先頭は常に、最後に入ってきた番地を指す
再帰呼び出し
- 再帰呼び出しとは、自分自身を呼び出すことである
- 入れ子になっているデータを扱う際に、コードも入れ子になる
- 入れ子の深さを可変長として扱いたい時、入れ子の数に合わせてコードを書くのは難しくなる
- 再帰呼び出しで入れ子を表現すると、何重の入れ子にも対応できるコードになる
エラー処理
- プログラムの失敗は0にはできない
- 失敗を伝える仕組みが必要
- エラー処理の書き方は「返り値を使う方法」と「事前に登録したエラー処理にジャンプする方法」の2つがある
- 先の2つのうち、後者を例外処理という
返り値を使う方法
- 例外処理がないC言語で使われる手法
- 2つ問題点がある
失敗を見落とす
- 問題発覚時期がずれる
- 関係の薄い場所で問題が発覚することがある
エラー処理のせいでコードが読みづらい
- 「本来やりたいこと」と「エラー処理」が混在する
- 例外処理のないC言語では、gotoを使って「本来やりたいこと」から「エラー処理」を分離する
例外処理を使う方法
変遷
UNIVAC Ⅰの時(エラーが起きたらジャンプ)
- 1950年のコンピュータでは、オーバーフロー時に000番地の命令を実装するという例外処理が存在した
- この機能は「割り込み」として発展している
COBOLの時(2種類のエラー処理)
- 1954、FORTRANには例外処理がなかった
- 1959、COBOLには2種類のエラーが用意されていた
- ファイル読み出しのエラーとオーバーフローのエラー
✖️プログラマがエラーの種類を増やせなかった
PL/Iの時(例外の追加と発生)
- ON構文は柔軟で統一的なエラー処理を実現した
- 言語処理系が返り値チェックを行う
- エラーの種類を追加できた
- 増やした種類のエラーを呼び出せた
✖️失敗処理、本来やりたいこと、の順番で書く必要があった
CLUの時(構文化)
- どういう例外を投げる可能性があるかを明示的に宣言
- 字句的に「やりたいこと」を囲む構文
- 1975、上記2つの必要性を主張する論文が登場した
- 「begin endで囲んでブロックにする機能」と、「命令の後にエラー処理を書くexpect構文」の組み合わせでCLUに導入された
C++の時(try, catch, throw)
- try catchというキーワードを使う構文としてC++に導入された
- tryという字句はわかりやすくする飾りとしてつけられた
- throwという言葉で例外を発生させるようになったのもこの時から。理由はより適した字句がCの標準ライブラリ関数の名前に使われていたため
Windows NT 3.1の時(構造化例外、finally)
- 1990、C言語で作った新しいOSに「構造化例外」機能を追加した
- 構造化例外では、_try, _expect, _finallyで構造を作る
- _finallyは_tryが失敗してもしなくても実行する処理を囲む
出口を一つに
Finally
- コードの信頼性を高めてくれる
- プログラミングには、あれをやればこれもやっておきたいという「ついになる処理」が多く存在する
- ロックをかけたら外したいと言ったようなもの
- 失敗してもしなくても、やっておきたい処理を簡潔にかける
C++のRAII
- finallyを持っていないC++では、RAIIテクニックを使う
- その対象を扱うためだけのクラスを作成し、コンストラクタとデストラクタで対の処理を実行する
D言語のscope
- 2001、「C++をよりよく」というコンセプトで作られたD言語ではスコープガードという概念を導入した
- 対になる処理は近くにあるべきという考え方に基づく
- スコープ(関数など)を抜ける際の処理を事前登録でき、呼び出せる
例外的な状況とは
- 言語により異なる解釈も多く、正解はないというのがファイナルアンサー
- おかしくなったら処理を停止して速やかに報告すべきというフェイルファーストが一般的には良いとされる
- ソフトウェアの目的によっては簡単に停止させると不利益が勝る
例外の伝搬
- 呼び出し先関数で例外が発生し、そこで例外が処理されなかった場合、呼び出し元関数を順番に辿る(伝搬)
- 辿ったどの関数でも例外が処理されなければ、プログラムは終了する
✖️ある関数が投げる例外を把握するためには、その関数が呼び出す関数の例外も全て把握しなければいけない
✖️プログラムの異常終了をコントロールできない
Javaの対応
- 例外を以下の3つに分類
- 「例外処理すべきではない重大な問題」
- 「例外処理をしてもよい、実行時例外」
- 「例外処理をしてもよい、その他の例外」(検査例外と呼ばれる)
- 検査例外を使うと「呼び出し元に伝える(宣言)」か「自分で処理する(catch)」かどちらかを実装しないとコンパイルエラーになってくれる
✖️めんどくさいのであまり普及していない
名前とスコープ
名前の概要と論点
- コンピュータには番号がわかりやすいが、人間には名前がわかりやすい
IPアドレスとドメインみたいな関係ですね
- 名前とモノの対応表を作って関連づける
- 名前は往々にしてかぶる
- プログラム全体で1つの対応表が使われているとき、定義された変数をグローバル変数と呼ぶ
- 名前の被りを避けるためには「長い変数名」「スコープ」で対応
スコープ
- 名前の有効範囲のこと
動的スコープ
- 関数の入り口で元の値を複製し、出口で復元する
✖️書き忘れしないように管理が必要
✖️書き換えた値が呼び出し先に波及する
静的スコープ
- 関数に入ったときにその関数専用の対応表を用意する
- 関数内での変数への代入はその対応表を使う
- 関数を抜ける時にその対応表を破棄する
- スコープ内で変数を参照するときは、まずスコープの対応表を見に行く
- なければグローバルスコープを見に行く
- 安心して短い名前を使えるようになった
静的スコープに残る問題
- ネストした関数で、変数が見つからない場合は1つ外のスコープを参照してほしいところだが、グローバル変数を見に行く
- ネストされたスコープで、内から外の変数を書き換えられない
- これらの問題は、代入によって変数を作ることに起因する
- Pythonではnonlocalと宣言することで、外のスコープを書き換えられるようにした
型
型が必要な理由
- 型とは、ビット列をどういう種類の値として解釈するか
- 同じビット列でも、型によって違う値になる
型の表現
変数名
- 変数名の先頭がI〜Nなら整数、などルールを決めて表現した
型宣言
- intと宣言して整数など、宣言によって表現した
- 変数と変数で演算する際、型が異なるとエラーになった
- 言語処理系が暗黙的に変換するようにした
問題点
- 例えば整数か浮動小数点数、どちらに変換して計算するかによって、計算結果に誤差が生まれる
- 特に割り算においては1/2の1をどちらとして扱うかによって、結果が0と0.5のように重大な差となる
- Pythonでは、x/yを切り捨てない割り算、x//yをその逆とした
型の展開
ユーザ定義型とオブジェクト指向
- 言語が用意している基本的な方を組み合わせて、新しい型を作る機能
- 関数などの「データをどう処理するか」も型とされた
- C++の設計者はユーザが定義できる型を「クラス」とした
クラス
- 「型は仕様である」という考え方が現れる
- publicとprivateで、外部とやり取りする部分だけを公開するようになった
インターフェース
- 具体的な実装を持たない、まさに仕様である型(インタフェース)が現れる
- 例外をどう投げるかも型に含める言語が登場
ジェネリクス、総称型、テンプレート
- 型を引数にとって作る関数が登場
- 一部だけ変更して再利用したいというニーズに応えた
動的型付け
- 前項までの静的型付けでは、言語処理系が「変数名」「メモリ上の番地」「値の種類」をセットで持っていた
- 一方「種類」と「値」をセットにしたのが動的型付け
- マシンの高速化で応用分野が拡大
- 大部分のスクリプト言語で採用される
動的型付けの実現
- メモリの上で同じ型として扱えるように設計されているため、型宣言が必要ない
- 例えばPythonでは、全ての値をPyObjectが型として扱えるようになっている
○柔軟な処理
✖️コンパイル時に行われる型の整合性チェックは、一部バグの早期発見につながっていた
型推論
- コンパイル時の型チェックはする、めんどくさい型宣言はしない、両取りしようとするアプローチ
- 例えば、関数の役割から型を推論する。例えば、引数の型から返り血の型を推論する
- どうやって、どれくらい型推論するかは言語により異なる
まとめ
- どんな種類の値なのかを管理するために型は生まれた
- 型に含める情報は多様な広がりを見せた
- 静的、動的型付けのように、タイミングの違いも生まれた
コンテナと文字列
- いくつものモノを入れるためのモノ
- コンテナの種類は、メモリへの格納方法の種類
- 万能なコンテナはなく、最適なコンテナは状況で変わる
整数と値
配列
- メモリ内で順番に値が格納される
○メモリ容量を節約できる
○n番目の要素を探すのが早い
✖️値を挿入する時に、挿入された位置よりも後ろは全てずらす(コピーする)必要があり時間がかかる
✖️まとまったメモリ空間が必要
連結リスト
- 値と、次の値の場所情報、をセットで持つ
○値を挿入する時、値を追加して、手前の値の場所情報を変更するだけで良い
○細切れのメモリ空間があれば良い
✖️メモリ容量を2倍食う
✖️n番目の要素を探すときに時間がかかる
言語による差異
- Pythonでは、[1, 2, 3]を配列として扱う
- 一方LISP, Scheme, Haskellでは連結リストとして扱う
- 記法も言語により異なる
文字列と値
- 文字列と値を対応づけたコンテナを、辞書・ハッシュ・連想配列と呼ぶ
- 配列は整数と値の対応づけ
- 値と対応づけられた文字列を「キー」と呼ぶ
ハッシュテーブル
- ハッシュ関数でキーを整数に変換して対応付けを実現する方法
- 多くの言語で採用されている
- 値を入れる大きな配列を用意する
- ハッシュ関数でキーを整数nに変換する
- 配列のn番目に値を格納する
○変換した整数でダイレクトに値を探せる
✖️メモリ消費量が大きい
木
- キーが小さければ左に、大きければ右にという基本ルールで格納される
- 2分探索と同じ原理である
文字
- 人間が決めた記号の集合
- これを「文字集合」「文字セット」などと呼ぶ
- 文字集合をデジタルデータとして表現するために「符号化」を行う
- 符号化も、人間が決めたルールに過ぎない
符号化の歴史
モールス符号
- 短点と長点の組み合わせで文字を表現した
- 人間が耳で聞いて受信する想定の方法
- 1秒間で送受信できる情報量に限界がある
ボーコード
- より高速に通信する需要から生まれた
- キーボード入力で送信し、プリンタ出力で受信する
- この時使われた国際的な通信網をテレックスといい、符号化方法がボーコード
- オンオフ5つの組み合わせ(5ビット)で1文字を表現した
- 改行や復帰などの「制御コード」が文字セットに追加された
- 5ビットでは32通りなので、シフトでモードを切り替えて使用した
EDSAC
- ここからコンピュータの話
- ここでも1文字5ビット、数字と文字をシフトで切り替え
ASCIIとEBCDIC
- 符号化方法の種類が増えた
- 標準化を目指す動きが起こり、ASCII(アスキー)が生まれる
- 1文字7ビット、128種類の文字によって、シフト切り替えが不要に
- 当時コンピュータシェアの大部分を持っていたIBMは8ビットのEBCDICを公開した
- 独自のやり方を使ってもらうことによって、他社製品への移行コストを高める狙い
- 標準化は遠のいた
日本語
- 「ISO-2022-JP」「Shift_JIS」「EUC-JP」などが生まれた
- 「ISO-2022-JP」はアルファベットモードとひらがなをモード切り替える
- 「Shift_JIS」は先頭の文字で1バイト文字と2バイト文字を識別する
- 「EUC-JP」2バイト文字の両方を1バイト文字と違うものにした
Unicode
- 1993、Unicodeが文字セットの国際標準化を成す
- よく目にするUTF-8は、Unicodeの文字セットを符号化する方法
文字列
- 文字列とは文字が並んだもの
- 表現方法は言語によりまちまちなので比較する
Pascal
- 1文字8bit(0~255)つまりASCIIとかEBCDIC
- 先頭に文字列の長さ情報を置く
C言a語
- 1文字8bit(0~255)
- 文字列が始まるメモリ上の位置は持っているが、長さは持っていない
- NUL文字(ぬるもじ)で終わりを表現する
Java
- char型が16bit(0~65535)つまりUnicode
Python
- 8bitと16bit両方をサポート
- 混ぜて使うと不具合の温床になった
- デフォルトをUnicodeにして、b”なんたら”とbをつけることで8bitの文字列とした
Ruby
- 符号化方法の情報も含めた8bitの文字列を展開
- 携帯電話で使う絵文字を素直に書けるメリットを持つ
GoogleやApple
- Unicodeで、携帯で使われている絵文字も含めるように働きかけた(追加された)
まとめ
- 何が文字であるか(文字集合)
- どうやって文字をビット列で表現するか(符号化方式)
- どういう情報をどうメモリに格納するか(文字列の実装)
- 実装による解決と標準化による解決
並行処理
処理を切り替える方法
- 人間が気づかないくらい短い感覚で複数の処理を切り替えながら実行する
- 複数の処理が走っている::ように見える::
- 現在はマルチコアのCPUが主流だが、プログラミング言語に焦点を当てるため、1つとして話を進める。
協調的マルチタスク
- キリの良い所で交代する
- それぞれの処理の「交代していいよ」命令に依存する
プリエンプティブマルチタスク
- 一定時間で交代する
- タスクスケジューラが管理する
- ほとんどのOSはこちらで並行実行している。
競合
- 交代しなさいといつ言われるか分からなくても大丈夫なプログラムを書く必要が出てきた
- 複数セットで行われる前提の処理が途中で止められて発生する「競合状態」が問題となった(振込みなど)
- 下の3条件は1つでもなくすことができれば競合が起こらない
競合状態の3条件
- 2つの処理が変数を共有している
- 少なくとも1つの処理がその変数を書き換える
- 一方の処理が一段落付く前にもう一方の処理が割り込む可能性がある
共有しない
- UNIXでは実行中のプログラムを「プロセス」と呼ぶ
- 異なるプロセスはメモリを共有しない
✖️メモリを共有しない割り切りは厳し過ぎた
- メモリを共有するプロセス「スレッド」に回帰する
アクターモデル
- メモリを共有するのではなく、「メッセージを送る」
- 返事を持つ時間に無駄が多くなる(ロックの問題)
書き換えない
・現実的な妥協策として、「一部の変数を書き換えられ無くする」を採用した言語は多い(constやval による変数宣言)
割り込まない
- ファイバー、コルーチン、グリーンスレッドなどと呼ばれる手法
- RubyのFiberクラスや、PythonやJavaScriptのジェネレータがこれに当たる
ロック
- 「今割り込まれると困る」処理に印をつける方法
- ロックとも呼ばれるが、印があるだけでロックされているわけではない
モニタ
- Javaが火付け役
- 「印をチェック、付いてたら待つ、付いてなければ印をつけて入る」という手順を簡単にした
- ただ「synchronizedブロック」で囲むだけで手軽にロックが使えるようになった
ロック
デッドロック
- 2つの処理がそれぞれロックしたモノを使いたくてお見合いになる状態
- ロックの順番を一貫しなければならない
合成
- ロックでは2つの命令の間に割り込まれることを防げない
- 2つの処理をつなげる前提の処理ができないということ
- 2つの処理をsynchronizedブロックなどで囲まなければいけず、めんどくさい
トランザクショナルメモリ
- コンセプトは「手元で試しにやってみて、失敗だったら最初からやりなおし、成功だったら変更を共有する」
- 一時的に別バージョンを作ってそれを書き換えて、ひとかたまりの処理が終わってから反映する
- 書き込み頻度が高い場合は「やりなおし」が多発して性能が悪くなる
オブジェクトとクラス
オブジェクト指向
目的
- コンピュータで課題を解決するには、現実世界の「モノ(オブジェクト)」の「モデル」をコンピュータの中に表現する必要がある
対立
- オブジェクト指向をめぐっては意見の対立がある
- C++の設計者は、型や継承に肯定的である
- Smalltalkの設計者は型や継承に否定的であり、オブジェクトがメッセージを送り合ってコミュニケーションするものとしている
クラス
- C++では「クラスはユーザが定義できる型のこと」
- 型の捉え方も言語により異なる。本章残りは動的型付け言語でのクラスの仕組みを解説する
- クラスを特に重要視した設計のJavaを除いて、プログラムを書くうえでクラスは必要不可欠ではない
- クラスを使うべきかどうかも、作ろうとしているプログラムによる
モデルの作成方法
- まとめてモデルを作成したいという目的がある
- 一旦、クラス以外の方法を見ていく
モジュール・パッケージ
- 関連性の強いものを「いくつかのまとまり」に分けたほうが理解が楽になる
- まとまりを明示するためにモジュールという概念を導入した
- 関数や変数をひとかたまりにして、それに名前をつけることでモデルを作る
複数個作成する
- 現実社会でしばしばある「似たようなものが複数個ある」というシチュエーションに対応できない
データ置き場
- 複数作るためには共通の動作、異なるデータ置き場が必要である
- データ置き場をそれぞれハッシュ(辞書・連想配列)で定義する
- ハッシュを関数の引数に渡して実行する
初期化処理
- 先の方法では何度もハッシュを定義することになる
- 定型的な作業なので、モジュールにnew関数を作り初期化方法を記述してしまう
- このような、オブジェクトを作る関数をコンストラクタ(構築するもの)と呼ぶ
データ置き場とモジュール
- ハッシュと特定のモジュールを結びつけるために「bless」(祝福)という概念が発明された
- 「bless $ハッシュ, “モジュール”;」のように記述する
- モジュールの関数をハッシュに対して矢印演算子で呼び出せる
関数もハッシュに入れる
ファーストクラス
- JavaScriptは、関数もハッシュに入れるというアプローチをとっている
- 「変数に代入する」「関数の引数として渡す」「関数の戻り値として返す」などが可能である値のことを「ファーストクラスの値」と呼ぶ
- JavaScriptの場合、関数はファーストクラスの値である
関数をハッシュに
- {func : function(){}, name: “name”}のように記述できる
- 関数からはthis.nameで”name”にアクセスできる(thisの詳しい説明は割愛)
- この方法も、モデルを1つしか作成できない
複数個作成する
- 初期化するための関数を作成する
- 新しく作成した関数の中で先ほどのハッシュをreturnで返すだけ
- 「複数のオブジェクトを作れる 」「ひとかたまりのものに見える 」「初期化の方法を人間が覚えなくてよい」という特徴を持つようになる
プロトタイプ
- 先の方法では、関数も複数作られて、メモリを圧迫する
- 共通する部分はプロトタイプにまとめてしまう
- 自分の知らない値について聞かれると、オブジェクトは自分のプロトタイプに聞いて答える
New演算子
- 新しいオブジェクトxを作る
- 作られたオブジェクトxのプロトタイプを、関数fのプロトタイプに変更する
- 作られたオブジェクトxをthisに入れて関数fの本体を実行する
- オブジェクトxを返す
- プロトタイプを用いたこれらの処理を一気に書けるように、new演算子が作られた
クロージャ
- 多くの言語では状態を持った関数を作ることができる
- 関数の中で変数を持つということ
- 例えば、呼ばれるたびに一ずつ増えていくカウンタ関数
- スコープ内の変数を同じくスコープ内の関数で操作することからクロージャと呼ぶ
クラス
- 原初のクラスとは単純に「分類」を表した
C++のクラス
- C++では、クラスはタイプである
- 「オブジェクトがどういうメソッドを持っていて、どういうメソッドを持っていないか」という仕様を宣言する役割も持っていた
クラスの持つ3つ役割
- まとまったものを作る生成器
- どういう操作が可能かという仕様 (インターフェース)
- コードを再利用する単位(継承)
継承
概観
- 往々にして、同じ分類のものは、同じ属性を共通に持っており、細かく分けてもその属性は引き継がれる
- 「子クラス」を作成したら親クラスの属性は自動で引き継ぐ
- 広く使われるようになり、広く解釈されるようになった
- 以下に大別した3通りの考え方を記す
①一般化・特殊化
- 親クラスで一般的な機能、子クラスで目的に特化した機能を実装する
②共通部分の抽出
- 複数のクラスの共通部分をその親クラスとして抽出する
③差分実装
- 継承して、変更点だけ実装する
論点
- 自由度が高く、混乱を招く
- 継承の階層が深くなると、上の階層での変更が及ぼす影響範囲が広くなる
リスコフの置換原則
- 小クラスは親クラスの部分集合でなくてはならない
リスコフの置換原則(LSP)をしっかり理解する – Qiita
多重継承
- 複数の分類に属していることを表現する
- 複数クラスからの継承
多重継承の問題点と4つの解決策
- 名前解決の問題が発生する
- 自分の知らない値について聞かれると、クラスは自分の親クラスに聞いて答える
JavaScriptのプロトタイプの時と同じですね
- 多重継承をして親クラスが同じ名前のメソッドを持ってたら?
①多重継承を禁止
- 多重継承の便利さを捨て去り、他の手段を講じる
委譲
- 使いたい機能を持ったクラスを作って呼び出す
- コンストラクタで注入するようになり、依存性の注入「DI(Dependency injection)が生まれた
インターフェース
- Javaはインターフェースの多重継承は認めた
- 多重継承の問題を避けつつ、コンパイラにチェックさせる
②メソッド解決順序
- 探索する順番を定義してしまうという試み
深さ優先探索
- 先に書かれた親から順に聞く
- ✖️菱形継承をすると近い階層を飛ばすことがある
C3線形化
引用
- 親クラスは子クラスより先に探索されない
- あるクラスが複数の親クラスを継承している場合は先に書いてあるものが優先される
③処理を混ぜ込む
- 再利用したい機能を持った小さいクラスを大きいクラスに混ぜ込む
- 菱形継承がなくなり継承ツリーの階層を浅くできる
④トレイト
- クラスは「ある機能について完全性を有した大きなクラス」と「純粋な小さなクラス」に大別できる
- 再利用に特化したもっと小さい構造を用意する(トレイト)
- 名前の衝突が起こった場合は明示的なエラーを投げる
- 既存のトレイトを継承・合成して新しいトレイトを作成できる

コメント