Rust プログラミング 入門

Rust完全ガイド 第4回 エラー処理と型

Rustのプログラミングにおいて、エラーハンドリングコレクション型 は、安全で効率的なコードを書くために欠かせない重要な概念です。
Rustは ゼロコスト抽象化 を目指しながらも、エラー処理の安全性を高める設計 を取り入れており、Result<T, E>Option<T> などの仕組みを活用することで、実行時エラーを未然に防ぐことができます。

また、データの管理には コレクション型(Vec<T>HashMap<K, V>HashSet<T> など) が不可欠です。
可変長のリストやキーと値のマッピングを扱うことで、より柔軟なデータ処理が可能になります。

本記事では、Rustにおける エラーハンドリングの基礎主要なコレクション型の使い方 について詳しく解説し、実践的なコードを通じて理解を深めていきます。
Rustの安全性とパフォーマンスを最大限に活かしたプログラムを作成するために、これらの概念をしっかりと習得しましょう!

エラーハンドリング

プログラムを開発する上で、エラーハンドリング(エラー処理) は非常に重要です。
エラーを適切に処理することで、プログラムの予期しないクラッシュを防ぎ、安全で信頼性の高いコードを実現 できます。

Rustは、エラー処理を明示的に行うことを推奨する設計 になっています。
panic! マクロを使ってプログラムをクラッシュさせる方法もありますが、通常は Result<T, E> を活用した「回復可能なエラー処理」 を行うことが一般的です。

この章では、Rustのエラー処理の基本、panic! マクロとリカバリー戦略、Result<T, E> の活用方法 について詳しく学んでいきます。

Rustのエラー処理モデル

Rustのエラー処理は、大きく分けて 2種類 あります。

