はい、承知いたしました。Rustの標準ライブラリ(std)について、初心者向けに基本から徹底解説する約5000語の詳細な記事を作成します。
以下に記事の内容を直接表示します。
Rust入門:標準ライブラリ(std)とは?基本を徹底解説
Rustプログラミング言語の世界へようこそ!Rustは、その安全性、速度、並行性の高さから、システムプログラミング、Web開発(Wasm)、組み込みシステムなど、幅広い分野で注目されています。Rustの強力な機能の多くは、言語そのものだけでなく、その標準ライブラリ(Standard Library、略称 std
)によって提供されています。
この記事では、Rustを学び始めた方が「標準ライブラリって何?」「どうやって使うの?」「どんな機能があるの?」といった疑問を解消できるよう、std
ライブラリの基本から主要なモジュール、使い方までを徹底的に解説します。この記事を読み終える頃には、std
がRustプログラミングにおいていかに不可欠で強力な存在であるかを理解し、日々のコーディングで自信を持って活用できるようになっているはずです。
さあ、Rustの標準ライブラリの魅力的な世界を探検し始めましょう!
1. 標準ライブラリ(std)とは?Rustにおけるその位置づけ
1.1 std
の定義と役割
Rustの標準ライブラリ (std
) は、Rust言語に付属する基本的な機能やデータ構造、I/O操作、並行処理ツールなどをまとめたライブラリです。あなたが普段Rustで書くコードのほとんどは、このstd
ライブラリの機能に依存しています。
std
の主な役割は以下の通りです。
- 基本的なデータ構造の提供: 動的配列 (
Vec
)、文字列 (String
)、ハッシュマップ (HashMap
) といった、あらゆるプログラミングで必要とされるデータ構造を提供します。 - 入出力(I/O)機能: ファイルの読み書き、標準入力/標準出力へのアクセスなど、プログラムが外部とやり取りするための機能を提供します。
- 並行処理と並列処理: スレッドの作成、スレッド間の通信、共有メモリの安全な管理(Mutex, RwLock, Channelなど)といった、現代的なアプリケーション開発に不可欠な機能を提供します。
- ネットワーキング: TCPやUDPといった基本的なネットワーク通信機能を提供します。
- その他ユーティリティ: 時間の扱い、環境変数へのアクセス、コマンド実行など、様々な便利な機能を提供します。
std
は、Rustの強力な型システム、所有権システム、借用チェッカーといった言語機能と密接に連携しており、これらの機能を生かした安全で効率的なコードを書くための基盤となります。
1.2 std
, alloc
, core
の関係性
Rustには標準ライブラリと呼ばれるものがいくつかあり、初心者には少し混乱しやすい部分かもしれません。主なものは以下の3つです。
core
: Rust言語で書かれた最も基本的なライブラリです。ヒープアロケータやOSに依存しない、言語のコア機能(プリミティブ型、演算、Option/Result、イテレータトレイトなど)を含みます。no_std
環境(OSがない組み込みシステムなど)でも使用できます。alloc
: ヒープメモリのアロケーション(動的なメモリ確保)に関する機能を提供します。Box<T>
,Vec<T>
,String
といった型は、内部でalloc
の機能を使用しています。no_std
環境でも、アロケータが利用可能であればalloc
を使用できます(#![feature(alloc)]
やカスタムアロケータが必要になることもあります)。std
:core
とalloc
の上に構築された、最も包括的な標準ライブラリです。ファイルI/O、ネットワーキング、スレッド、OS連携など、一般的なオペレーティングシステム上で動作するアプリケーションに必要な機能のほとんどを含みます。std
はcore
とalloc
の両方に依存しています。
普段私たちがデスクトップやサーバー上でRustプログラムを書く際には、デフォルトでstd
が使用されます。これは、std
が提供する豊富な機能が開発効率を大幅に向上させるためです。組み込み開発など、特殊な環境でstd
を使わない場合は、明示的に#![no_std]
アトリビュートを記述してcore
やalloc
のみを使用します。
この記事では、主に一般的なRust開発で使用されるstd
に焦点を当てて解説します。
1.3 std
はクレートである
Rustにおいて、再利用可能なコードの単位はクレート(crate)と呼ばれます。実行可能なプログラムを生成するバイナリクレートと、他のクレートから利用されるライブラリを提供するライブラリクレートがあります。
std
もまた、Rustコンパイラに付属する特別なライブラリクレートです。一般的なクレートはCargo.toml
で依存関係として明示的に指定する必要がありますが、std
クレートはほとんどのプロジェクトでデフォルトで自動的にリンクされます。
つまり、あなたは特に何も設定しなくても、std
が提供する機能 (Vec
, println!
, File
など) をコード中で直接使うことができるのです。
2. stdライブラリの主要なモジュールと機能
std
ライブラリは、機能ごとにいくつかのモジュールに分割されています。それぞれのモジュールが特定の目的のための機能群を提供しています。ここでは、初心者の方が特によく使うであろう主要なモジュールと、そこに属する機能について詳しく見ていきましょう。
2.1 std::vec
(動的配列)
std::vec
モジュールは、最も基本的なデータ構造の一つである動的配列 Vec<T>
を提供します。Vec<T>
は、実行時にサイズを変更できる要素のシーケンス(リスト)です。他の言語の「ArrayList」や「Vector」に相当します。
-
特徴:
- ヒープ上にデータを格納し、必要に応じてメモリを再割り当てしてサイズを拡張します。
- 末尾への要素の追加(push)は効率的です(通常償却O(1))。
- インデックスを使った要素へのアクセスは高速です(O(1))。
- 途中への挿入/削除は、その位置より後ろの要素を移動させる必要があるため、コストが高くなる場合があります(O(n))。
- 要素はメモリ上で連続して配置されます。
-
基本的な使い方:
“`rust
use std::vec::Vec; // 明示的なuseは必須ではないが、理解のために記述
fn main() {
// 新しい空のVecを作成
let mut my_vector: Vec
// 要素を追加
my_vector.push(10);
my_vector.push(20);
my_vector.push(30);
println!("Vec: {:?}", my_vector); // 出力: Vec: [10, 20, 30]
// 要素数を確認
println!("Size: {}", my_vector.len()); // 出力: Size: 3
// インデックスで要素にアクセス (Result<T, E>を返すget()を使うのが安全)
match my_vector.get(1) {
Some(value) => println!("Element at index 1: {}", value), // 出力: Element at index 1: 20
None => println!("Index out of bounds"),
}
// インデックスで要素にアクセス (パニックする可能性がある直接アクセス)
// let second_element = my_vector[1]; // これは20
// println!("Second element: {}", second_element);
// 要素を削除 (末尾から)
let last_element = my_vector.pop(); // Option<i32>を返す
println!("Popped element: {:?}", last_element); // 出力: Popped element: Some(30)
println!("Vec after pop: {:?}", my_vector); // 出力: Vec after pop: [10, 20]
// Vec!マクロを使った初期化
let mut another_vector = vec![100, 200, 300];
println!("Another Vec: {:?}", another_vector); // 出力: Another Vec: [100, 200, 300]
// 要素の変更
if let Some(elem) = another_vector.get_mut(1) {
*elem = 250;
}
println!("Another Vec after modification: {:?}", another_vector); // 出力: Another Vec after modification: [100, 250, 300]
// イテレーション
println!("Iterating through the vector:");
for element in &another_vector {
println!("{}", element);
}
}
“`
Vec<T>
は非常に汎用性が高く、Rustで最も頻繁に使用されるコレクション型の一つです。
2.2 std::string
(String
とstr
)
Rustにおける文字列の扱いは、初心者にとって少し複雑に感じられるかもしれません。std::string
モジュールは、ヒープに確保される所有権を持つ文字列型 String
を提供します。これに対し、&str
は文字列スライスと呼ばれ、文字列データへの参照です。
-
String
:- UTF-8エンコードされた、成長可能な文字列データです。
- データはヒープに格納されます。
- 所有権を持ちます。変更可能です (
mut String
)。
-
&str
(文字列スライス):String
や文字列リテラル(バイナリに埋め込まれた静的な文字列)の一部への参照です。- 固定長(実行時にサイズが変わらない参照先)です。
- 変更不可能です(参照先は)。
- 文字列リテラル
"hello"
の型は&'static str
です。
-
基本的な使い方:
“`rust
use std::string::String; // 明示的なuseは必須ではないが、理解のために記述
fn main() {
// 文字列リテラル (&str)
let s1: &str = “hello, world!”; // 静的な文字列スライス
// 新しい空のStringを作成
let mut s2 = String::new();
// Stringに文字列を追加
s2.push_str("hello"); // &strを追加
s2.push(','); // charを追加
s2.push(' ');
// format!マクロでStringを作成
let s3 = format!("{}world!", s2); // s3はString型
println!("s1: {}", s1); // 出力: s1: hello, world!
println!("s2: {}", s2); // 出力: s2: hello,
println!("s3: {}", s3); // 出力: s3: hello, world!
// Stringと&strの相互変換 (String -> &str)
let s4 = String::from("Rust is great");
let s4_slice: &str = &s4; // String全体へのスライス
println!("s4_slice: {}", s4_slice);
// Stringと&strの相互変換 (&str -> String)
let s5_str: &str = "convert me";
let s5_string: String = s5_str.to_string(); // または String::from(s5_str)
println!("s5_string: {}", s5_string);
// 文字列の連結 (Stringは+演算子で&strまたはStringを連結可能)
let s6 = s5_string + " to String"; // s5_stringの所有権はムーブされる!
// println!("s5_string after concatenation: {}", s5_string); // エラー: s5_stringはムーブ済み
println!("s6: {}", s6); // 出力: s6: convert me to String
// 文字列の長さ (バイト数)
println!("Length of s6 (bytes): {}", s6.len()); // UTF-8のバイト数
// 文字列のイテレーション (char単位)
println!("Characters in s6:");
for c in s6.chars() {
println!("{}", c);
}
// 部分文字列 (スライス)
let hello = &s6[0..7]; // 0から7バイト目まで (UTF-8文字境界に注意!)
println!("Slice of s6: {}", hello); // 出力: Slice of s6: convert
// replaceメソッド
let replaced = s6.replace("String", "Rust");
println!("Replaced string: {}", replaced); // 出力: Replaced string: convert me to Rust
}
“`
String
と&str
の違いと使い分けは、Rustを習得する上で非常に重要なポイントです。String
が必要な場合はヒープに確保される可変な文字列、&str
が必要な場合は参照として文字列データにアクセスすると理解してください。
2.3 std::collections
(コレクション)
std::collections
モジュールは、Vec
やString
以外にも様々な有用なデータ構造(コレクション)を提供します。
HashMap<K, V>
: キーと値のペアを格納するハッシュマップです。キーのハッシュ値に基づいて要素を格納するため、キーによる要素の挿入、検索、削除が高速(通常O(1))です。要素の順序は保証されません。BTreeMap<K, V>
: キーと値のペアを格納するマップですが、Balanced Tree(平衡二分探索木)に基づいています。キーが常にソートされた状態で保持されるため、範囲クエリや最小/最大要素の取得に便利です。操作の計算量はO(log n)です。HashSet<T>
: 重複しない要素の集合です。HashMap
のキーのみを格納するようなものです。要素の順序は保証されません。挿入、検索、削除は高速(通常O(1))です。BTreeSet<T>
: 重複しない要素の集合で、Balanced Treeに基づいています。要素が常にソートされた状態で保持されます。操作の計算量はO(log n)です。VecDeque<T>
(Vector Deque): 両端キュー(Double-Ended Queue)です。シーケンスの両端(先頭と末尾)への要素の追加/削除が効率的です。内部的には循環バッファを使用します。LinkedList<T>
: 双方向連結リストです。シーケンスのどの位置への要素の挿入/削除も効率的(O(1))ですが、インデックスによるアクセスは非効率(O(n))です。メモリの断片化を引き起こしやすいという欠点もあります。
これらのコレクションは、特定の用途に合わせて最適なデータ構造を選択する際に役立ちます。
HashMap
の基本的な使い方:
“`rust
use std::collections::HashMap;
fn main() {
// 新しい空のHashMapを作成
let mut scores = HashMap::new();
// キーと値を挿入
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Yellow"), 50);
println!("HashMap: {:?}", scores); // 出力例: HashMap: {"Blue": 10, "Yellow": 50} (順序は不定)
// キーに対応する値を取得 (Option<V>を返す)
let team_name = String::from("Blue");
let score = scores.get(&team_name); // 参照を渡す
match score {
Some(s) => println!("Score for {}: {}", team_name, s), // 出力: Score for Blue: 10
None => println!("{} has no score.", team_name),
}
// キーが存在しない場合にのみ挿入
scores.entry(String::from("Blue")).or_insert(25); // "Blue"は既に存在するので挿入されない
scores.entry(String::from("Red")).or_insert(30); // "Red"は存在しないので挿入される
println!("HashMap after entry: {:?}", scores); // 出力例: HashMap after entry: {"Blue": 10, "Yellow": 50, "Red": 30}
// HashMapのイテレーション
println!("Iterating through HashMap:");
for (key, value) in &scores {
println!("{}: {}", key, value);
}
}
“`
2.4 std::io
(入出力)
std::io
モジュールは、プログラムが外部ソース(ファイル、ネットワーク接続、標準入出力など)からデータを読み込んだり、外部シンクにデータを書き込んだりするための機能を提供します。
-
主要なトレイト:
Read
: データソースからバイト列を読み込む機能を提供します。Write
: データシンクにバイト列を書き込む機能を提供します。BufRead
: バッファリングされた読み込み機能を提供します(例: 行単位の読み込み)。Seek
: データソース/シンク内の位置を変更する機能を提供します。
-
主要な構造体:
Stdin
,Stdout
,Stderr
: 標準入力、標準出力、標準エラー出力を表します。File
: ファイルを表します。TcpStream
,TcpListener
,UdpSocket
: ネットワーク通信のエンドポイントを表します。BufReader<R>
,BufWriter<W>
: バッファリングによってI/O操作の効率を向上させます。
-
基本的な使い方(標準入力/出力、ファイルI/O):
“`rust
use std::io::{self, Read, Write, BufRead, BufReader, BufWriter, ErrorKind};
use std::fs::File;
fn main() -> io::Result<()> { // main関数がResultを返すことでエラーハンドリングを簡略化
// --- 標準出力 ---
// println!マクロ: フォーマットされた文字列と改行を出力
println!("Hello, world!");
// print!マクロ: フォーマットされた文字列を出力 (改行なし)
print!("This is on the same line. ");
println!("This is too.");
// Writeトレイトを使った出力 (より低レベル)
io::stdout().write_all(b"Writing bytes to stdout\n")?; // b"" はバイトリテラル
io::stderr().write_all(b"Writing bytes to stderr\n")?;
// --- 標準入力 ---
println!("Please enter some text:");
let mut input = String::new();
io::stdin().read_line(&mut input)?; // 標準入力から1行読み込み、改行を含む
println!("You entered: {}", input.trim()); // trim()で前後の空白や改行を削除
// または、BufReaderを使って行単位で読み込み
// let stdin = io::stdin();
// for line in stdin.lock().lines() {
// let line = line?; // Result<String, io::Error> からStringを取り出す
// println!("Read line: {}", line);
// }
// --- ファイルI/O ---
let filename = "example.txt";
// ファイルに書き込み (既存ファイルは上書きされる)
let mut file = File::create(filename)?; // ファイル作成。失敗したらエラーを返す
file.write_all(b"Hello from Rust file I/O!\n")?;
file.write_all("UTF-8 string example\n".as_bytes())?; // Stringをバイト列に変換
println!("Wrote to {}", filename);
// ファイルから読み込み (全体を一気に)
let mut file = File::open(filename)?; // ファイルオープン。失敗したらエラーを返す
let mut contents = String::new();
file.read_to_string(&mut contents)?; // ファイル全体をStringに読み込み
println!("File contents:\n{}", contents);
// ファイルから読み込み (バッファリングして行単位で)
println!("Reading file line by line:");
let file = File::open(filename)?;
let reader = BufReader::new(file);
for line in reader.lines() {
let line = line?; // Result<String, io::Error> からStringを取り出す
println!(" {}", line);
}
// ファイルに追記
use std::io::OpenOptions;
let mut file = OpenOptions::new().append(true).open(filename)?;
file.write_all(b"Appended line.\n")?;
println!("Appended to {}", filename);
// エラーハンドリングの例
let non_existent_file = "non_existent.txt";
match File::open(non_existent_file) {
Ok(f) => println!("File opened successfully! (should not happen)"),
Err(error) => match error.kind() {
ErrorKind::NotFound => {
println!("Error: The file '{}' was not found.", non_existent_file);
}
other_error => {
println!("Error opening file: {:?}", other_error);
}
},
}
Ok(()) // Result::Ok(()) を返す
}
“`
I/O操作はエラーが発生しやすいため、io::Result
や?
演算子を使ったエラーハンドリングが非常に重要になります。
2.5 std::thread
(スレッド)
std::thread
モジュールは、オペレーティングシステムのネイティブスレッドを作成・管理するための機能を提供します。並行処理を行うことで、プログラムのパフォーマンスを向上させたり、応答性を高めたりすることができます。
-
主要な機能:
thread::spawn
: 新しいスレッドを作成し、クロージャを実行します。JoinHandle
: スレッドの実行が完了するのを待つために使用します。thread::sleep
: 現在のスレッドを指定された時間だけ一時停止します。
-
基本的な使い方:
“`rust
use std::thread;
use std::time::Duration;
fn main() {
println!(“Main thread starting.”);
// 新しいスレッドを作成し、処理を実行
let handle = thread::spawn(|| {
// これは新しいスレッドで実行されるコード
for i in 1..5 {
println!("Hi number {} from the spawned thread!", i);
thread::sleep(Duration::from_millis(1)); // 少し待つ
}
});
// メインスレッドの処理
for i in 1..3 {
println!("Hi number {} from the main thread!", i);
thread::sleep(Duration::from_millis(1)); // 少し待つ
}
// spawned threadの完了を待つ
// join()はResultを返すので、unwarp()で成功時の値を取り出す
// spawned threadでパニックが発生した場合、join()もErrを返す
handle.join().unwrap();
println!("Main thread finishing.");
// スレッド間でデータを共有する例 (データのムーブ)
let v = vec![1, 2, 3];
let handle2 = thread::spawn(move || { // moveキーワードでvの所有権をクロージャに移動
println!("Here's a vector from the spawned thread: {:?}", v);
// vはクロージャにムーブされたため、メインスレッドではもう使えない
});
// println!("Vector in main: {:?}", v); // エラー: vはムーブ済み
handle2.join().unwrap();
}
“`
thread::spawn
に渡されるクロージャは、スレッドにデータを渡すためにmove
キーワードを使う必要がある場合があります。これは、Rustの所有権システムが、異なるスレッド間でデータが安全に共有されることを保証するためです。
2.6 std::sync
(同期プリミティブ)
複数のスレッドが同じデータにアクセスする場合、データ競合(data race)を防ぐために同期メカニズムが必要です。std::sync
モジュールは、安全なスレッド間のデータ共有を可能にするための同期プリミティブを提供します。
Mutex<T>
(Mutual Exclusion): 排他制御ロックです。一度に一つのスレッドだけがデータを読み書きできるようにします。データへのアクセスはロックを取得してから行い、アクセスが終わったらロックを解放します。RwLock<T>
(Reader-Writer Lock): 複数のスレッドが同時にデータを読み取れる(リーダー)が、書き込む(ライター)場合は一つのスレッドだけがロックを取得できるロックです。読み込みが多いが書き込みが少ないシナリオでMutex
より高い並列性を実現できます。Arc<T>
(Atomic Reference Counted): 不変なデータを複数のスレッド間で安全に共有するためのポインタ型です。参照カウント方式で、データへの参照がなくなるまでデータをメモリ上に保持します。スレッドセーフな参照カウント (AtomicUsize
) を使用します。-
mpsc
(Multiple Producer, Single Consumer): 複数プロデューサー・単一コンシューマーのチャネルです。スレッド間でメッセージを送信するために使用されます。送信側 (Sender
) は複数作成できますが、受信側 (Receiver
) は一つだけです。 -
基本的な使い方(MutexとArc、mpsc):
“`rust
use std::thread;
use std::sync::{Mutex, Arc, mpsc}; // mpscはchannel用
use std::time::Duration;
fn main() {
// — MutexとArcを使った共有データの例 —
// 複数のスレッドで共有したいデータ (ここではカウンター)
// Arcで参照カウントをスレッド間で共有可能にし、Mutexでミュータブルなアクセスを安全にする
let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];
for i in 0..10 {
let counter = Arc::clone(&counter); // counterの参照カウントを増やす
let handle = thread::spawn(move || {
let mut num = counter.lock().unwrap(); // ロックを取得 (Result<MutexGuard<T>, PoisonError>を返す)
*num += 1; // ロックガードがスコープを抜けるときに自動的にロックが解放される
println!("Thread {} incremented counter to {}", i, num);
// ロックガード `num` はここでスコープを抜ける
});
handles.push(handle);
}
// 全スレッドの完了を待つ
for handle in handles {
handle.join().unwrap();
}
println!("Final counter value: {}", *counter.lock().unwrap()); // 出力: 10
// --- mpscチャネルを使ったスレッド間通信の例 ---
// チャネルを作成 (送信側txと受信側rx)
let (tx, rx) = mpsc::channel();
// 新しいスレッドを作成 (プロデューサー)
thread::spawn(move || {
let messages = vec![
String::from("hi"),
String::from("from"),
String::from("the"),
String::from("thread"),
];
for msg in messages {
tx.send(msg).unwrap(); // メッセージを送信
thread::sleep(Duration::from_millis(10));
}
// tx (Sender) がドロップされると、チャネルはクローズされる
});
// メインスレッド (コンシューマー) はメッセージを受信する
for received in rx { // rxはIteratorトレイトを実装しており、チャネルがクローズされるまでブロックして待つ
println!("Got: {}", received);
}
// 出力例:
// Got: hi
// Got: from
// Got: the
// Got: thread
println!("Channel example finished.");
}
“`
Arc
とMutex
の組み合わせは、複数のスレッドが単一の可変なデータに安全にアクセスするための一般的なパターンです。チャネル (mpsc
) は、データの共有ではなく、スレッド間でメッセージを「受け渡し」する場合に適しています。
2.7 std::net
(ネットワーキング)
std::net
モジュールは、TCPやUDPといった基本的なネットワーク通信のための機能を提供します。
- 主要な構造体:
TcpListener
: TCP接続を待ち受けるサーバー側ソケット。TcpStream
: TCP接続のクライアント側ソケット、またはTcpListener
が受け付けたサーバー側の接続を表します。UdpSocket
: UDP通信のエンドポイントソケット。IpAddr
,SocketAddr
: IPアドレスやソケットアドレス(IPアドレスとポート番号の組み合わせ)を表します。
これらのAPIはブロッキング(同期)です。つまり、読み込みや書き込み操作はデータが利用可能になるか、操作が完了するまでスレッドをブロックします。非同期ネットワーキングには、通常 tokio
や async-std
のような外部クレートが使用されます。
- 基本的な使い方(TCPサーバーとクライアント):
TCPサーバー側 (server.rs
):
“`rust
use std::net::{TcpListener, TcpStream};
use std::io::{Read, Write};
use std::thread;
fn handle_client(mut stream: TcpStream) {
let mut buffer = [0; 1024]; // バッファサイズ
// クライアントからデータを読み込み、そのまま書き戻す (エコーサーバー)
loop {
let bytes_read = match stream.read(&mut buffer) {
Ok(0) => { // 接続が閉じられた
println!(“Client disconnected.”);
break;
}
Ok(n) => n, // 読み込んだバイト数
Err(e) => {
println!(“Error reading from stream: {}”, e);
break;
}
};
// 読み込んだデータをそのままクライアントに書き戻す
if let Err(e) = stream.write_all(&buffer[0..bytes_read]) {
println!("Error writing to stream: {}", e);
break;
}
// bufferの内容をクリアしても良いが、write_allはbytes_read分だけ書くので不要
}
}
fn main() {
// TCP接続を待ち受けるアドレスを指定
let listener = TcpListener::bind(“127.0.0.1:7878”).expect(“Failed to bind address”);
println!(“Server listening on 127.0.0.1:7878”);
// 接続を待ち受けるループ
for stream in listener.incoming() {
match stream {
Ok(stream) => {
println!("New client connected: {:?}", stream.peer_addr().unwrap());
// 新しい接続ごとに新しいスレッドを生成して処理
thread::spawn(|| {
handle_client(stream);
});
}
Err(e) => {
println!("Error accepting connection: {}", e);
}
}
}
}
“`
TCPクライアント側 (client.rs
):
“`rust
use std::net::TcpStream;
use std::io::{Write, Read};
use std::str;
fn main() -> std::io::Result<()> {
// サーバーに接続
let mut stream = TcpStream::connect(“127.0.0.1:7878”)?;
println!(“Connected to server.”);
// サーバーにデータを送信
let message = "Hello, server!";
stream.write_all(message.as_bytes())?;
println!("Sent: {}", message);
// サーバーからの応答を読み込み
let mut buffer = [0; 1024];
let bytes_read = stream.read(&mut buffer)?;
let response = str::from_utf8(&buffer[0..bytes_read]).expect("Invalid UTF-8");
println!("Received: {}", response);
// 接続を閉じる (スコープを抜けると自動的に閉じられるが明示的にシャットダウンも可能)
// stream.shutdown(std::net::Shutdown::Both)?;
Ok(())
}
“`
これらの例を実行するには、まずserver.rs
をコンパイルして実行し、次に別のターミナルでclient.rs
をコンパイルして実行します。
2.8 std::fs
(ファイルシステム)
std::fs
モジュールは、ファイルやディレクトリといったファイルシステムオブジェクトに対する操作を提供します。
-
主要な構造体:
File
: ファイルを表し、Read
およびWrite
トレイトを実装します。Metadata
: ファイルやディレクトリのメタデータ(サイズ、パーミッション、更新時刻など)を表します。DirEntry
: ディレクトリの内容を反復処理する際に、個々のエントリを表します。
-
主要な関数:
File::open
,File::create
read
,read_to_string
,write
(Fileへのメソッド)remove_file
,remove_dir
,remove_dir_all
create_dir
,create_dir_all
copy
,rename
metadata
read_dir
(ディレクトリの内容を読み込む)canonicalize
(絶対パスに解決)
-
基本的な使い方:
“`rust
use std::fs;
use std::io::{self, Write};
use std::path::Path;
fn main() -> io::Result<()> {
let test_dir = “my_test_dir”;
let test_file = Path::new(test_dir).join(“test_file.txt”); // パスの結合
// ディレクトリを作成 (もし存在しないなら)
if !Path::new(test_dir).exists() {
fs::create_dir(test_dir)?;
println!("Created directory: {}", test_dir);
}
// ファイルに書き込み (簡易版)
fs::write(&test_file, "Hello from fs::write!\nAnother line.\n")?;
println!("Wrote to file: {:?}", test_file);
// ファイルから読み込み (簡易版)
let contents = fs::read_to_string(&test_file)?;
println!("Read from file:\n{}", contents);
// ファイルのメタデータを取得
let metadata = fs::metadata(&test_file)?;
println!("File size: {} bytes", metadata.len());
println!("Is file: {}", metadata.is_file());
println!("Is directory: {}", metadata.is_dir());
// ディレクトリの内容をリストアップ
println!("Contents of directory '{}':", test_dir);
for entry in fs::read_dir(test_dir)? {
let entry = entry?; // Result<DirEntry, io::Error> からDirEntryを取り出す
let path = entry.path();
println!(" {:?}", path.display()); // display()で表示可能な形式に
}
// ファイルを削除
fs::remove_file(&test_file)?;
println!("Removed file: {:?}", test_file);
// ディレクトリを削除 (空である必要がある)
// fs::remove_dir(test_dir)?;
// println!("Removed directory: {}", test_dir);
// ディレクトリと内容を再帰的に削除
// fs::remove_dir_all(test_dir)?;
// println!("Removed directory and contents recursively: {}", test_dir);
Ok(())
}
“`
std::fs
の関数は、ファイルシステムの操作がオペレーティングシステムによって失敗する可能性があるため、ほとんどが io::Result<T>
を返します。エラーハンドリングが重要です。
2.9 std::env
(環境)
std::env
モジュールは、プログラムが実行されている環境に関する情報にアクセスするための機能を提供します。
-
主要な関数:
args()
: コマンドライン引数をイテレータとして返します。vars()
: 環境変数をキー-値のペアのイテレータとして返します。var(key)
: 特定の環境変数の値を取得します(Result<String, VarError>
を返す)。current_dir()
: 現在の作業ディレクトリを取得します(Result<PathBuf, io::Error>
を返す)。set_var
,remove_var
: 環境変数を設定/削除します。exit(code)
: プログラムを終了します。
-
基本的な使い方:
“`rust
use std::env;
use std::process;
fn main() {
// コマンドライン引数を取得
println!(“Command line arguments:”);
for arg in env::args() {
println!(” {}”, arg);
}
// 最初の引数はプログラム自身のパスです。
// 特定の環境変数を取得
match env::var("PATH") {
Ok(val) => println!("PATH environment variable: {}", val),
Err(_) => println!("PATH environment variable not set."),
}
match env::var("HOME") {
Ok(val) => println!("HOME environment variable: {}", val),
Err(_) => println!("HOME environment variable not set."),
}
// 全ての環境変数をリストアップ
println!("All environment variables:");
for (key, value) in env::vars() {
// 注意: env::vars()は非決定的な順序を返します
// println!("{}: {}", key, value); // 全て表示すると長くなるのでコメントアウト
}
// 現在の作業ディレクトリを取得
match env::current_dir() {
Ok(path) => println!("Current directory: {:?}", path),
Err(e) => println!("Failed to get current directory: {}", e),
}
// 環境変数を設定 (このプログラムの実行中のみ有効)
env::set_var("MY_CUSTOM_VAR", "my_value");
println!("MY_CUSTOM_VAR: {:?}", env::var("MY_CUSTOM_VAR")); // Some("my_value")
// 環境変数を削除 (このプログラムの実行中のみ有効)
env::remove_var("MY_CUSTOM_VAR");
println!("MY_CUSTOM_VAR after removal: {:?}", env::var("MY_CUSTOM_VAR")); // Err(NotPresent)
// プログラムの終了コードを指定して終了
// process::exit(0); // 成功終了
// process::exit(1); // エラー終了
}
“`
env::args()
を使用する際は、最初の要素が実行ファイルのパスであることに注意が必要です。通常、引数は2番目から取得します。
2.10 std::time
(時間)
std::time
モジュールは、時間に関連する機能(時間の計測、期間の表現、システム時刻の取得など)を提供します。
-
主要な構造体:
Duration
: 時間の長さを表します。Instant
: 単調時計に基づいた特定の時点を表し、経過時間を正確に計測するのに適しています。SystemTime
: システムの壁時計に基づいた特定の時点を表し、日付や時刻に関連する操作に適しています。
-
基本的な使い方:
“`rust
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
use std::thread;
fn main() {
// Duration (時間の長さ) の作成
let one_sec = Duration::from_secs(1);
let ten_millis = Duration::from_millis(10);
let hundred_micros = Duration::from_micros(100);
let nano = Duration::from_nanos(1);
println!("1 sec: {:?}", one_sec);
println!("10 millis: {:?}", ten_millis);
println!("100 micros: {:?}", hundred_micros);
println!("1 nano: {:?}", nano);
// 時間の計測 (Instant)
let start = Instant::now(); // 今の時間を記録 (Instant)
// 何か時間のかかる処理 (例: スリープ)
thread::sleep(one_sec + ten_millis);
let duration = start.elapsed(); // 開始からの経過時間 (Duration)
println!("Elapsed time: {:?}", duration);
println!("Elapsed time in milliseconds: {} ms", duration.as_millis());
// システム時刻 (SystemTime) の取得
let now = SystemTime::now();
println!("Current system time: {:?}", now);
// UNIXエポックからの経過時間を取得 (UNIXタイムスタンプ)
match now.duration_since(UNIX_EPOCH) {
Ok(elapsed) => {
println!("Current system time since UNIX EPOCH: {} seconds", elapsed.as_secs());
}
Err(e) => {
// システム時刻がUNIXエポックより前の場合に発生
println!("System time is before UNIX EPOCH: {}", e);
}
}
// SystemTimeとInstantの使い分け:
// Instant: プログラムの起動からの経過時間など、正確な時間間隔の計測に使う (時計が戻る影響を受けない単調時計)
// SystemTime: ファイルの作成/更新時刻、ネットワーク通信のタイムスタンプなど、カレンダー時間に関連する情報に使う (OSによって時刻が変更される影響を受ける)
}
“`
Instant
は、システムの時刻変更(夏時間や手動での調整)の影響を受けないため、ベンチマークやタイムアウト処理など、正確な時間間隔を計測する場合に推奨されます。SystemTime
は、ファイル更新時刻など、現実世界の日時を扱う場合に利用します。
2.11 std::boxed
, std::rc
, std::sync::Arc
, std::cell
, std::sync::Mutex
(メモリ管理・ポインタ)
Rustの所有権システムは、コンパイル時にメモリ安全性を保証しますが、特定の状況ではより柔軟なメモリ管理やデータの共有が必要になります。std
ライブラリは、これらのニーズに応えるための様々なスマートポインタや内部可変性を提供する型を提供します。これらはalloc
の上に構築されています。
-
Box<T>
: ヒープに値を割り当てます。主に以下の目的で使用されます。- コンパイル時にサイズが不明な型の値を扱う場合(例: トレイトオブジェクト
Box<dyn Trait>
)。 - 大きなデータをスタックではなくヒープに置く場合。
- 再帰的なデータ構造を定義する場合。
- コンパイル時にサイズが不明な型の値を扱う場合(例: トレイトオブジェクト
-
Rc<T>
(Reference Counted): 単一スレッド内で、複数の所有者でデータを共有します。データの参照カウントを持ち、参照が0になったときにデータを解放します。データは不変(immutable)として共有されます。可変な共有が必要な場合は、Rc
とRefCell
を組み合わせて使用します。 -
Arc<T>
(Atomic Reference Counted):Rc<T>
のスレッドセーフ版です。複数のスレッド間で不変なデータを共有できます。参照カウントはアトミック操作によって管理されるため、マルチスレッド環境でも安全です。可変な共有が必要な場合は、Arc
とMutex
を組み合わせて使用します。 -
Cell<T>
:Copy
トレイトを実装する型Tについて、単一スレッド内で内部可変性を提供します。.get()
で値のコピーを取得し、.set(value)
で値を置き換えることができます。借用チェッカーはコンパイル時にミュータブルな借用を許可しませんが、Cell
を使うことで「Cellの中身だけ」実行時に変更できるようになります。 -
RefCell<T>
: 単一スレッド内で内部可変性を提供します。実行時に借用ルール(複数のImmutable Borrowまたは単一のMutable Borrow)をチェックします。.borrow()
で共有参照 (&T
) を、.borrow_mut()
で可変参照 (&mut T
) を取得できます。実行時の借用違反はパニックを引き起こします。Rc
と組み合わせて、単一スレッド内で複数の所有者を持つ可変データを安全に共有する定番のパターンです。 -
Mutex<T>
: マルチスレッド環境で可変データを安全に共有するための排他制御ロックです。上でstd::sync
で説明しましたが、ここではスマートポインタの文脈で再度触れます。Arc
と組み合わせて、複数のスレッドで同じ可変データを安全に共有するための標準的なパターンです。
これらのスマートポインタやセル型は、Rustの所有権システムの厳密さを維持しつつ、柔軟性を提供するための高度なツールです。初心者の方は、まずBox
から理解し、必要に応じてRc
, Arc
, Cell
, RefCell
へと進むのが良いでしょう。
- 基本的な使い方(RcとRefCell):
“`rust
use std::rc::Rc;
use std::cell::RefCell;
fn main() {
// RcとRefCellを使った、単一スレッドでの可変データの複数所有
let data = Rc::new(RefCell::new(vec![1, 2, 3]));
// dataへの参照を作成 (参照カウントが増える)
let data_ref1 = Rc::clone(&data); // Rc::clone()はディープコピーではなく参照カウントを増やす
let data_ref2 = Rc::clone(&data);
println!("Initial data: {:?}", data.borrow()); // borrow()で不変参照を取得
println!("Reference count after clones: {}", Rc::strong_count(&data)); // 出力: 3
// いずれかの参照からデータを変更
data_ref1.borrow_mut().push(4); // borrow_mut()で可変参照を取得
println!("Data after push: {:?}", data.borrow()); // 出力: Data after push: [1, 2, 3, 4]
// 別の参照からデータを変更
{
let mut borrowed_mut = data_ref2.borrow_mut(); // 可変参照を取得
borrowed_mut[0] = 10;
// borrowed_mut はスコープを抜けるときに解放される
}
println!("Data after modification: {:?}", data.borrow()); // 出力: Data after modification: [10, 2, 3, 4]
// borrow()やborrow_mut()は実行時に借用ルールをチェックする
// 以下のコードは実行時にパニックを引き起こす
// let ref1 = data.borrow();
// let ref2 = data.borrow_mut(); // 実行時エラー: already mutably borrowed
// Rcがドロップされて参照カウントが0になると、中のデータも解放される
drop(data_ref1);
println!("Reference count after drop ref1: {}", Rc::strong_count(&data)); // 出力: 2
drop(data_ref2); // data_ref2がドロップされると、Rcの参照カウントは1になる
// data (元の変数) がスコープを抜けるときに最後の参照カウントが0になり、Vec<i32>が解放される
}
“`
2.12 std::error
(エラーハンドリング)
Rustのエラーハンドリングは、言語機能 (Result<T, E>
, Option<T>
, panic!
) と標準ライブラリの型やトレイトが組み合わさって実現されます。std::error
モジュールは、エラー型を定義する際に実装するべきError
トレイトを提供します。
-
Error
トレイト:- カスタムエラー型が実装すべきトレイトです。
Display
トレイトとDebug
トレイトの実装を要求します。source()
メソッドを提供し、エラーの根本原因(他のエラー)を示すことができます。これにより、エラーチェーンを構築できます。From
トレイトの実装と組み合わせることで、?
演算子を使ったエラー変換が容易になります。
-
Result<T, E>
とOption<T>
: これらは言語のコア機能に近いですが、そのメソッド(unwrap
,expect
,map
,and_then
,?
など)はstd
ライブラリで定義されています。これらの型は、失敗する可能性のある操作の結果を表現するための主要な手段です。 -
panic!
: 回復不能なエラーを示します。デフォルトではプログラムはクラッシュし、スタックトレースが表示されます。開発中や想定外の状態に遭遇した場合に使用します。 -
基本的な使い方(Resultと?演算子、Option):
“`rust
use std::fs::File;
use std::io::{self, Read};
use std::error::Error; // Errorトレイトをインポート
use std::fmt; // DisplayとDebugのために必要
// — カスタムエラー型の定義例 —
[derive(Debug)]
enum MyError {
Io(io::Error),
Parse(std::num::ParseIntError),
NotFound(String),
}
// Displayトレイトの実装 (ユーザー向けのエラーメッセージ)
impl fmt::Display for MyError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
MyError::Io(e) => write!(f, “I/O error: {}”, e),
MyError::Parse(e) => write!(f, “Parse error: {}”, e),
MyError::NotFound(filename) => write!(f, “File not found: {}”, filename),
}
}
}
// Errorトレイトの実装 (根本原因など)
impl Error for MyError {
fn source(&self) -> Option<&(dyn Error + ‘static)> {
match self {
MyError::Io(e) => Some(e),
MyError::Parse(e) => Some(e),
MyError::NotFound(_) => None, // このエラー自体が根本原因
}
}
}
// 他のエラー型からの自動変換 (Fromトレイト)
impl From
fn from(err: io::Error) -> MyError {
MyError::Io(err)
}
}
impl From
fn from(err: std::num::ParseIntError) -> MyError {
MyError::Parse(err)
}
}
// Resultと?演算子を使った関数の例 (MyErrorを返す)
fn read_and_parse_number(filename: &str) -> Result
let mut file = File::open(filename)?; // io::Error -> MyError::Io へ自動変換される
let mut contents = String::new();
file.read_to_string(&mut contents)?; // io::Error -> MyError::Io へ自動変換される
let number_str = contents.trim();
let number = number_str.parse::<i32>()?; // ParseIntError -> MyError::Parse へ自動変換される
if number < 0 {
// 特定の条件で独自エラーを返す
return Err(MyError::NotFound(String::from(filename))); // この場合はFromを使わない
}
Ok(number) // 成功時はOk()で包む
}
// — Optionの基本的な使い方 —
fn find_first_positive(numbers: Vec
for num in numbers {
if num > 0 {
return Some(num); // 見つかった場合はSome(値)を返す
}
}
None // 見つからなかった場合はNoneを返す
}
fn main() {
// Resultと?演算子の使用例
let filename = “my_number.txt”;
// ダミーファイルを作成 (成功するケースのため)
if let Err(e) = fs::write(filename, “123\n”) {
eprintln!(“Error creating dummy file: {}”, e);
process::exit(1);
}
match read_and_parse_number(filename) {
Ok(num) => println!("Successfully read and parsed: {}", num),
Err(e) => {
eprintln!("Error: {}", e); // Displayトレイトによって分かりやすく表示される
if let Some(source) = e.source() {
eprintln!("Caused by: {}", source); // 根本原因を表示
}
}
}
// 失敗するケースの例 (ファイルが存在しない)
match read_and_parse_number("non_existent_file.txt") {
Ok(num) => println!("Successfully read and parsed: {}", num),
Err(e) => {
eprintln!("Error: {}", e);
if let Some(source) = e.source() {
eprintln!("Caused by: {}", source);
}
}
}
// 失敗するケースの例 (パースエラー)
if let Err(e) = fs::write(filename, "abc\n") { // ダミーファイルを上書き
eprintln!("Error creating dummy file: {}", e);
process::exit(1);
}
match read_and_parse_number(filename) {
Ok(num) => println!("Successfully read and parsed: {}", num),
Err(e) => {
eprintln!("Error: {}", e);
if let Some(source) = e.source() {
eprintln!("Caused by: {}", source);
}
}
}
// Optionの使用例
let numbers1 = vec![-1, -2, 3, -4];
match find_first_positive(numbers1) {
Some(num) => println!("Found first positive number: {}", num),
None => println!("No positive number found."),
}
let numbers2 = vec![-1, -2, -3, -4];
match find_first_positive(numbers2) {
Some(num) => println!("Found first positive number: {}", num),
None => println!("No positive number found."),
}
// Optionのメソッド例
let maybe_value: Option<i32> = Some(10);
let doubled_value = maybe_value.map(|x| x * 2); // Some(20)
println!("Doubled value: {:?}", doubled_value);
let maybe_none: Option<i32> = None;
let doubled_none = maybe_none.map(|x| x * 2); // None
println!("Doubled none: {:?}", doubled_none);
let result: Option<i32> = Some(5);
let final_result: Option<i32> = result.and_then(|x| if x > 0 { Some(x * 2) } else { None }); // Some(10)
println!("Final result: {:?}", final_result);
let result2: Option<i32> = Some(-5);
let final_result2: Option<i32> = result2.and_then(|x| if x > 0 { Some(x * 2) } else { None }); // None
println!("Final result2: {:?}", final_result2);
// panic! の例 (コメントアウトして実行しないのが一般的)
// println!("About to panic!");
// panic!("Something went wrong!");
// println!("This line will not be reached.");
// ダミーファイルをクリーンアップ
if let Err(e) = fs::remove_file(filename) {
eprintln!("Error cleaning up dummy file: {}", e);
}
}
“`
Rustにおけるエラーハンドリングのスタイルは、結果として成功か失敗かを明示的に扱うことで、堅牢なプログラムを書くことを奨励しています。Option
とResult
、そして?
演算子は、このスタイルの中心的な要素です。
2.13 std::iter
(イテレータ)
std::iter
モジュールは、コレクションやシーケンスを反復処理するためのイテレータに関するトレイトや関数を提供します。イテレータは遅延評価(Lazy Evaluation)であり、実際に要素が必要になったときに処理が行われます。
-
主要なトレイト:
Iterator
: イテレータの基本的なトレイトです。next()
メソッドを実装することで、シーケンスの次の要素(もしあればSome(value)
、なければNone
)を取得できます。DoubleEndedIterator
: 前後両方向から要素を取得できるイテレータのためのトレイトです。ExactSizeIterator
: 要素の正確な数を事前に知っているイテレータのためのトレイトです。
-
主要なイテレータアダプタ(メソッド):
イテレータトレイトには、元のイテレータを変更したり組み合わせたりして新しいイテレータを生成する様々なメソッド(アダプタ)が豊富に用意されています。map()
: 各要素に関数を適用して新しい要素を生成します。filter()
: 各要素に述語を適用し、条件を満たす要素だけを通過させます。zip()
: 2つのイテレータの要素をペアにして結合します。enumerate()
: 要素とそのインデックスをペアにして提供します。skip()
,take()
: 要素をスキップしたり、最初のn個だけを取ったりします。collect()
: イテレータの結果を別のコレクション型に収集します。
-
基本的な使い方:
“`rust
fn main() {
let numbers = vec![1, 2, 3, 4, 5, 6];
// Vecのイテレータを取得 (&Vecはiter()を呼び出す)
let mut num_iter = numbers.iter();
// next()を使って要素を一つずつ取得
println!("First element: {:?}", num_iter.next()); // Some(1)
println!("Second element: {:?}", num_iter.next()); // Some(2)
println!("Remaining elements will be processed by the loop below.");
// forループはIteratorトレイトを利用している糖衣構文
println!("Iterating using a for loop:");
for num in numbers.iter() { // &i32型の要素を取得 (不変参照)
println!(" {}", num);
}
println!("Iterating using a for loop (mutable):");
let mut mutable_numbers = vec![10, 20, 30];
for num in mutable_numbers.iter_mut() { // &mut i32型の要素を取得 (可変参照)
*num += 5; // 要素を直接変更
println!(" {}", num);
}
println!("Mutable numbers after iteration: {:?}", mutable_numbers); // [15, 25, 35]
println!("Iterating using a for loop (consuming):");
let consuming_numbers = vec![100, 200, 300];
for num in consuming_numbers { // Vecの所有権をムーブし、要素(i32)を値として取得
println!(" {}", num);
}
// println!("{:?}", consuming_numbers); // エラー: consuming_numbersはムーブ済み
// --- イテレータアダプタの例 ---
let numbers2 = vec![1, 2, 3, 4, 5, 6];
// mapとcollect
let doubled_numbers: Vec<i32> = numbers2.iter() // イテレータを作成 (&i32)
.map(|x| x * 2) // 各要素を2倍にする新しいイテレータ (&i32 -> i32)
.collect(); // 結果をVec<i32>に収集
println!("Doubled numbers: {:?}", doubled_numbers); // [2, 4, 6, 8, 10, 12]
// filter
let even_numbers: Vec<i32> = numbers2.into_iter() // Vecの所有権をムーブし、要素(i32)を値として取得するイテレータ
.filter(|x| x % 2 == 0) // 偶数だけをフィルタリング
.collect(); // 結果をVec<i32>に収集
println!("Even numbers: {:?}", even_numbers); // [2, 4, 6]
// chainとzip
let letters = vec!['a', 'b', 'c'];
let digits = vec![1, 2, 3];
let combined: Vec<(char, i32)> = letters.into_iter().zip(digits.into_iter()).collect();
println!("Combined (zip): {:?}", combined); // [('a', 1), ('b', 2), ('c', 3)]
let v1 = vec![1, 2];
let v2 = vec![3, 4];
let chained: Vec<i32> = v1.into_iter().chain(v2.into_iter()).collect();
println!("Chained: {:?}", chained); // [1, 2, 3, 4]
// sum, product, max, min (コンシューマアダプタ)
let numbers3 = vec![1, 2, 3, 4, 5];
let total: i32 = numbers3.iter().sum(); // sum()はIntoIteratorを実装する型に対して呼び出せる
let product: i32 = numbers3.iter().product();
let max_val: Option<&i32> = numbers3.iter().max();
let min_val: Option<&i32> = numbers3.iter().min();
println!("Sum: {}", total); // 15
println!("Product: {}", product); // 120
println!("Max: {:?}", max_val); // Some(5)
println!("Min: {:?}", min_val); // Some(1)
// enumerate
println!("Enumerating:");
for (index, value) in doubled_numbers.iter().enumerate() {
println!(" Index {}: {}", index, value);
}
}
“`
イテレータとそれらのアダプタは、Rustでコレクションを扱う上で非常に強力かつ効率的な手段です。遅延評価により、不要な中間コレクションの生成を避けることができます。
2.14 std::fmt
(フォーマット)
std::fmt
モジュールは、文字列のフォーマットに関するトレイトと関数を提供します。println!
, print!
, format!
, eprintln!
, eprint!
といったマクロは、このモジュールの機能を利用しています。
-
主要なトレイト:
Display
: 人間が読みやすい形で値を文字列にフォーマットするためのトレイトです。{}
マーカーで使用されます。Debug
: 開発者がデバッグ目的で値をフォーマットするためのトレイトです。{:?}
マーカーで使用されます。#[derive(Debug)]
アトリビュートで簡単に実装できます。Binary
,Octal
,LowerHex
,UpperHex
: 数値を異なる基数でフォーマットするためのトレイトです。Pointer
: ポインタ値をフォーマットするためのトレイトです。LowerExp
,UpperExp
: 浮動小数点数を指数形式でフォーマットするためのトレイトです。
-
基本的な使い方:
“`rust
use std::fmt;
// DebugとDisplayを実装する構造体の例
[derive(Debug)] // デバッグ表示のためにDebugを自動実装
struct Point {
x: i32,
y: i32,
}
// Displayを手動で実装 (ユーザーフレンドリーな表示)
impl fmt::Display for Point {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, “({}, {})”, self.x, self.y) // write!マクロを使ってフォーマッタに書き込み
}
}
fn main() {
let p = Point { x: 10, y: 20 };
// Debug フォーマット {:?}
println!("Debug format: {:?}", p); // 出力例: Debug format: Point { x: 10, y: 20 }
// Pretty Debug フォーマット {:#?} (構造体を整形して表示)
println!("Pretty Debug format:\n{:#?}", p); // 出力例: 整形された構造体表示
// Display フォーマット {}
println!("Display format: {}", p); // 出力例: Display format: (10, 20)
// 様々なフォーマットフラグ
let num = 42;
println!("Decimal: {}", num); // 10進数
println!("Binary: {:b}", num); // 2進数
println!("Octal: {:o}", num); // 8進数
println!("Lower Hex: {:x}", num); // 16進数 (小文字)
println!("Upper Hex: {:X}", num); // 16進数 (大文字)
println!("Pointer: {:p}", &num); // ポインタアドレス
let pi = 3.14159;
println!("Floating point: {}", pi);
println!("Exponential (lower): {:e}", pi); // 3.14159e0
println!("Exponential (upper): {:E}", pi); // 3.14159E0
// 幅と精度、アライメント
let text = "Rust";
println!("Padded: {:>10}", text); // 右寄せ、幅10
println!("Padded: {:<10}", text); // 左寄せ、幅10
println!("Padded: {:^10}", text); // 中央寄せ、幅10
println!("Padded (fill): {:_>10}", text); // 埋め文字を指定
println!("Precision (float): {:.2}", pi); // 小数点以下2桁
// format! マクロ: 文字列を生成するが、出力はしない
let formatted_string = format!("The point is {}", p);
println!("Generated string: {}", formatted_string);
// エラー型の表示でもfmt::Displayが使われる
let non_existent_file = "no_such_file.txt";
match File::open(non_existent_file) {
Err(e) => println!("File error: {}", e), // io::ErrorはDisplayを実装している
_ => {}
}
}
“`
Debug
は開発中にデバッグ情報を素早く表示するために非常に便利ですが、ユーザーに見せるメッセージとしては通常Display
を使用します。独自の型を定義した場合、特にライブラリとして公開する場合は、必要に応じてDisplay
とDebug
の両方を実装することが推奨されます。
2.15 std::panic
(パニック)
std::panic
モジュールは、回復不能なエラー、すなわちプログラムの継続が不可能になった状態を処理するための機能を提供します。
-
主要な機能:
panic!(...)
: パニックを引き起こし、メッセージを表示します。デフォルトではスタック巻き戻し(unwinding)が行われ、プログラムが終了します。panic::catch_unwind
: パニックを捕捉し、プログラムの終了を防ぐことができます。ただし、これは一般的に推奨されるエラーハンドリング方法ではありません。主にFFI(外部関数インターフェース)の境界などで、RustコードがCコードなどにパニックを伝播させないようにするために使用されます。panic::set_hook
,panic::take_hook
: パニック発生時に実行されるカスタムフックを設定/取得します。
-
パニック vs Result/Option:
- パニック: プログラムが不正な状態になり、これ以上進めない場合に使用します。例: プログラマーの論理エラー(配列の範囲外アクセス)、リカバリが不可能な環境エラー。
- Result/Option: 失敗が予期され、呼び出し元がその失敗を処理できる場合に使用します。例: ファイルが見つからない、ユーザー入力のパース失敗、ネットワーク接続の失敗。
-
基本的な使い方:
“`rust
fn main() {
// シンプルなパニック
// panic!(“Crash and burn!”);
// フォーマット付きパニック
// let x = 10;
// panic!("This is a formatted panic. Value was: {}", x);
// ベクターの範囲外アクセスによるパニック (std::vec::Vec::index::<i32>)
// let v = vec![1, 2, 3];
// println!("{}", v[10]); // 実行時パニック
// Optionのunwrap()によるパニック
// let five = Some(5);
// let six = five.unwrap() + 1; // 6
// let none: Option<i32> = None;
// let seven = none.unwrap(); // 実行時パニック
// Resultのunwrap()によるパニック
// use std::fs::File;
// let f = File::open("hey").unwrap(); // 実行時パニック (ファイルが見つからない)
// --- catch_unwindの例 (推奨されないが、存在を知っておく) ---
use std::panic;
use std::thread;
let result = panic::catch_unwind(|| {
// このクロージャ内でパニックが発生しても、catch_unwindがそれを捕捉する
println!("Entering guarded code...");
// panic!("Oh no!"); // この行を実行するとパニックが捕捉される
println!("Exiting guarded code normally.");
42 // 成功時の戻り値
});
println!("Catch unwind result: {:?}", result); // パニックした場合 Err(...), しない場合 Ok(42)
// スレッド内のパニックはデフォルトでスレッドを終了させる
// join()はパニックをResult::Errとしてメインスレッドに伝播させる
let handle = thread::spawn(|| {
panic!("Panic in spawned thread!");
});
let join_result = handle.join();
println!("Spawned thread join result: {:?}", join_result); // Err(...)
}
“`
多くのアプリケーションでは、パニックはプログラムの終了を意味します。ライブラリを作成する場合は、可能な限りResult
やOption
を使用して、呼び出し元がエラーから回復できるようにすることが推奨されます。バイナリ(実行可能ファイル)の場合、コマンドラインツールのように、予期しないエラーで即座に終了するのが妥当な振る舞いであれば、パニックをそのままにしておくこともあります。Webサーバーなどの長時間稼働するサービスでは、スレッド単位でパニックを捕捉し、他のスレッドの実行に影響を与えないようにすることが必要になる場合があります(ただしcatch_unwind
は注意して使用する必要があります)。
3. stdライブラリの利用方法
これまで見てきたように、std
ライブラリの機能は非常に豊富です。これらの機能を自分のコードから利用する方法はいくつかあります。
3.1 Prelude (事前インポート)
Rustのモジュールシステムでは、通常、外部のモジュールや構造体、関数などを使用する際には use
キーワードを使って明示的にインポートする必要があります。
しかし、std
ライブラリの中には、非常に頻繁に使用される機能があり、これらを毎回 use
するのは煩雑です。そこで、Rustではプレリュード (Prelude) という概念があります。プレリュードは、全てのモジュールでデフォルトでスコープに入る(暗黙的にインポートされる)項目群です。
std::prelude::v1
モジュール(Rust 2018エディション以降のデフォルト)には、以下のようなものが含まれています。
std::mem::drop
std::clone::Clone
std::cmp::{PartialEq, Eq, PartialOrd, Ord}
std::convert::{AsRef, AsMut, Into, From}
std::default::Default
std::iter::{Iterator, Extend, IntoIterator, DoubleEndedIterator, ExactSizeIterator}
std::option::Option::{self, Some, None}
std::result::Result::{self, Ok, Err}
std::string::String
std::vec::Vec
これらの項目は、あなたが新しいRustプロジェクトを作成した際に、特に use
を書かなくてもすぐに使うことができます。例えば、Vec::new()
や Ok()
、None
といったものを use std::vec::Vec;
や use std::option::Option;
と書かずに使えるのは、プレリュードのおかげです。
3.2 use
キーワードを使った明示的なインポート
プレリュードに含まれていない std
の機能を使用する場合は、use
キーワードを使って明示的にインポートする必要があります。これにより、コードの可読性が向上し、どの機能がどこから来ているのかが明確になります。
“`rust
// 明示的なインポートの例
use std::collections::HashMap; // stdクレートのcollectionsモジュールのHashMap構造体
use std::io::Write; // stdクレートのioモジュールのWriteトレイト
use std::fs::File; // stdクレートのfsモジュールのFile構造体
use std::thread::spawn; // stdクレートのthreadモジュールのspawn関数
use std::time::Duration; // stdクレートのtimeモジュールのDuration構造体
fn main() {
let mut map = HashMap::new();
map.insert(“key”.to_string(), 1);
let _ = File::create("temp.txt").expect("Failed to create file")
.write_all(b"hello");
let handle = spawn(|| {
println!("Hello from thread");
std::thread::sleep(Duration::from_secs(1)); // sleepはuseしていないのでフルパス
});
handle.join().unwrap();
}
“`
3.3 フルパスでの利用
use
を使わずに、機能の完全なパス(クレート名::モジュール名::...::項目名
)を使って直接利用することも可能です。これは通常、名前の衝突を避けるため、あるいは特定の機能がコード中で一度しか使われない場合に選択されます。
“`rust
fn main() {
let mut file = std::fs::File::create(“another.txt”).expect(“Failed to create file”);
std::io::Write::write_all(&mut file, b”content”).expect(“Failed to write”);
let args: Vec<String> = std::env::args().collect();
println!("Args: {:?}", args);
}
“`
3.4 as
キーワードによるリネーム
インポートする際に、as
キーワードを使って別名を付けることができます。これは、長い名前を短くしたり、名前の衝突を解決したりする場合に便利です。
“`rust
use std::collections::HashMap as StdHashMap; // HashMapをStdHashMapとしてインポート
fn main() {
let mut map = StdHashMap::new(); // StdHashMapとして使用
map.insert(“key”.to_string(), 10);
println!(“{:?}”, map);
}
“`
3.5 グロブ (Glob) インポート (*
)
モジュール内の全てのパブリックな項目を一度にインポートするには、グロブ (*
) を使用します。これは、テストモジュールなど、特定のモジュールの内容を頻繁に使用する場合に便利ですが、名前の衝突を引き起こしやすく、コードの可読性を損なう可能性があるため、一般的には推奨されません。
“`rust
use std::collections::*; // std::collectionsモジュールの全てをインポート
fn main() {
let mut map = HashMap::new(); // std::collections::HashMap
let mut set = HashSet::new(); // std::collections::HashSet
map.insert(1, 2);
set.insert(3);
println!("{:?}", map);
println!("{:?}", set);
}
“`
4. まとめ:stdライブラリの重要性と今後の学習
この記事では、Rustの標準ライブラリ std
が何であるか、なぜ重要なのか、そしてその主要なモジュールと機能について詳しく見てきました。
std
は、Rustプログラミングの土台となる、不可欠なツールキットです。基本的なデータ構造から、I/O、並行処理、ネットワーキング、ファイルシステム操作、エラーハンドリング、時間、環境情報へのアクセスなど、現代のアプリケーション開発に必要な機能の大部分を提供しています。
Rustが提供する安全性やパフォーマンスといった利点は、言語自体の機能に加え、std
ライブラリがこれらの言語機能を効果的に活用していることによっても実現されています。例えば、Vec
やString
が所有権システムと連携してメモリ安全性を保証したり、Arc
やMutex
がスレッド間の安全なデータ共有を可能にしたりしています。
今後の学習に向けて:
- 実際にコードを書く: 紹介した各モジュールの基本的な使い方を、実際にコードを書いて試してみてください。手を動かすことが最も重要です。
- 公式ドキュメントを参照する: Rustの公式ドキュメント(doc.rust-lang.org)は非常に充実しています。
std
クレートのドキュメントを積極的に参照し、各構造体やトレイトが持つ全てのメソッドや関連関数を調べてみましょう。例えば、Vec
やString
には、ここで紹介しきれなかった多くの便利なメソッドがあります。 - Rust By ExampleやThe Rust Programming Languageを読む: これらの公式の学習リソースでも、
std
の様々な機能が紹介されています。 - 外部クレートを知る:
std
は基本的な機能を提供しますが、より高度なニーズ(例: 非同期I/Oに特化したtokio
やasync-std
、強力なシリアライゼーション/デシリアライゼーションライブラリserde
、乱数生成のrand
など)には、crates.ioで公開されている豊富な外部クレートを利用するのが一般的です。std
を理解することは、これらの外部クレートを効果的に使用するための基礎となります。
標準ライブラリ std
を深く理解し使いこなせるようになることは、Rustacean(Rustプログラマーの愛称)として成長する上で避けて通れない道です。この記事が、その道のりの一助となれば幸いです。
Happy Rusting!