コンテンツにスキップ

その他の言語#

教科書該当なし プログラミング言語

本ページでは例外処理,及びここまで取り上げなかったモダンな言語について紹介する.

例外処理#

例外処理は多くのプログラミング言語で採用される基本的な機能であり,実践的なプログラミングにおいて必須知識である.特にJavaでは検査例外が存在しているため,例外を知らずにJavaを使いこなすことはできない.

また例外の理解はデバッグの効率化にも繋がる.例外が発生した際のスタックトレースを理解できれば,エラー発生の原因となった箇所を極めて効率的に絞り込むことができる.

本設ではまず例外処理が必要となる状況を共有し,メソッドの提供者側利用者側の2つの観点から例外処理の使い方を説明する.

例外の必要性#

Javaによるスタックの実装を例題に,例外の必要性を考える.

Javaによるスタック実装
public class Main {
    public static void main(String[] args) {
        ArrayStack stack = new ArrayStack();  // 自前のスタック

        stack.push(1);
        stack.push(2);
        System.out.println(stack);  // [1, 2]

        stack.pop();
        stack.pop();
        System.out.println(stack);  // []

        stack.pop();  // ★この時ArrayStackはどう振る舞うべき?
    }
}
Javaによるスタック実装
public class ArrayStack {
    private int sp;
    private int[] elements;
    public ArrayStack() {
        this.sp = 0;
        this.elements = new int[10];
    }
    public void push(int element) {
        elements[sp++] = element;
    }
    public int pop() {
        // ★過剰なpop()の際にどう振る舞うべきか?
        return elements[--sp];
    }
    ...
}

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:何も返さない#

public int pop() {
    if (sp - 1 <= 0) return;
    return elements[--sp];
}

👎:そもそもコンパイルできない.int値を返しておらずint pop()というシグネチャのルールに従っていない.

対策2:強制終了する#

public int pop() {
    if (sp - 1 <= 0) System.exit(1);
    return elements[--sp];
}

👎:危険すぎる.利用者が誤って過剰pop()をしてしまうとプログラム全体が強制終了する.配列外参照によるクラッシュとほぼ同様の振る舞いであり,問題は何一つ改善されていない.

対策3:特殊な値を返す#

public int pop() {
    if (sp - 1 <= 0) return -1;
    return elements[--sp];
}

👎:-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).

  1. 正常時はreturnで,異常時はthrowで値を返す,と解釈しても良い.

throw対象であるnew EmptyStackException()new節を用いた単なるオブジェクトのインスタンス化である.Javaはオブジェクト指向言語であり,例外そのものもクラスとして表現されている.throw時には例外をインスタンス化して投げる(1).

  1. throw対象のクラスはjava.lang.Throwableクラスを継承している必要がある.

今回の例では,java.utilパッケージのEmptyStackExceptionクラスを投げている.Javaには様々な例外が用意されており,エラーの状況を表す適切な名前の例外クラスを探すと良い.スタック利用者が例外の名前だけを見て,何が起きているのか予想可能な例外クラスを選ぶ.

メソッド内でthrowする際には,必ずシグネチャにthrowsを加え,どのような例外を発生させ得るのかを宣言する必要がある.return節の返り値がint型であることをint pop()のように宣言するのと同じ理由である.シグネチャとはメソッドの仕様である(1).

  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の関係が成り立つ.

EmptyStackException e = new EmptyStackException(); // 普通の宣言
Exception e = new EmptyStackException(); // 親クラスで受けてもOK
Object e = new EmptyStackException(); // さらに親で受けてもOK

String s = new String("a"); // 普通の宣言
Object o = new String("a"); // 親クラスで受けてもOK

例外の処理方法:利用者側#

利用者側ではtry catch節によって例外を掴む必要がある.

try {
    stack.pop();
} catch (EmptyStackException e) {
    ...  // 例外発生時の処理を追加
}

例外を発生させ得るメソッドpop()の呼び出しは,必ずtry { ... }ブロックで囲む必要がある.tryは「失敗するかもしれない処理を試す」というニュアンスである.もしpop()内で例外が投げられた場合,catch() { ... }側に処理が移る.「発生した例外を掴む」というニュアンスである.提供者側がthrow節で投げた例外インスタンスはcatch(例外の型 e)の変数eとして受け取ることができる.

例外発生時にcatch内でどのような処理を行うべきかは状況によって異なる.回復不能な致命的な状況であれば強制終了catch (..) { System.exit(1); }とすることもある.あるいは過剰pop()を無視して良ければcatch (..) {}としても良い.これを例外を握りつぶすとも言う.