エラーの種類説明
回復不能なエラー(panic!修復が難しく、プログラムをクラッシュさせるべきエラーバッファオーバーフロー、配列の範囲外アクセス
回復可能なエラー(Result<T, E>適切に処理すれば、プログラムの継続が可能なエラーファイルが見つからない、無効なユーザー入力

回復不能なエラー(panic!)とは?

回復不能なエラーとは、プログラムの継続が不可能で、即座にクラッシュさせるべきエラー です。
Rustでは、こうしたエラーが発生すると スタックトレース(エラーの履歴) が出力され、プログラムが終了します。

例えば、配列の範囲外アクセス をすると、Rustは panic! を発生させます。

fn main() {
    let numbers = vec![1, 2, 3];
    println!("{}", numbers[5]); // パニック発生!
}

このコードを実行すると、次のようなエラーメッセージが表示されます。

thread 'main' panicked at 'index out of bounds: the len is 3 but the index is 5'

これは 回復不能なエラー であり、プログラムは強制終了されます。
しかし、エラーを事前に回避することで、このようなクラッシュを防ぐことができます。

panic!マクロとリカバリー戦略

panic! マクロの使い方

Rustでは、明示的に panic! を使ってプログラムをクラッシュ させることができます。

fn main() {
    panic!("重大なエラーが発生しました!");
}

このコードを実行すると、プログラムは即座にクラッシュし、次のようなメッセージが出力されます。

thread 'main' panicked at '重大なエラーが発生しました!'

リカバリー戦略

panic! を使うのではなく、適切にエラーを処理することでプログラムの継続を可能にする ことが重要です。
Rustでは、Result<T, E> 型を使うことで、エラーを適切に処理 できます。

例えば、ファイルの読み込み時にエラーが発生する可能性を考えてみましょう。

use std::fs::File;

fn main() {
    let file = File::open("not_found.txt");

    match file {
        Ok(_) => println!("ファイルを開きました"),
        Err(e) => println!("エラー: {}", e),
    }
}
  • Ok(_) の場合は、ファイルが正常に開かれたことを示す。
  • Err(e) の場合は、発生したエラーの詳細を表示する。

このように match を使うことで、プログラムのクラッシュを防ぎながら適切にエラーを処理 できます。

Result<T, E>の活用方法

Rustでは、エラーを処理するために Result<T, E> 型を活用します。
これは 成功時には Ok(T) を、エラー時には Err(E) を返す 型です。

Result<T, E> の基本的な使い方

use std::fs::File;

fn open_file() -> Result {
    File::open("hello.txt")
}

fn main() {
    match open_file() {
        Ok(file) => println!("ファイルを開きました: {:?}", file),
        Err(e) => println!("エラー発生: {}", e),
    }
}

エラー処理のパターン

Result<T, E> を処理する方法はいくつかあります。

方法説明
match明示的に OkErr の両方を処理するmatch result { Ok(v) => ..., Err(e) => ... }
unwrap()Ok の場合は値を取得、Err の場合は panic!result.unwrap()
expect()unwrap() + カスタムエラーメッセージresult.expect("エラー発生")
? 演算子エラーを自動で伝播let file = File::open("file.txt")?;

unwrap()expect() の違い

unwrap() はエラーが発生すると panic! を発生させます。

let file = File::open("not_found.txt").unwrap(); // エラー時にパニック

一方、expect() は、エラーメッセージを指定できます。

let file = File::open("not_found.txt").expect("ファイルを開けませんでした");

? 演算子を使ったエラーの伝播

エラーを呼び出し元に自動で伝播するには ? 演算子を使います。

use std::fs::File;
use std::io::Error;

fn open_file() -> Result {
    let file = File::open("hello.txt")?; // エラーが発生したら自動で返す
    Ok(file)
}

この ? 演算子を使うと、エラーハンドリングを簡潔に記述できます。

まとめ

  • Rustのエラー処理は panic!(回復不能)と Result<T, E>(回復可能)の2種類に分かれる。
  • panic! は緊急時にプログラムをクラッシュさせるために使用するが、通常は Result<T, E> を活用して回復可能なエラー処理を行う。
  • match を使うことで、安全にエラーを処理できる。
  • unwrap()expect() は簡易的なエラーハンドリングとして利用できるが、安全性を考慮するなら ? 演算子を活用するのが推奨される。

これで Rustのエラーハンドリングの基本 を学びました。
次は、より複雑なデータ管理を行う コレクション型 について学び、Rustの強力なデータ構造を活用できるようになりましょう。

コレクション型

プログラムでは、データを効率的に管理・操作するためにコレクション型を活用します。
Rustでは、リスト・マップ・セットなどの異なるコレクション型が標準ライブラリで提供されており、用途に応じて適切なデータ構造を選択することが重要 です。

特に、キーと値のペアを扱う HashMap<K, V>、重複を許さない HashSet<T>、順序付きのマップ BTreeMap<K, V> は、プログラムのパフォーマンスやデータ管理の効率を向上させるために重要な役割を果たします。

この章では、HashMap<K, V> の基本操作、HashSet<T> の使い方、BTreeMap<K, V> との比較 を詳しく学んでいきます。

HashMap<K, V> の基本操作

HashMap<K, V> とは?

HashMap<K, V>キー(K)と値(V)のペアを管理するデータ構造 です。
キーを指定して値にアクセスできるため、検索やデータの関連付けを高速に処理することができます。

HashMap の作成と要素の追加

Rustでは、HashMap を使うために std::collections::HashMap をインポートする必要があります。

use std::collections::HashMap;

fn main() {
    let mut scores = HashMap::new();

    scores.insert("Alice", 80);
    scores.insert("Bob", 90);
    scores.insert("Charlie", 85);

    println!("{:?}", scores); // {"Alice": 80, "Bob": 90, "Charlie": 85}
}
  • HashMap::new() で空のハッシュマップを作成する。
  • insert(key, value) で要素を追加する。

要素の取得

get() メソッドを使うと、キーに対応する値を取得 できます。

fn main() {
    let mut scores = HashMap::new();
    scores.insert("Alice", 80);
    
    match scores.get("Alice") {
        Some(score) => println!("Aliceのスコア: {}", score),
        None => println!("スコアが見つかりません"),
    }
}
  • get(key) を使うと Option<&V> を返すため、Some() / None で処理する。
  • キーが存在しない場合は None となるため、エラー処理を忘れないことが重要。

要素の更新

特定のキーの値を変更する場合は、insert() を使う ことで上書きできます。

fn main() {
    let mut scores = HashMap::new();
    scores.insert("Alice", 80);
    
    scores.insert("Alice", 95); // 上書き
    
    println!("{:?}", scores); // {"Alice": 95}
}
  • すでに存在するキーに対して insert() を呼ぶと、値が上書きされる。

特定のキーが存在しない場合にのみ値を設定したい場合は、entry() を使います。

fn main() {
    let mut scores = HashMap::new();
    scores.insert("Alice", 80);

    scores.entry("Alice").or_insert(90);
    scores.entry("Bob").or_insert(85);

    println!("{:?}", scores); // {"Alice": 80, "Bob": 85}
}
  • entry("Alice").or_insert(90) は、Alice のスコアがすでに存在するため、変更されない。
  • entry("Bob").or_insert(85) は、新しいエントリを追加する。

HashSet<T> の使い方

HashSet<T> とは?

HashSet<T> は、一意な要素を管理するデータ構造 です。
リストと異なり、同じ値を2回追加しても1つしか保存されません。

HashSet の作成と要素の追加

use std::collections::HashSet;

fn main() {
    let mut numbers = HashSet::new();

    numbers.insert(10);
    numbers.insert(20);
    numbers.insert(30);
    numbers.insert(10); // 重複しているため追加されない

    println!("{:?}", numbers); // {10, 20, 30}
}
  • HashSet::new() で空のセットを作成する。
  • insert(value) で要素を追加するが、重複は許可されない

要素の存在確認

要素が HashSet に含まれているかを確認するには contains() を使います。

fn main() {
    let mut numbers = HashSet::new();
    numbers.insert(10);
    
    if numbers.contains(&10) {
        println!("10 はセットに含まれています");
    }
}
  • contains(&value)bool を返すため、条件分岐に利用可能。

要素の削除

fn main() {
    let mut numbers = HashSet::new();
    numbers.insert(10);
    
    if numbers.contains(&10) {
        println!("10 はセットに含まれています");
    }
}

BTreeMap<K, V>との比較

HashMap<K, V>BTreeMap<K, V> の主な違いは、キーの順序が保持されるかどうか です。

データ構造特徴使いどころ
HashMap<K, V>キーの順序なし、高速な検索ランダムなキーアクセスが多い場合
BTreeMap<K, V>キーが順序付きで管理されるソートされたデータが必要な場合

BTreeMap の使い方

use std::collections::BTreeMap;

fn main() {
    let mut map = BTreeMap::new();
    map.insert(3, "Three");
    map.insert(1, "One");
    map.insert(2, "Two");

    println!("{:?}", map); // {1: "One", 2: "Two", 3: "Three"}
}
  • BTreeMap はキーの順序を保持するため、ソートされた状態でデータを取得できる。

まとめ

  • HashMap<K, V> はキーと値のペアを管理し、データの検索や関連付けに適している。
  • HashSet<T> は重複を許さない集合を管理し、要素の存在確認や一意性の確保に役立つ。
  • BTreeMap<K, V>HashMap<K, V> と異なり、キーが順序付きで管理されるため、ソートが必要な場合に便利。

これで 第4回「エラー処理と型」 の学習が完了しました。
次回の 第5回では「高度なRustの機能」 について学びます。
ジェネリクスやトレイト、ライフタイムなど、Rustの強力な型システムを活かしたプログラミング手法を深掘りし、より柔軟で拡張性のあるコードを書くための知識を身につけましょう!

  • この記事を書いた人

ふくまる

機械設計業をしていたが25歳でエンジニアになると決意して行動開始→ 26歳でエンジニアに転職→ 28歳でフリーランスエンジニアに→ 現在、34歳でフリーランス7年目 Go案件を受注中 Go,GCPが得意分野

-Rust, プログラミング, 入門