その他の言語#
教科書該当なし プログラミング言語
本ページでは例外処理,及びここまで取り上げなかったモダンな言語について紹介する.
例外処理#
例外処理は多くのプログラミング言語で採用される基本的な機能であり,実践的なプログラミングにおいて必須知識である.特にJavaでは検査例外が存在しているため,例外を知らずにJavaを使いこなすことはできない.
また例外の理解はデバッグの効率化にも繋がる.例外が発生した際のスタックトレースを理解できれば,エラー発生の原因となった箇所を極めて効率的に絞り込むことができる.
本設ではまず例外処理が必要となる状況を共有し,メソッドの提供者側と利用者側の2つの観点から例外処理の使い方を説明する.
例外の必要性#
Javaによるスタックの実装を例題に,例外の必要性を考える.
ArrayStackはint型の値を格納するスタッククラスであり,Mainがそのスタックを利用している.この例では,Mainクラスがスタックに対する過剰なpop()
操作を呼び出している.ArrayStackクラスでは過剰pop()
の対策を行っていないため,return elements[--sp]
の部分で配列外参照が発生してしまいプログラムはクラッシュする.
この場合,ArrayStackクラスはどのように振る舞うのが適切だろうか?
pop()
の操作をどのように修正すればよいだろうか?
まずはスタック利用(Main)側ではなく,スタック自体(ArrayStack)がどう振る舞うべきかを考える.
Note
プログラム開発時には,利用者(呼び出し側,caller)と提供者(呼び出される側,callee)の2つの異なる視点が存在している点を認識せよ.今回の場合,Mainクラスがスタックの利用者であり,ArrayStackクラスがスタックの提供者である.
演習のような小さなプログラムの開発中は,利用者側と提供者側の振る舞いを自由に変更できる場合が多い.この場合,提供者側を修正せずとも利用者側で過剰pop()
が発生しないように気をつけるだけで良い.
他方,実際のプログラム開発では利用者≠提供者であるケースが多々ある.例えばStringクラスに対しては,Javaランタイム自体がStringの提供者であり,我々はその利用者である.この場合,提供者は利用者がどのような使い方をしても良いようにプログラムを提供するべきである.
以下のような対策が考えられるが,いずれも問題点がある.
対策1:何も返さない#
👎:そもそもコンパイルできない.int
値を返しておらずint pop()
というシグネチャのルールに従っていない.
対策2:強制終了する#
👎:危険すぎる.利用者が誤って過剰pop()
をしてしまうとプログラム全体が強制終了する.配列外参照によるクラッシュとほぼ同様の振る舞いであり,問題は何一つ改善されていない.
対策3:特殊な値を返す#
👎:-1という値がスタック中にあったのか,過剰pop()
してしまったのか判別できない.また「-1は特殊な値だ」という特殊ルールを利用者側が認識しておく必要がある.
対策4:特殊フラグを設ける#
ArrayStack側にエラーを表す特殊フラグフィールドを作る.
public class ArrayStack {
private int sp;
private int[] elements;
boolean error = false; // 特殊フラグフィールド
...
public int pop() {
if (sp - 1 <= 0) {
this.error = true; // エラーが発生したらフラグtrue
return -1; // この場合返り値は意味をなさない
}
return elements[--sp];
}
Main側ではpop()
後にフラグを確認する.
public static void main(String[] args) {
stack.pop();
if (stack.error) { ... }
stack.pop();
if (stack.error) { ... }
👎:「pop()
後にフラグを確認せよ」という特殊ルールを利用者が認識しておく必要がある.
プログラム処理中のエラー発生は,プログラミングにおいて極めて一般的な現象である.
- 配列外参照を行った(
elements[--sp]
elements[elements.size()]
) - 0除算を行った(
y = 1 / x
) - ファイルの作成処理で,すでに同名のファイルが存在していた
- ネットワーク処理中に,何かしらの理由でコネクションが切断された
エラーが発生する条件のチェック自体は難しい問題ではない.スタックに対するpop()
の場合if (sp - 1 <= 0)
がエラー条件のチェック処理に該当する.問題は「エラーが発生した」という状況を提供者側がどのように表現し,クラスの利用者に伝えるかである.
多くのプログラミングでは,プログラム処理中のエラーの発生を表す方法が言語仕様として設けられている.このエラーのことを例外(exception),例外のハンドル方法を例外処理(exception handling)と呼ぶ.直感的には,return
による正常時の返り値とは独立に異常時の返り値を定義する方法である.
例外の処理方法:提供者側#
Javaにおける例外処理方法を説明する.対策4(特殊フラグ)と同様,提供者側と利用者側の双方で例外の対策が必要である.提供者は例外を発生させ,利用者が例外を受け取るという処理を加える.
まず提供者側では,特定の状況下で例外を発生させる.
public int pop() throws EmptyStackException { // 例外を宣言に追加
if (sp - 1 <= 0) {
throw new EmptyStackException(); // 実際に例外を投げる
// return ??? // throwしたらreturn不可(到達不可)
}
return elements[--sp];
}
エラー条件if (sp - 1 <= 0)
を確認した後,throw
節によって例外インスタンスを投げる.throw
節は例外を投げた直後にメソッドを終了する命令であり,例外専用のreturn
節だと認識しても良い(1).
- 正常時は
return
で,異常時はthrow
で値を返す,と解釈しても良い.
throw
対象であるnew EmptyStackException()
はnew
節を用いた単なるオブジェクトのインスタンス化である.Javaはオブジェクト指向言語であり,例外そのものもクラスとして表現されている.throw
時には例外をインスタンス化して投げる(1).
throw
対象のクラスはjava.lang.Throwableクラスを継承している必要がある.
今回の例では,java.utilパッケージのEmptyStackExceptionクラスを投げている.Javaには様々な例外が用意されており,エラーの状況を表す適切な名前の例外クラスを探すと良い.スタック利用者が例外の名前だけを見て,何が起きているのか予想可能な例外クラスを選ぶ.
メソッド内でthrow
する際には,必ずシグネチャにthrows
を加え,どのような例外を発生させ得るのかを宣言する必要がある.return
節の返り値がint型であることをint pop()
のように宣言するのと同じ理由である.シグネチャとはメソッドの仕様である(1).
- 複数の例外をメソッド内で
throw
しても構わない. この場合,throws Exception1, Exception2
のように複数種類の例外が発生する可能性があることを宣言する.
Quiz
以下のプログラムはthrows
による宣言と,throw
による実際の例外クラスが異なるにも関わらずコンパイルエラーは発生しない.その理由は何か?
public int pop() throws Exception {
if (sp - 1 <= 0) {
throw new EmptyStackException();
}
return elements[--sp];
}
Answer
EmptyStackExceptionクラスがExceptionクラスを継承しているから.
EmptyStackException is a Exception
の関係が成り立つ.
Object o = new String("a");
が成り立つのと同じ理由である.
String is a Object
の関係が成り立つ.
例外の処理方法:利用者側#
利用者側ではtry catch
節によって例外を掴む必要がある.
例外を発生させ得るメソッドpop()
の呼び出しは,必ずtry { ... }
ブロックで囲む必要がある.try
は「失敗するかもしれない処理を試す」というニュアンスである.もしpop()
内で例外が投げられた場合,catch() { ... }
側に処理が移る.「発生した例外を掴む」というニュアンスである.提供者側がthrow
節で投げた例外インスタンスはcatch(例外の型 e)
の変数e
として受け取ることができる.
例外発生時にcatch
内でどのような処理を行うべきかは状況によって異なる.回復不能な致命的な状況であれば強制終了catch (..) { System.exit(1); }
とすることもある.あるいは過剰pop()
を無視して良ければcatch (..) {}
としても良い.これを例外を握りつぶすとも言う.
例外クラスはprintStackTrace()
というメソッドを必ず持っている(継承している).例外が発生した瞬間のメソッドの呼び出し系列(1)を標準出力に書き出すメソッドである.例外発生の状況把握に役立つため,ひとまずcatch
後にはe.printStacktrace()
してみると良い.
- スタックフレームの情報のこと.
標準出力には以下のような内容が書き込まれる.日常的によく見るスタックトレースである.
java.util.EmptyStackException // ← 発生した例外の名前
at jp.osakau.ArrayStack.pop(ArrayStack.java:18)
at jp.osakau.Main.main(Main.java:13)
// ↗ ↗ ↑ ↖
// パッケージ名 クラス名 メソッド名 ファイル名:行番号
これまで説明した例外処理を適用済みのMainクラスとArrayStackクラスを以下に示す.
public class Main {
public static void main(String[] args) {
ArrayStack stack = new ArrayStack();
stack.push(1);
stack.push(2);
System.out.println(stack);
try { // まとめてtry-catchも可,ただし範囲はでできるだけ狭く
stack.pop();
stack.pop();
System.out.println(stack);
stack.pop();
} catch (EmptyStackException e) {
e.printStackTrace(); // 適切な対策が不明なのでひとまずprint
// TODO // TODOをつけておくと良い
}
}
}
検査例外と非検査例外#
Javaには検査例外と非検査例外の2種類の例外がある.検査例外は検査が必須となる例外である.検査していない場合はコンパイルエラーとなる.
検査例外を発生させるメソッドを呼び出す際は,try catch
で例外を直接検査するか,throws
宣言を加えて例外をさらに親メソッドへ投げる必要がある.他方,非検査例外は例外の検査がオプショナルであり,利用者側で例外の発生を無視することができる(1).
- 先の例のEmptyStackExceptionクラスは非検査例外である. つまりMainクラス側での
try catch
を省略してもコンパイル・実行が可能である.
Javaではファイル周りの操作で検査例外が多数設けられている.よってJavaによるファイル操作はプログラムの記述量が多くなりがちである(1).
- 他の言語と比較して厳格だといえる.
try {
Files.delete(Paths.get("tmp.txt"));
} catch (NoSuchFileException e) { // fileがないとき(検査例外)
...
} catch (DirectoryNotEmptyException e) {// dirが空でないとき(検査例外)
...
}
スレッド周りの処理も検査例外が多い.
Pythonは全て非検査例外であり検査例外が存在しない.つまり利用者側による例外検査は必須ではない.
Quiz
以下のプログラムを実行したときに,f2()
が生成し投げた例外がf1()
でどう扱われるかを考えよ.Exceptionクラスは検査例外である.
クイズ | |
---|---|
Quiz
以下のプログラムを実行したときに何が起こるか考えよ.
クイズ | |
---|---|
Answer
出力は以下の通りである.先のクイズのスタックトレースにいくつかの情報が加えられている.
ERROR!
Exception in thread "main" java.lang.Exception:
at Main.f2(Main.java:10)
at Main.f1(Main.java:7)
at Main.main(Main.java:4)
発生した例外を誰も掴まなかった場合,JavaランタイムがERROR! Exception in ..
というメッセージとともに,その例外に対してprintStackTrace()
を呼び出す.
先述の通り,検査例外を検査しなければコンパイルエラーとなる.このコンパイルエラーを避けるためにthrows
を適当に追加すると,上記のような状況になりやすい.Eclipse等のIDEのコード補完はこのthrows
による解決を推奨してくることがあるが,検査例外は可能な限りtry catch
を用いて適切に処理することを推奨する.
例外が発生しても回復可能なケースが多々ある.例えば,ファイル削除の際には「指定したファイルが存在しない」状況を保証できれば良い場合がある.この場合,以下のように例外を処理すれば良い.
API仕様の読み方#
OO言語と上記例外の内容を理解できれば,JavaのAPI仕様を読めるようになる.Google等で下手に検索するよりも,公式のAPI仕様を読んだほうが問題解決の近道となる場合が多い.ドキュメントを読む上手さもプログラミングの必須能力だと心がけよ.
以下はjava.nio.file.Files#delete()
のAPI仕様である.
delete
#ファイルを削除します。
(略)
ファイルがディレクトリの場合、ディレクトリは空である必要があります。 (略)
パラメータ:#
path
- 削除するファイルへのパス例外:#
NoSuchFileException
- ファイルが存在しない場合(オプションの固有例外)DirectoryNotEmptyException
- ファイルがディレクトリで、ディレクトリが空でないために削除できなかった場合(オプションの固有例外)- (略)
Quiz
おそらく最も目にする例外はNullPointerException(NPE)である.null
要素に対してメソッド呼び出しなどの操作を行うと発生する例外である.
String s = null;
s.toUpperCase(); // NPE発生
Stack stack = null;
stack.pop(); // NPE発生
List l = null;
l.size(); // NPE発生
ERROR!
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.toUpperCase()" because "s" is null
at Main.main(Main.java:8)
NullPointerExceptionは検査例外か?非検査例外か?
Quiz
以下6種類のメソッドについて,コンパイルエラーとなるメソッドはどれか考えよ.Exceptionは検査例外,RuntimeExceptionは非検査例外である.
void f1a() {
throw new Exception();
}
void f1b() {
throw new RuntimeException();
}
void f2a() throws Exception {
throw new Exception();
}
void f2b() throws RuntimeException {
throw new RuntimeException();
}
void f3a() {
try {
throw new Exception();
} catch(Exception e) {}
}
void f3b() {
try {
throw new RuntimeException();
} catch(RuntimeException e) {}
}
Answer
f1a()
のみコンパイルできない.検査例外を検査していないためである.
モダンな言語#
ここまで取り上げなかったモダンな言語について紹介する.言語の選定と比較基準はまつ本の主観が多分に含まれている点に注意せよ.
あらかじめ本節の要点をまとめておく.
- 2000年以降も新たな言語が登場している
- 最強の言語は存在しない・一つの言語のみを習得するだけでは不十分である
- 一つの言語を習得できれば他の言語の習得は比較的容易である
- 言語ごとの得意不得意がある・状況に応じて様々な言語を使い分ける必要がある
- 様々な便利な言語機能は登場しているが,本質は大きく変化していない
比較表#
プログラミング言語の歴史
©1
言語 | 開発者 | 登場 | 実行形式 | 型システム | メモリ | 補足(主観含む) | 言語 | |
---|---|---|---|---|---|---|---|---|
Go | 2009 | コンパイル | 静的+動的 | GC | シンプルさ/並行処理 | Go | ||
Swift | Apple | 2014 | コンパイル | 静的+動的 | GC | iOS/macOSアプリ | Swift | |
Haskell | P.Hudakら | 1990 | コンパイル | 静的+動的 | GC | 純粋関数型/実用的 | Haskell | |
Python | G.Rossum | 1991 | インタプリタ | 動的+静的 | GC | 機械学習/スクリプト言語 | Python | |
Rust | Mozilla | 2010 | コンパイル | 静的+動的 | 非GC | 学習コスト高/野心的 | Rust | |
JavaScript | Netscape | 1995 | インタプリタ | 動的 | GC | Webフロントエンド | JavaScript | |
TypeScript | Microsoft | 2012 | コンパイラ | 静的+動的 | GC | JSのスーパーセット言語 | TypeScript |
各種言語による階乗計算#
Note
GoやSwiftでは型宣言は変数名の後ろに記述する.
GoやSwiftは静的型付け言語ではあるが,型推論もサポートしている.言い換えると,型宣言はオプショナルな要素だといえる.オプショナルな部分を後ろに配置することで,型を宣言する場合も省略する場合も一貫した記述が可能になる.結果的に人による可読性や,コンパイラによる解析の容易さを確保している.
Note
PythonやSwiftでは名前付き引数(named arguments,キーワード引数)をサポートしている.実引数と仮引数の紐づけを,引数の順序ではなく名前で指定できる.
JSフレームワーク#
Angular Vue.js React Svelteなど
- JavaScriptフレームワーク(プログラミング言語ではない)
- Webフロントエンド開発(HTML+CSS+JS)のための仕組み
文字列が英数字のみかをチェックする例:
<input type="text">
<p class="msg"></p>
<p class="error" style="display:none">Invalid uid!!</p>
<script>
const validateUid = function(e) {
const uid = e.target.value;
document.querySelector('.msg').innerText = uid;
if (!uid.match(/^[a-zA-Z0-9]*$/)) {
document.querySelector('.error').style.display = null;
} else {
document.querySelector('.error').style.display = 'none';
}
}
const elem = document.querySelector('input');
elem.addEventListener('input', validateUid);
</script>
<input bind:value={uid} on:input={validateUid}>
<p>{uid}</p>
{#if isInvalid}
<p>Invalid uid!!</p>
{/if}
<script>
let uid = '';
let isInvalid = false;
let validateUid = function() {
isInvalid = !uid.match(/^[a-zA-Z0-9]*$/);
}
</script>
ゼミA楠本研でSvelteをやります.
《宿題》#
Homework
今日は宿題はありません.試験勉強をがんばってください.
期末試験の範囲は以下のとおりです.