例外クラスはprintStackTrace()というメソッドを必ず持っている(継承している).例外が発生した瞬間のメソッドの呼び出し系列(1)を標準出力に書き出すメソッドである.例外発生の状況把握に役立つため,ひとまずcatch後にはe.printStacktrace()してみると良い.

  1. スタックフレームの情報のこと.
try {
    stack.pop();
} catch (EmptyStackException e) {
    e.printStackTrace();  // よくある(一時的な)例外処理
}

標準出力には以下のような内容が書き込まれる.日常的によく見るスタックトレースである.

java.util.EmptyStackException    //  ← 発生した例外の名前
        at jp.osakau.ArrayStack.pop(ArrayStack.java:18)
        at jp.osakau.Main.main(Main.java:13)
 //            ↗      ↗    ↑             ↖
 //  パッケージ名  クラス名  メソッド名   ファイル名:行番号

これまで説明した例外処理を適用済みのMainクラスとArrayStackクラスを以下に示す.

Javaによるスタック実装(例外処理あり)
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によるスタック実装(例外処理あり)
public class ArrayStack {
    private int sp;
    private int[] elements;
    ...

    public int pop() throws EmptyStackException {
        if (sp - 1 <= 0) {
            throw new EmptyStackException();
        }
        return elements[--sp];
    }

検査例外と非検査例外#

Javaには検査例外非検査例外の2種類の例外がある.検査例外は検査が必須となる例外である.検査していない場合はコンパイルエラーとなる.

検査例外を発生させるメソッドを呼び出す際は,try catchで例外を直接検査するか,throws宣言を加えて例外をさらに親メソッドへ投げる必要がある.他方,非検査例外は例外の検査がオプショナルであり,利用者側で例外の発生を無視することができる(1).

  1. 先の例のEmptyStackExceptionクラスは非検査例外である. つまりMainクラス側でのtry catchを省略してもコンパイル・実行が可能である.

Javaではファイル周りの操作で検査例外が多数設けられている.よってJavaによるファイル操作はプログラムの記述量が多くなりがちである(1).

  1. 他の言語と比較して厳格だといえる.
Javaによるファイル削除
try {
    Files.delete(Paths.get("tmp.txt"));
} catch (NoSuchFileException e) { // fileがないとき(検査例外)
    ...
} catch (DirectoryNotEmptyException e) {// dirが空でないとき(検査例外)
    ...
}

スレッド周りの処理も検査例外が多い.

Javaによるsleep
try {
    Thread.sleep(1000);
} catch (InterruptedException e) { // スレッドが割り込まれたら
    ..
}

Pythonは全て非検査例外であり検査例外が存在しない.つまり利用者側による例外検査は必須ではない.

Pythonによるファイル削除
import os
os.remove("tmp.txt")
Pythonによるファイル削除(必要なら例外検査をしても良い)
import os
try:
    os.remove("tmp.txt")
except FileNotFoundError:
    ...

Quiz

以下のプログラムを実行したときに,f2()が生成し投げた例外がf1()でどう扱われるかを考えよ.Exceptionクラスは検査例外である.

クイズ
public static void main(String[] args) {
    try {
        f1();
    } catch (Exception e) {
        e.printStackTrace();
    }
}
public static void f1() throws Exception {
    f2(); // どうなる?
}
public static void f2() throws Exception {
    throw new Exception();
}
Answer

f1()f2()が投げた例外をそのまま元メソッドmain()に投げる(スルーする).

出力は以下の通りである.スタックトレースが確認できる.

java.lang.Exception:
    at Main.f2(Main.java:14)
    at Main.f1(Main.java:11)
    at Main.main(Main.java:5)

Quiz

以下のプログラムを実行したときに何が起こるか考えよ.

クイズ
public static void main(String[] args) throws Exception {
    f1();
}
public static void f1() throws Exception {
    f2();
}
public static void f2() throws Exception {
    throw new Exception();
}
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を用いて適切に処理することを推奨する.

例外が発生しても回復可能なケースが多々ある.例えば,ファイル削除の際には「指定したファイルが存在しない」状況を保証できれば良い場合がある.この場合,以下のように例外を処理すれば良い.

try {
    Files.delete(Paths.get("tmp.txt"));
} catch (NoSuchFileException e) {} // 例外を握りつぶす(無視する)
                                   // ファイルが存在しなければ
                                   // 削除失敗は無視してOK

API仕様の読み方#

OO言語と上記例外の内容を理解できれば,JavaのAPI仕様を読めるようになる.Google等で下手に検索するよりも,公式のAPI仕様を読んだほうが問題解決の近道となる場合が多い.ドキュメントを読む上手さもプログラミングの必須能力だと心がけよ.

以下はjava.nio.file.Files#delete()のAPI仕様である.

delete#

public static void delete(Path path) throws IOException

ファイルを削除します。

(略)

ファイルがディレクトリの場合、ディレクトリは空である必要があります。 (略)

パラメータ:#
  • 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は検査例外か?非検査例外か?

Answer

非検査例外である.もしNPEが検査例外だとしたら,Javaのあらゆるメソッド呼び出しでtry catchが必要になってしまう.

try {
    s.toUpperCase();
} catch (NullPointerException e) {
    ...
}
try {
    System.out.println("Hello World");
} catch (NullPointerException e) {
    ...
}

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 Google 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

各種言語による階乗計算#

package main

import "fmt"

func factorial(n int) int {
    if n <= 1 {
        return 1
    }
    return n * factorial(n - 1)
}
func main() {
    var ans int = factorial(5)
    fmt.Println(ans) // 120
}
func factorial(n: Int) -> Int {
  if n <= 1 {
    return 1
  }
  return n * factorial(n: n - 1)
}

let ans: Int = factorial(n: 5)  // 120
print(ans)
factorial n = if n <= 0
              then 1
              else n * factorial(n - 1)
main = print (factorial 5)
def factorial(n):
    if n <= 1:
        return 1
    return n * factorial(n - 1)

ans = factorial(5)
print(ans)  # 120
fn factorial(n : i32) -> i32 {
    if n <= 1 {
        return 1;
    }
    return n * factorial(n - 1);
}

fn main() {
    let ans = factorial(5);  // 120
    println!("{}", ans);
}
function factorial(n) {
    if (n <= 1) {
        return 1;
    }
    return n * factorial(n - 1);
}

ans = factorial(5);
console.log(ans);  // 120
function factorial(n: number) : number {
    if (n <= 1) {
        return 1
    }
    return n * factorial(n - 1)
}

let ans: number = factorial(5)
console.log(ans)  // 120

Note

GoやSwiftでは型宣言は変数名の後ろに記述する.

Java
int i = 10;
String s = "hi";
Go
var i int = 10;
var s string = "hi";

GoやSwiftは静的型付け言語ではあるが,型推論もサポートしている.言い換えると,型宣言はオプショナルな要素だといえる.オプショナルな部分を後ろに配置することで,型を宣言する場合も省略する場合も一貫した記述が可能になる.結果的に人による可読性や,コンパイラによる解析の容易さを確保している.

参考:モダンなプログラミング言語ではなぜ型宣言が変数名の後ろにあるのか?

Note

PythonやSwiftでは名前付き引数(named arguments,キーワード引数)をサポートしている.実引数と仮引数の紐づけを,引数の順序ではなく名前で指定できる.

Python
def create_user(name, age):  # 関数の宣言
    ...

create_user("shinsuke", 42)          # 順序で引数を紐づけ
create_user(name="shinsuke", age=42) # 名前付き引数
create_user(age=42, name="shinsuke") # 名前があるので順序を変えてもOK

JSフレームワーク#

Angular Vue.js React Svelteなど

  • JavaScriptフレームワーク(プログラミング言語ではない)
  • Webフロントエンド開発(HTML+CSS+JS)のための仕組み

文字列が英数字のみかをチェックする例:

PureJS

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>

Svelte

JSフレームワークあり(Svelte)
<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

今日は宿題はありません.試験勉強をがんばってください.

期末試験の範囲は以下のとおりです.

  • 論理型言語
    • 中間試験とほぼ同じ形式です
  • C言語
    • 配列副作用を持つ単項演算子は含めない
    • SCコードの暗記は不要
    • C言語プログラムがスタック計算機の上でどのように動いているかを理解すると良い
  • Java言語
    • GCは含めない
    • C言語と同様,JVMコードの暗記は不要
    • 特にメソッド呼び出し周りの挙動を理解しておくこと
    • ヒープとスタックを理解しておくこと
  • その他の言語の例外処理
    • モダンな言語は含めない
    • Javaを題材に例外の問題を出します
    • 検査例外と非検査例外の違いを理解しておくこと
  • SCコードとバイトコード一覧を補足資料として配布する