コンテンツにスキップ

Rust チートシート

Rust 1.98・Edition 2024をベースにしたチートシートです。


Rust公式 - Install Rust

Rustのツールチェーンは、公式の rustup で管理する方法が推奨されています。macOS、Linux、WSLなどのUnix系環境では、公式サイトに掲載されている次のコマンドでインストーラを起動します。

Terminal window
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

Windowsでは公式インストールページから rustup-init.exe を取得して実行します。環境によってはVisual StudioのC++ Build Toolsも必要です。

インストール後はターミナルを開き直し、バージョンを確認します。

Terminal window
rustc --version
cargo --version
rustup --version

安定版ツールチェーンの更新と、フォーマッタ・linterの追加は次のように行います。

Terminal window
rustup update stable
rustup component add rustfmt clippy

既存プロジェクトに rust-toolchain.tomlrust-toolchain がある場合は、その指定を優先します。ライブラリでは Cargo.tomlrust-version でサポートする最小Rustバージョン(MSRV)を示せます。


fn main() {
println!("Hello, Rust 1.98!");
}
  • 実行可能バイナリのエントリポイントは main 関数
  • 文は通常 ; で終える
  • println! のように ! が付くものはマクロ
構文 用途
// ... 行末までの通常コメント
/* ... */ 複数行にまたがる通常コメント。入れ子にできる
/// ..., /** ... */ 直後の項目を説明する外部ドキュメントコメント
//! ..., /*! ... */ crateやmodule自身を説明する内部ドキュメントコメント
// 行コメント
let value = 42;
/* ブロックコメントは
複数行に記述できる */
assert_eq!(value, 42);

ドキュメントコメントはMarkdownとして処理され、cargo doc で生成されるAPIドキュメントに反映されます。コードブロックは通常 cargo test でドキュメントテストとして実行されます。

mod math {
//! 基本的な計算機能を提供します。
/// 2つの整数を加算します。
///
/// # Examples
///
/// ```
/// assert_eq!(2 + 3, 5);
/// ```
pub fn add(left: i32, right: i32) -> i32 {
left + right
}
}
assert_eq!(math::add(2, 3), 5);

Cargoでプロジェクトを作成・実行します。

Terminal window
cargo new hello-rust
cd hello-rust
cargo run

cargo new で作成したプロジェクトは、Cargo.tomledition = "2024" が設定されます。

公式リファレンス - キーワード

Rustのキーワードは、常に特別な意味を持つ厳密キーワード、将来のために確保された予約キーワード、特定の文脈だけで意味を持つ弱いキーワードに分類されます。

キーワード 主な用途
_ パターンで値を無視する、または型・値などを推論させるプレースホルダー
as プリミティブ型のキャスト、useでの別名、完全修飾構文
async Futureを返す非同期関数・ブロック・クロージャを定義
await Futureの完了まで現在の非同期処理を一時停止
break ループを終了し、必要に応じて値を返す
const コンパイル時定数、const関数・ブロック・ジェネリクスなどを指定
continue 現在のループ反復の残りを飛ばして次へ進む
crate 現在のcrate、またはcrateルートを参照
dyn 実行時に動的ディスパッチするトレイトオブジェクトを表す
else ifif letの条件が成立しない場合の分岐
enum 複数のバリアントを持つ列挙型を定義
extern 外部ABI、外部関数・static、外部crateを扱う
false bool型の偽リテラル
fn 関数または関数ポインタ型を定義
for イテレータによるループ、または対象型へのトレイト実装を記述
if 条件に応じて式を分岐
impl 型のメソッドやトレイト実装を定義
in forループで反復対象を指定
let パターンへ値を束縛して変数を宣言
loop 明示的に終了するまで繰り返すループを作成
match パターンマッチによって網羅的に分岐
mod moduleを宣言・定義
move クロージャなどへキャプチャした値を移動
mut 変数、参照、パターンなどを可変として指定
pub 項目を指定した範囲へ公開
ref パターン内で値を移動せず参照として束縛
return 関数から値を早期に返す
self メソッドのレシーバー、または現在のmoduleを参照
Self 現在定義・実装している型自身を参照
static 固定された格納場所を持つstatic項目を定義
struct 名前付き・タプル形式・ユニット形式の構造体を定義
super 親moduleを参照
trait 型が共有できる振る舞いを定義
true bool型の真リテラル
type 型エイリアスまたは関連型を定義
unsafe コンパイラだけでは安全性を保証できない操作や契約を明示
use パスを現在のスコープへ導入、または項目を再公開
where ジェネリクスやライフタイムの境界条件を記述
while 条件が成立している間ループを繰り返す

abstract, become, box, do, final, gen, macro, override, priv, try, typeof, unsized, virtual, yield

これらは将来の言語拡張のために予約されています。Rust 1.98ではキーワードとしての機能を持たず、通常の識別子にも使用できません。typeof も予約されていますが、型を取得する演算子としては実装されていません。

キーワード 意味を持つ文脈
'static プログラム全体にわたって有効なライフタイム
macro_rules 宣言的マクロを定義
raw &raw const&raw mutによる生ポインタの作成
safe unsafe externブロック内で、安全に呼び出せる関数・staticを宣言
union 複数フィールドがストレージを共有する共用体を定義

識別子にキーワードと同じ名前が必要な場合は、利用可能な文脈に限り r#type のようなraw identifierを使えます。


Rustの変数はデフォルトで不変です。値を変更する場合は mut を付けます。

let name = "Ferris"; // 型推論により &str
let age: u32 = 10; // 型を明示
let mut count = 0; // 可変変数
count += 1;
// パターンで分割代入
let (x, y) = (10, 20);
// 使用前に必ず初期化すれば、宣言時の初期値は省略できる
let status: String;
status = String::from("ready");
println!("{status}");

同じ名前を let で再宣言できます。mut と異なり、型も変更できます。

let spaces = " ";
let spaces = spaces.len(); // &str から usize に変更
const MAX_RETRIES: u32 = 3;
static APPLICATION_NAME: &str = "example";
  • const はコンパイル時に評価される値。型の明示が必要
  • static はプログラム全体で固定されたメモリアドレスを持つ値
  • 可変な static mut へのアクセスは unsafe が必要で、通常は同期プリミティブなどを検討する

複数スレッドから安全に更新する単純なカウンターには、アトミック型を利用できます。

use std::sync::atomic::{AtomicU64, Ordering};
static REQUEST_COUNT: AtomicU64 = AtomicU64::new(0);
fn next_request_count() -> u64 {
REQUEST_COUNT.fetch_add(1, Ordering::Relaxed) + 1
}
assert_eq!(next_request_count(), 1);
assert_eq!(next_request_count(), 2);

この例の Relaxed は、カウンター自身の不可分な更新だけが必要で、ほかのメモリアクセスとの順序を同期しない場合に適しています。状態間の順序保証が必要な処理では、要件に合う Ordering やロックを選びます。

リテラルはソースコードに直接記述する値です。数値には型サフィックスを付けられ、_ は桁区切りとして無視されます。

let integer = 42; // i32
let unsigned = 42_u64; // u64
let floating = 3.14_f32; // f32
let enabled = true; // bool
let character = 'R'; // char
let text = "Rust"; // &str
let byte = b'R'; // u8
let bytes = b"Rust"; // &[u8; 4]
let raw = r#"\をエスケープしない文字列"#;
let million = 1_000_000;
assert_eq!(integer, 42);
assert_eq!(bytes, &[82, 117, 115, 116]);
assert!(raw.contains('\\'));
assert_eq!(million, 1_000_000);

String::from("Rust") のような呼び出しで作られる値や、left + right のような式の評価結果も値です。Rustでは多くの構文が式であり、ブロック、ifmatchloop も値を返せます。

公式リファレンス - 型

分類 補足
符号付き整数 i8, i16, i32, i64, i128, isize 数値リテラルのデフォルトは i32
符号なし整数 u8, u16, u32, u64, u128, usize usize は添字やサイズで使用
浮動小数点 f32, f64 デフォルトは f64
真偽値 bool true, false
文字 char Unicodeスカラー値(4バイト)
ユニット () 値を返さないことを表す
let decimal = 98_222;
let hex = 0xff;
let octal = 0o77;
let binary = 0b1111_0000;
let byte = b'A'; // u8
let ratio = 3.5_f64;
let enabled = true;
let crab = '🦀';

デバッグビルドでは整数オーバーフローが実行時に検出されます。意図的に回り込ませる場合は wrapping_add などを使います。

let value = u8::MAX;
assert_eq!(value.wrapping_add(1), 0);
assert_eq!(value.saturating_add(1), u8::MAX);
assert_eq!(value.checked_add(1), None);
// タプル:異なる型をまとめられる
let user = ("Ferris", 10, true);
let (name, age, active) = user;
println!("{name}: {age}, active={active}");
// 配列:長さが型の一部で、スタック上に固定長で確保される
let numbers: [i32; 3] = [10, 20, 30];
let zeros = [0; 5];
println!("{} {}", numbers[0], zeros.len());

配列への [] アクセスは、範囲外の場合にpanicします。安全に取得するには get を使います。

let values = [10, 20, 30];
match values.get(5) {
Some(value) => println!("{value}"),
None => println!("範囲外です"),
}

数値型は暗黙変換されません。as または From / TryFrom を使います。

let small: u8 = 42;
let large = u32::from(small); // 損失のない変換
let integer = 42_i32;
let floating = integer as f64; // プリミティブ型のキャスト
assert_eq!(floating, 42.0);
let value: i32 = 300;
let byte = u8::try_from(value); // Result<u8, TryFromIntError>
assert!(byte.is_err());
let integer = 65_u32;
let letter = char::from_u32(integer); // Option<char>
assert_eq!(letter, Some('A'));

as は切り詰めなどが発生し得ます。入力値の範囲を保証できない場合は TryFrom を優先します。

let value = 300_u16;
assert_eq!(value as u8, 44); // 上位ビットが切り捨てられる
assert!(u8::try_from(value).is_err());

型エイリアスは既存の型に別名を付けます。新しい型は作られないため、元の型と相互に代入できます。

type UserId = u64;
type IoResult<T> = Result<T, std::io::Error>;
fn load_user(id: UserId) -> IoResult<String> {
Ok(format!("user-{id}"))
}
let id: UserId = 42;
let raw_id: u64 = id; // 同じ型として扱われる
assert_eq!(load_user(raw_id).unwrap(), "user-42");

値を混同できない別の型にしたい場合は、タプル構造体を使うnewtypeパターンを検討します。

struct UserId(u64);
struct ProductId(u64);
fn find_user(id: UserId) {
println!("user={}", id.0);
}
find_user(UserId(42));
// find_user(ProductId(42)); // 異なる型なのでコンパイルエラー

標準ライブラリ - std::any::type_name

Rustに組み込みの typeof 演算子はありません。型を明示して取得する場合は std::any::type_name::<T>()、値から確認する場合は std::any::type_name_of_val を利用できます。

fn type_of<T: ?Sized>(_: &T) -> &'static str {
std::any::type_name::<T>()
}
let number = 123;
println!("{}", type_of(&number));
println!("{}", std::any::type_name_of_val(&number));

値を直接受け取るヘルパーとして、次の形でも動作します。

fn type_of<T>(_: T) -> &'static str {
std::any::type_name::<T>()
}
let number = 123;
println!("{}", type_of(number));

ただし、この形は引数を値で受け取るため、String などの Copy ではない値を渡すと所有権が関数へ移動します。汎用ヘルパーとしては参照を受け取る形が扱いやすくなります。

type_name の出力形式は保証されておらず、コンパイラのバージョンによって変わる可能性があります。型の一意な識別やアプリケーションの分岐条件には使わず、診断やログ表示に利用します。

分類 演算子 補足
算術 +, -, *, /, % 整数同士の除算は小数部を切り捨てる
単項 -, ! 数値の符号反転、真偽値の否定・整数のビット反転
比較 ==, !=, <, <=, >, >= 型に応じて PartialEq / PartialOrd などを利用
論理 &&, ||, ! &&|| は短絡評価
ビット &, |, ^, <<, >> 整数のビット演算
代入 =, +=, -=, *=, /=, %=, &=, |=, ^=, <<=, >>= 複合代入を含む
範囲 .., ..= 終端を含まない/含む範囲
借用・デリファレンス &, &mut, * 参照の作成と参照先へのアクセス
エラー伝播 ? Result / Option などの失敗を早期リターン
assert_eq!(7 / 2, 3);
assert_eq!(7.0 / 2.0, 3.5);
assert_eq!(-5_i32, 0 - 5);
assert_eq!(!true, false);
let mut value = 5;
value *= 2;
assert_eq!(value, 10);
let mut flags = 0b0011_u8;
flags |= 0b0100;
flags <<= 1;
assert_eq!(flags, 0b1110);
let in_range = (1..=10).contains(&value);
assert!(in_range);

ユーザー定義型では、AddPartialEqIndex などの標準トレイトを実装することで対応する演算子を利用できます。任意の新しい演算子を定義したり、演算子の優先順位を変更したりすることはできません。


The Rust Programming Language - 所有権

Rustはガベージコレクタを使わず、所有権の規則をコンパイル時に検査してメモリを管理します。

基本規則は次の3つです。

  1. 各値には所有者が1つ存在する
  2. 同時に存在できる所有者は1つだけ
  3. 所有者がスコープを抜けると値は破棄される
let original = String::from("hello");
let moved = original; // Stringの所有権がmovedへ移動
println!("{moved}");
// println!("{original}"); // コンパイルエラー:移動後は使えない

i32 のように Copy を実装する型は、代入時に値がコピーされます。

let x = 5;
let y = x;
println!("{x} {y}"); // どちらも使用できる

独立したヒープデータを複製する場合は clone を使います。

let original = String::from("hello");
let cloned = original.clone();
println!("{original} {cloned}");

参照を渡すと、所有権を移動せずに値を借用できます。

fn length(text: &str) -> usize {
text.len()
}
let message = String::from("hello");
let size = length(&message);
println!("{message}: {size}");

借用の主な規則は次のとおりです。

  • 任意の数の不変参照 &T、または1つの可変参照 &mut T を作れる
  • 同じ値に対する不変参照と可変参照を同時に有効にはできない
  • 参照は常に有効な値を指す必要がある
fn append_world(text: &mut String) {
text.push_str(", world");
}
let mut message = String::from("hello");
append_world(&mut message);
println!("{message}");

スライスはコレクション内の連続した範囲を借用します。データそのものは所有しません。

let numbers = [10, 20, 30, 40];
let middle: &[i32] = &numbers[1..3];
assert_eq!(middle, &[20, 30]);
let text = String::from("hello world");
let hello: &str = &text[..5];
assert_eq!(hello, "hello");

文字列の範囲指定はUTF-8の文字境界でなければpanicします。文字単位の処理には chars、バイト単位には bytes を使います。

let text = "日本語";
let characters: Vec<char> = text.chars().collect();
assert_eq!(characters, vec!['日', '本', '語']);
assert_eq!(text.len(), 9); // UTF-8のバイト数
assert_eq!(text.chars().count(), 3); // Unicodeスカラー値の数

ライフタイムは参照が有効な範囲を表します。多くの場合はコンパイラが推論します。

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() >= y.len() { x } else { y }
}

'a は返される参照が、入力参照のうち短い方の有効期間を超えないことを示します。値の寿命を延ばす指定ではありません。

'static はプログラム全体にわたって有効なライフタイムです。文字列リテラルは &'static str です。


Rustの if は式なので値を返せます。条件式は必ず bool です。

let score = 80;
let grade = if score >= 80 { "A" } else { "B" };
let name = Some("Ferris");
if let Some(value) = name {
println!("{value}");
}
fn print_name(name: Option<&str>) {
let Some(name) = name else {
println!("名前がありません");
return;
};
println!("{name}");
}
// 無限ループ。breakで値を返せる
let mut count = 0;
let result = loop {
count += 1;
if count == 3 {
break count * 10;
}
};
assert_eq!(result, 30);
// while
while count > 0 {
count -= 1;
}
// for
for value in [10, 20, 30] {
println!("{value}");
}
for number in 1..=3 {
println!("{number}");
}
// continue:現在の反復の残りを飛ばす
for number in 1..=5 {
if number % 2 == 0 {
continue;
}
println!("odd: {number}");
}

for では into_iter が呼び出されます。コレクションを値で渡すと所有権が移動する場合があります。

let mut names = vec![String::from("Alice"), String::from("Bob")];
for name in &names { // iter(): &String
println!("{name}");
}
for name in &mut names { // iter_mut(): &mut String
name.push('!');
}
assert_eq!(names, vec!["Alice!", "Bob!"]);
// for name in names { } // into_iter(): String。namesを消費
'outer: for x in 0..3 {
for y in 0..3 {
if x + y == 3 {
break 'outer;
}
}
}

match はすべてのパターンを網羅する必要があります。

let number = 7;
let description = match number {
0 => "zero",
1..=9 => "one digit",
n if n % 2 == 0 => "even",
_ => "other",
};
println!("{description}");
let point = (0, 5);
match point {
(0, y) => println!("y軸上: {y}"),
(x, 0) => println!("x軸上: {x}"),
(x, y) => println!("({x}, {y})"),
}

引数と戻り値の型は明示します。最後の式に ; を付けなければ、その値が返されます。

fn add(left: i32, right: i32) -> i32 {
left + right
}
fn divide(left: f64, right: f64) -> Option<f64> {
if right == 0.0 {
return None;
}
Some(left / right)
}

文と式の違いに注意します。

let value = {
let base = 10; // 文
base * 2 // 式
};
assert_eq!(value, 20);

クロージャは周囲の変数を借用または取得できます。引数・戻り値の型は多くの場合推論されます。

let offset = 10;
let add_offset = |value: i32| value + offset;
assert_eq!(add_offset(5), 15);
let mut values = vec![3, 1, 2];
values.sort_by_key(|value| *value);

move を付けると、使用する値の所有権をクロージャへ移動します。スレッドに渡すときによく使います。

let message = String::from("hello");
let show = move || println!("{message}");
show();

クロージャは使用方法に応じて FnFnMutFnOnce のトレイトを実装します。

fn apply_twice<F>(mut operation: F, value: i32) -> i32
where
F: FnMut(i32) -> i32,
{
let once = operation(value);
operation(once)
}
assert_eq!(apply_twice(|x| x + 1, 10), 12);

環境をキャプチャしない関数やクロージャは fn 型として扱えます。

fn double(value: i32) -> i32 {
value * 2
}
fn calculate(operation: fn(i32) -> i32, value: i32) -> i32 {
operation(value)
}
assert_eq!(calculate(double, 5), 10);

構造体・共用体・列挙型・パターン

Section titled “構造体・共用体・列挙型・パターン”
#[derive(Debug, Clone, PartialEq)]
struct User {
name: String,
age: u32,
active: bool,
}
let name = String::from("Ferris");
let user = User {
name,
age: 10,
active: true,
};
let updated = User {
age: 11,
..user.clone()
};
println!("{updated:?}");

#[derive(...)] で標準トレイトの実装を自動生成できます。

トレイト 用途
Debug {:?} / {:#?} によるデバッグ表示
Clone 明示的な複製
Copy 代入時の暗黙コピー。Drop を実装する型には付けられない
PartialEq, Eq 等価比較
PartialOrd, Ord 順序比較
Hash HashMap / HashSet のキー
Default デフォルト値の生成

タプル構造体・ユニット構造体

Section titled “タプル構造体・ユニット構造体”
struct UserId(u64);
struct Point(i32, i32);
struct Marker;
let id = UserId(42);
let point = Point(10, 20);
println!("{} {}", id.0, point.1);

公式リファレンス - Union types

union は複数のフィールドが同じメモリ領域を共有する型です。主にCとのFFIや低レベルのデータ表現で利用します。

#[repr(C)]
union Number {
integer: i32,
floating: f32,
}
let number = Number { integer: 42 };
// SAFETY: numberはintegerフィールドで初期化している。
let integer = unsafe { number.integer };
assert_eq!(integer, 42);

フィールドへの書き込みは安全ですが、読み取りは格納されているビット列がそのフィールド型として有効かをコンパイラが確認できないため unsafe が必要です。union には「現在有効なフィールド」を記録する仕組みがないため、必要なら別のタグを持たせて管理します。

フィールド型には、破棄処理を必要としない型、参照、または ManuallyDrop<T> などの制約があります。通常のアプリケーションで複数種類の値を安全に表現する場合は、まず enum を検討します。

#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
// 関連関数。Rectangle::square(10) と呼ぶ
fn square(size: u32) -> Self {
Self {
width: size,
height: size,
}
}
// 不変借用
fn area(&self) -> u32 {
self.width * self.height
}
// 可変借用
fn resize(&mut self, width: u32, height: u32) {
self.width = width;
self.height = height;
}
// 所有権を受け取る
fn dimensions(self) -> (u32, u32) {
(self.width, self.height)
}
}
let mut rectangle = Rectangle::square(10);
rectangle.resize(20, 30);
assert_eq!(rectangle.area(), 600);
assert_eq!(rectangle.dimensions(), (20, 30));

各バリアントは異なる型・個数のデータを保持できます。

#[derive(Debug)]
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(u8, u8, u8),
}
fn handle(message: Message) {
match message {
Message::Quit => println!("quit"),
Message::Move { x, y } => println!("move to {x}, {y}"),
Message::Write(text) => println!("{text}"),
Message::ChangeColor(r, g, b) => println!("rgb({r}, {g}, {b})"),
}
}
handle(Message::Write(String::from("hello")));

Rustには null の代わりに Option<T> があります。

enum Option<T> {
None,
Some(T),
}
fn find_even(values: &[i32]) -> Option<i32> {
values.iter().copied().find(|value| value % 2 == 0)
}
let value = find_even(&[1, 3, 4, 5]);
assert_eq!(value, Some(4));
// 値があるときだけ変換
let doubled = value.map(|number| number * 2);
assert_eq!(doubled, Some(8));
// デフォルト値
assert_eq!(None::<i32>.unwrap_or(0), 0);

unwrapexpectNone でpanicします。回復可能な不在を通常の処理として扱う場合は、パターンマッチ、?unwrap_or などを使います。


Vec<T> は同じ型の値を連続したメモリ領域に保持する可変長配列です。

let mut values = Vec::with_capacity(4);
values.push(10);
values.extend([20, 30]);
assert_eq!(values[0], 10); // 範囲外はpanic
assert_eq!(values.get(1), Some(&20));
for value in &values {
println!("{value}");
}
let last = values.pop();
assert_eq!(last, Some(30));

vec! マクロで簡潔に生成できます。

let numbers = vec![1, 2, 3];
let zeros = vec![0; 5];
所有権 主な用途
String 所有する 可変・伸長可能なUTF-8文字列
&str 借用する 文字列スライス、関数の読み取り引数
let mut message = String::from("Hello");
message.push(',');
message.push_str(" Rust");
fn greet(name: &str) -> String {
format!("Hello, {name}!")
}
assert_eq!(greet(&message), "Hello, Hello, Rust!");

複数の文字列型を読み取り専用で受け取る関数は、通常 &String より &str を使うと柔軟です。

let first = String::from("Hello");
let second = String::from("Rust");
let joined = format!("{first}, {second}"); // 所有権を奪わない
let combined = first + ", " + &second; // firstは移動する
use std::collections::HashMap;
let mut scores = HashMap::new();
scores.insert(String::from("Alice"), 10);
scores.insert(String::from("Bob"), 20);
// 値を取得
if let Some(score) = scores.get("Alice") {
println!("{score}");
}
// キーがない場合だけ追加
scores.entry(String::from("Carol")).or_insert(0);
// 既存値を更新
*scores.entry(String::from("Alice")).or_insert(0) += 5;
for (name, score) in &scores {
println!("{name}: {score}");
}

HashMap の反復順序には依存しないでください。順序が必要なら BTreeMap を使います。

use std::collections::{BTreeMap, HashSet, VecDeque};
let unique: HashSet<_> = [1, 2, 2, 3].into_iter().collect();
assert_eq!(unique.len(), 3);
let mut sorted = BTreeMap::new();
sorted.insert("b", 2);
sorted.insert("a", 1); // キー順に反復できる
let mut queue = VecDeque::new();
queue.push_back("first");
queue.push_back("second");
assert_eq!(queue.pop_front(), Some("first"));

The Rust Programming Language - エラーハンドリング

Rustでは、回復可能なエラーに Result<T, E>、回復不能なバグに panic! を使います。

enum Result<T, E> {
Ok(T),
Err(E),
}
use std::fs;
use std::io;
fn read_config() -> Result<String, io::Error> {
match fs::read_to_string("config.toml") {
Ok(contents) => Ok(contents),
Err(error) => Err(error),
}
}

? は成功値を取り出し、エラーなら呼び出し元へ早期リターンします。必要なら From によるエラー変換も行います。

use std::fs;
use std::io;
fn read_config() -> Result<String, io::Error> {
let contents = fs::read_to_string("config.toml")?;
Ok(contents)
}

mainResult を返せます。

use std::error::Error;
use std::fs;
fn main() -> Result<(), Box<dyn Error>> {
let contents = fs::read_to_string("config.toml")?;
println!("{contents}");
Ok(())
}

Box<dyn Error> は例を簡潔にできますが、ライブラリでは呼び出し側が分類できる具体的なエラー型を検討します。

fn parse_number(text: &str) -> Result<i32, String> {
let number = text
.parse::<i32>()
.map_err(|error| format!("整数に変換できません: {error}"))?;
Ok(number)
}
assert_eq!(parse_number("42"), Ok(42));

よく使うメソッドは次のとおりです。

メソッド 概要
map Ok の値を変換
map_err Err の値を変換
and_then 成功時に別の Result を返す処理を連結
unwrap_or エラー時の値を指定
unwrap_or_else エラー時の値をクロージャで生成
inspect / inspect_err 値を変更せず副作用を実行

標準ライブラリだけで実装する例です。

use std::error::Error;
use std::fmt;
#[derive(Debug)]
enum ParseUserError {
EmptyName,
InvalidAge(std::num::ParseIntError),
}
impl fmt::Display for ParseUserError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::EmptyName => write!(formatter, "名前が空です"),
Self::InvalidAge(error) => write!(formatter, "年齢が不正です: {error}"),
}
}
}
impl Error for ParseUserError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
Self::InvalidAge(error) => Some(error),
Self::EmptyName => None,
}
}
}
impl From<std::num::ParseIntError> for ParseUserError {
fn from(error: std::num::ParseIntError) -> Self {
Self::InvalidAge(error)
}
}

アプリケーションや大規模なエラー型では、要件に応じて thiserroranyhow などの外部crateも検討できます。

panic!("継続できない状態です");
let value = Some(42).expect("値が存在する前提");
assert_eq!(value, 42);

unwrap / expect は、テスト、サンプル、または失敗がプログラムの不変条件違反を意味する箇所に限定すると意図が明確です。通常の入力エラーには Result / Option を使います。


ジェネリクス・トレイト・ライフタイム

Section titled “ジェネリクス・トレイト・ライフタイム”
fn largest<T: PartialOrd>(values: &[T]) -> Option<&T> {
let mut iterator = values.iter();
let mut largest = iterator.next()?;
for value in iterator {
if value > largest {
largest = value;
}
}
Some(largest)
}
assert_eq!(largest(&[3, 1, 4, 2]), Some(&4));
#[derive(Debug)]
struct Point<T> {
x: T,
y: T,
}
impl<T> Point<T> {
fn x(&self) -> &T {
&self.x
}
}

トレイトは複数の型が共有する振る舞いを定義します。

trait Summary {
fn summarize(&self) -> String;
fn category(&self) -> &'static str {
"general"
}
}
struct Article {
title: String,
}
impl Summary for Article {
fn summarize(&self) -> String {
self.title.clone()
}
}
use std::fmt::{Debug, Display};
// impl Trait構文
fn print_summary(item: &impl Display) {
println!("{item}");
}
// トレイト境界構文
fn print_debug<T: Debug>(item: &T) {
println!("{item:?}");
}
// 複数の境界とwhere句
fn compare_and_print<T, U>(left: &T, right: &U)
where
T: Display + PartialOrd<U>,
U: Display,
{
println!("{left} / {right}");
}

関連型はトレイト実装ごとに具体型を1つ定めます。

trait Repository {
type Item;
type Error;
fn find(&self, id: u64) -> Result<Option<Self::Item>, Self::Error>;
}

標準ライブラリの Iteratortype Item という関連型を持ちます。

静的ディスパッチと動的ディスパッチ

Section titled “静的ディスパッチと動的ディスパッチ”
use std::fmt::Display;
// コンパイル時に具体型が決まる。単相化される
fn static_dispatch(value: &impl Display) {
println!("{value}");
}
// 実行時にvtable経由で呼び出す
fn dynamic_dispatch(value: &dyn Display) {
println!("{value}");
}

異なる具体型を同じコレクションに入れる場合はトレイトオブジェクトを使えます。

use std::fmt::Display;
let values: Vec<Box<dyn Display>> = vec![
Box::new(42),
Box::new(String::from("hello")),
];
struct Excerpt<'a> {
text: &'a str,
}
impl<'a> Excerpt<'a> {
fn text(&self) -> &str {
self.text
}
}

所有する String を構造体に持たせれば、構造体のライフタイム注釈が不要になることがあります。借用が本当に必要かも含めて設計します。


イテレータは遅延評価されます。mapfilter などのアダプタだけでは処理されず、collectsum などの消費アダプタで評価されます。

let result: Vec<i32> = (1..=10)
.filter(|number| number % 2 == 0)
.map(|number| number * number)
.collect();
assert_eq!(result, vec![4, 16, 36, 64, 100]);
let values = [10, 20, 30];
let indexed: Vec<_> = values.iter().enumerate().collect();
let total: i32 = values.iter().sum();
let found = values.iter().find(|&&value| value >= 20);
let all_positive = values.iter().all(|value| *value > 0);
let first_two: Vec<_> = values.iter().take(2).copied().collect();
assert_eq!(total, 60);
assert_eq!(found, Some(&20));
assert!(all_positive);
assert_eq!(first_two, vec![10, 20]);
呼び出し 要素 コレクション
iter() &T 借用する
iter_mut() &mut T 可変借用する
into_iter() 通常 T 消費する
let mut values = vec![1, 2, 3];
for value in values.iter_mut() {
*value *= 2;
}
assert_eq!(values, vec![2, 4, 6]);
let total: i32 = values.into_iter().sum();
// valuesはここでは使用できない
assert_eq!(total, 12);
struct Counter {
current: u32,
end: u32,
}
impl Iterator for Counter {
type Item = u32;
fn next(&mut self) -> Option<Self::Item> {
if self.current >= self.end {
return None;
}
self.current += 1;
Some(self.current)
}
}
let values: Vec<_> = Counter { current: 0, end: 3 }.collect();
assert_eq!(values, vec![1, 2, 3]);

Box<T> は値をヒープに配置し、単一の所有権を持ちます。再帰型や大きな値の移動コストを一定にしたい場合などに使います。

enum List {
Cons(i32, Box<List>),
Nil,
}
let list = List::Cons(1, Box::new(List::Cons(2, Box::new(List::Nil))));
  • Rc<T>:単一スレッド内の参照カウント共有所有権
  • Arc<T>:複数スレッド間で使えるアトミック参照カウント共有所有権
  • どちらもデフォルトでは内部の値を変更できない
use std::rc::Rc;
let shared = Rc::new(String::from("shared"));
let first = Rc::clone(&shared);
let second = Rc::clone(&shared);
assert_eq!(Rc::strong_count(&shared), 3);
assert_eq!(first.as_str(), second.as_str());

外側が不変でも内部を変更できる「内部可変性」を提供します。

use std::cell::RefCell;
let values = RefCell::new(vec![1, 2]);
values.borrow_mut().push(3);
assert_eq!(*values.borrow(), vec![1, 2, 3]);

RefCell<T> は借用規則を実行時に検査し、違反するとpanicします。スレッドセーフではありません。

struct Connection;
impl Drop for Connection {
fn drop(&mut self) {
println!("接続を閉じます");
}
}
let connection = Connection;
drop(connection); // スコープ末尾より前に明示的に破棄

RustではRAIIにより、値が破棄されるとロックやファイルなどのリソースも解放されます。


モジュール・パッケージ・可視性

Section titled “モジュール・パッケージ・可視性”

Cargoの用語は次のとおりです。

用語 概要
package Cargo.toml が定義する1つ以上のcrate
crate コンパイル単位。バイナリcrateまたはライブラリcrate
module crate内の名前空間・可視性を構成する単位
mod network {
pub mod client {
pub fn connect() {
println!("connected");
}
}
fn private_helper() {}
}
use network::client;
fn main() {
client::connect();
}

項目はデフォルトで非公開です。

指定 可視範囲
pub 外部から公開
pub(crate) 同じcrate内
pub(super) 親モジュール内
pub(in path) 指定した祖先モジュール内
src/
├── main.rs # バイナリcrateのルート
├── lib.rs # ライブラリcrateのルート
├── config.rs # mod config;
└── network/
├── mod.rs # mod network; の従来形式
└── client.rs

現在は network.rsnetwork/client.rs を組み合わせる構成も利用できます。

use std::collections::HashMap;
use std::fmt::{self, Display};
use std::io::Result as IoResult;
mod models {
pub struct User;
}
pub use models::User; // crateルートでは外部へ再公開

The Rust Programming Language - 並行処理

Rustでは所有権と Send / Sync トレイトにより、多くの並行処理上の誤りをコンパイル時に防ぎます。

use std::thread;
let values = vec![1, 2, 3];
let handle = thread::spawn(move || {
let total: i32 = values.iter().sum();
total
});
let total = handle.join().expect("スレッドがpanicしました");
assert_eq!(total, 6);

スコープ付きスレッドなら、現在のスコープの値を所有権移動なしで借用できます。

use std::thread;
let values = vec![1, 2, 3];
thread::scope(|scope| {
scope.spawn(|| println!("{values:?}"));
});

標準の mpsc チャネルは複数送信者・単一受信者です。

use std::sync::mpsc;
use std::thread;
let (sender, receiver) = mpsc::channel();
for id in 0..2 {
let sender = sender.clone();
thread::spawn(move || {
sender.send(format!("worker {id}")).unwrap();
});
}
drop(sender); // 元の送信側を閉じる
for message in receiver {
println!("{message}");
}

複数スレッドで可変状態を共有する代表的な組み合わせです。

use std::sync::{Arc, Mutex};
use std::thread;
let counter = Arc::new(Mutex::new(0));
let mut handles = Vec::new();
for _ in 0..10 {
let counter = Arc::clone(&counter);
handles.push(thread::spawn(move || {
let mut value = counter.lock().expect("mutexがpoisonされました");
*value += 1;
}));
}
for handle in handles {
handle.join().unwrap();
}
assert_eq!(*counter.lock().unwrap(), 10);

ロックガードはスコープを抜けると自動的に解放されます。複数ロックを使う場合は取得順序を統一し、ロック保持時間を短くします。

  • Send:値の所有権を別スレッドへ移動できる
  • Sync&T を複数スレッド間で安全に共有できる

これらはマーカートレイトで、多くの型には構成要素に基づいて自動実装されます。手動の unsafe impl は安全性の不変条件を実装者が保証できる場合に限ります。

Rustの標準ライブラリは Future と構文を提供しますが、汎用の非同期ランタイムは提供しません。実際のI/O処理ではTokioなどのランタイムを用途に応じて選びます。

async fn fetch_value() -> u32 {
42
}
async fn calculate() -> u32 {
let value = fetch_value().await;
value * 2
}

async fn の呼び出しは遅延評価される Future を返します。.await するかランタイム上で実行するまで処理は進みません。


let name = "Ferris";
let error = "接続に失敗しました";
println!("name={name}");
eprintln!("error: {error}");
let message = format!("id={}", 42);
let values = vec![1, 2, 3];
assert!(values.contains(&2));
assert_eq!(values.len(), 3);
assert_ne!(message, "");
todo!("未実装");
unreachable!("この分岐には到達しない前提");

todo!unimplemented!unreachable! は実行されるとpanicします。

macro_rules! hash_map {
($($key:expr => $value:expr),* $(,)?) => {{
let mut map = std::collections::HashMap::new();
$(map.insert($key, $value);)*
map
}};
}
let scores = hash_map! {
"Alice" => 10,
"Bob" => 20,
};

手続き的マクロにはカスタムderive、属性マクロ、関数形式マクロがあり、別の proc-macro crateとして定義します。

#[derive(Debug, Clone, PartialEq)]
struct User;
#[cfg(target_os = "linux")]
fn platform_name() -> &'static str {
"Linux"
}
#[must_use]
fn calculate() -> i32 {
42
}

条件付きコンパイルはCargo featureにも利用できます。

#[cfg(feature = "json")]
mod json_support;

Cargo Book - cargo test

pub fn add(left: i32, right: i32) -> i32 {
left + right
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn adds_two_numbers() {
assert_eq!(add(2, 3), 5);
}
#[test]
#[should_panic(expected = "範囲外")]
fn panics_when_out_of_range() {
panic!("範囲外です");
}
#[test]
#[ignore = "外部サービスが必要"]
fn integration_like_test() {}
}
#[test]
fn parses_number() -> Result<(), Box<dyn std::error::Error>> {
let value: i32 = "42".parse()?;
assert_eq!(value, 42);
Ok(())
}

tests/ 配下の各ファイルは別のcrateとしてコンパイルされ、ライブラリの公開APIをテストします。

src/lib.rs
tests/api_test.rs
tests/api_test.rs
use my_crate::add;
#[test]
fn public_api_adds_numbers() {
assert_eq!(add(2, 3), 5);
}
/// 2つの整数を加算します。
///
/// # Examples
///
/// ```
/// let result = my_crate::add(2, 3);
/// assert_eq!(result, 5);
/// ```
pub fn add(left: i32, right: i32) -> i32 {
left + right
}

cargo test は通常のテストに加えてドキュメントテストも実行します。

Terminal window
cargo test # すべてのテスト
cargo test add # 名前で絞り込み
cargo test -- --nocapture # 標準出力を表示
cargo test -- --ignored # ignoreされたテスト
cargo test --doc # ドキュメントテストのみ
cargo test --all-features # 全featureを有効化

let name = "Ferris";
let age = 10;
println!("{name} is {age}");
println!("debug: {:?}", vec![1, 2, 3]);
println!("pretty: {:#?}", vec![1, 2, 3]);
println!("hex: {age:x}, binary: {age:b}");
println!("right: {:>8}", name);
println!("zero pad: {age:04}");
let text = " Hello, Rust ";
assert_eq!(text.trim(), "Hello, Rust");
assert!(text.contains("Rust"));
assert!(text.trim().starts_with("Hello"));
assert_eq!(text.replace("Rust", "World"), " Hello, World ");
let parts: Vec<_> = "a,b,c".split(',').collect();
assert_eq!(parts, vec!["a", "b", "c"]);
let joined = ["a", "b", "c"].join("-");
assert_eq!(joined, "a-b-c");
fn main() -> Result<(), Box<dyn std::error::Error>> {
let number: i32 = "42".parse()?;
let hex = i32::from_str_radix("ff", 16)?;
let text = number.to_string();
assert_eq!(hex, 255);
assert_eq!(text, "42");
Ok(())
}
use std::fs;
use std::io;
use std::path::{Path, PathBuf};
fn copy_file() -> io::Result<()> {
let path = Path::new("data").join("input.txt");
let contents = fs::read_to_string(&path)?;
fs::write("output.txt", contents)?;
let mut buffer = PathBuf::from("data");
buffer.push("output.json");
Ok(())
}

大きなファイルは全体をメモリに読み込まず、BufReader で逐次処理できます。

use std::fs::File;
use std::io::{self, BufRead, BufReader};
fn print_lines(path: &str) -> io::Result<()> {
let file = File::open(path)?;
for line in BufReader::new(file).lines() {
println!("{}", line?);
}
Ok(())
}

環境変数・コマンドライン引数

Section titled “環境変数・コマンドライン引数”
use std::env;
let args: Vec<String> = env::args().collect();
match env::var("APP_ENV") {
Ok(value) => println!("APP_ENV={value}"),
Err(env::VarError::NotPresent) => println!("未設定"),
Err(env::VarError::NotUnicode(_)) => println!("Unicodeではありません"),
}

シークレットをログやエラーメッセージに出力しないよう注意します。

use std::thread;
use std::time::{Duration, Instant, SystemTime};
let started = Instant::now();
thread::sleep(Duration::from_millis(10));
println!("elapsed={:?}", started.elapsed());
let now = SystemTime::now();

標準ライブラリは日時の書式化やタイムゾーン処理を広範には提供しません。必要な場合は要件に合う外部crateを選びます。

use std::sync::{LazyLock, OnceLock};
static CONFIG: OnceLock<String> = OnceLock::new();
static DEFAULTS: LazyLock<Vec<String>> =
LazyLock::new(|| vec![String::from("default")]);
let config = CONFIG.get_or_init(|| String::from("production"));
assert_eq!(config, "production");
assert_eq!(DEFAULTS.len(), 1);

外部関数の呼び出し(extern・FFI)

Section titled “外部関数の呼び出し(extern・FFI)”

公式リファレンス - External blocks

extern はRust以外のABIを使う関数やstatic項目を宣言するために使います。extern "C" はC ABIを表します。

Edition 2024では、外部ブロックを unsafe extern として宣言する必要があります。宣言した関数のシグネチャが実際の外部関数と一致することはRustコンパイラが検証できないため、宣言者が安全性を保証します。

use std::ffi::c_int;
unsafe extern "C" {
fn abs(input: c_int) -> c_int;
}
// SAFETY: Cのabsへ、有効範囲内のc_int値を渡している。
let value = unsafe { abs(-42) };
assert_eq!(value, 42);

Rustの関数をC ABIで外部へ公開する場合も extern "C" を使います。

#[unsafe(no_mangle)]
pub extern "C" fn add(left: i32, right: i32) -> i32 {
left + right
}
assert_eq!(add(2, 3), 5);

Edition 2024では no_mangle はunsafe属性のため、#[unsafe(no_mangle)] と記述します。

FFI境界では次の点を明示的に確認します。

  • ABIと関数シグネチャが外部定義と一致していること
  • 共有する構造体などに必要に応じて #[repr(C)] を指定すること
  • ポインタのnull、有効範囲、アラインメント、ライフタイム
  • 文字列のエンコーディングと終端方法
  • メモリをどちらが確保・解放するか
  • panicや外部例外をABI境界を越えて伝播させないこと

外部ヘッダーから多数の宣言を取り込む場合は、手書きによる不一致を避けるため、用途に応じてbindgenなどの生成ツールも検討します。


The Rust Programming Language - unsafe Rust

unsafe は借用検査を無効にするものではなく、安全性をコンパイラだけでは証明できない一部の操作を許可します。

unsafeコンテキストで可能になる主な操作は次のとおりです。

  • 生ポインタのデリファレンス
  • unsafe関数・メソッドの呼び出し
  • 可変static変数へのアクセス
  • unsafeトレイトの実装
  • union フィールドへのアクセス
unsafe fn first_unchecked(values: &[i32]) -> &i32 {
// SAFETY: 呼び出し側がvaluesは空でないことを保証する。
unsafe { values.get_unchecked(0) }
}
let values = [10, 20];
// SAFETY: valuesには2要素あり、空ではない。
let first = unsafe { first_unchecked(&values) };
assert_eq!(*first, 10);

Edition 2024では、unsafe fn の本体でもunsafe操作を明示的な unsafe ブロックに入れる必要があります。unsafeコードは範囲を小さくし、安全性の前提を SAFETY コメントで記録します。

安全なAPIとして公開する場合は、内部のすべての呼び出しに対して安全性の不変条件が成り立つよう設計・検証する必要があります。


The Cargo Book

Terminal window
cargo new app # 新規バイナリpackage
cargo new --lib my-library # 新規ライブラリpackage
cargo init # 既存ディレクトリを初期化
cargo check # 高速な型チェック
cargo build # デバッグビルド
cargo build --release # 最適化ビルド
cargo run -- arg1 arg2 # 実行ファイルへ引数を渡す
cargo test # テスト
cargo doc --open # ドキュメント生成・表示
cargo fmt --check # フォーマット確認
cargo clippy --all-targets --all-features -- -D warnings

cargo check は最終バイナリを生成しないため、実装中の確認に適しています。リリース前は cargo build / cargo test も実行します。

[package]
name = "example"
version = "0.1.0"
edition = "2024"
rust-version = "1.98"
[dependencies]
[dev-dependencies]
[features]
default = []
json = []

editionrust-version は別の設定です。

  • edition:利用する言語Editionと移行上の互換性境界
  • rust-version:packageがサポートする最小Rustバージョン(MSRV)

ライブラリでは、実際にサポート・CI検証する最小バージョンを rust-version に指定します。

Terminal window
cargo add serde --features derive
cargo add --dev tempfile
cargo update
cargo tree
cargo tree -d # 重複バージョンを確認

Cargo.lock は再現可能なビルドのための正確な依存バージョンを記録します。バイナリ・アプリケーションでは通常コミットします。ライブラリでもワークスペースやCIの再現性のためコミットできますが、利用者が解決する依存バージョンは Cargo.toml の制約に従います。

[features]
default = []
json = ["dep:serde", "dep:serde_json"]
[dependencies]
serde = { version = "1", features = ["derive"], optional = true }
serde_json = { version = "1", optional = true }
Terminal window
cargo build --features json
cargo test --all-features
cargo test --no-default-features

featureは加算的に設計することが推奨されます。同じfeature名を無効化の意味に使うと、依存グラフ上で組み合わされたときに予期しない動作になりやすくなります。

[workspace]
resolver = "3"
members = ["crates/core", "crates/cli"]
[workspace.package]
edition = "2024"
rust-version = "1.98"
[workspace.dependencies]
serde = { version = "1", features = ["derive"] }
crates/core/Cargo.toml
[package]
name = "example-core"
version = "0.1.0"
edition.workspace = true
rust-version.workspace = true
[dependencies]
serde.workspace = true

借用で十分な箇所に clone を追加すると、所有権エラーは消えても余分な割り当てやコピーが発生します。

fn print_name(name: &str) {
println!("{name}");
}
let name = String::from("Ferris");
print_name(&name); // clone不要
println!("{name}");

まず所有権を誰が持つべきか、関数が値を所有する必要があるかを検討します。

fn length(text: &str) -> usize {
text.len()
}
length("literal");
length(&String::from("owned"));

読み取り専用の文字列引数は &str にすると、文字列リテラルと String の両方を受け取れます。同様に、読み取り専用の可変長配列は &Vec<T> より &[T] が一般的です。

let values = vec![10, 20, 30];
// 入力に由来する添字など、範囲外になり得る場合
if let Some(value) = values.get(10) {
println!("{value}");
}

values[index] は範囲外でpanicします。不確実な添字には get を使います。

let text = "🦀Rust";
assert_eq!(text.len(), 8); // バイト数
let first = text.chars().next();
assert_eq!(first, Some('🦀'));

String / str は整数による直接インデックスを提供しません。書記素クラスタ(見た目上の文字)単位の処理が必要なら、Unicode仕様を扱う外部crateを検討します。

Mutexガードをawaitをまたいで保持しない

Section titled “Mutexガードをawaitをまたいで保持しない”

同期 Mutex のガードを .await の間保持すると、デッドロックやタスク停滞の原因になります。必要な値を取り出してガードを先に破棄するか、非同期ランタイム向けのMutexを要件に応じて使います。

use std::sync::Mutex;
async fn async_operation(_value: String) {}
async fn process(shared: &Mutex<String>) {
let value = {
let guard = shared.lock().expect("mutexがpoisonされました");
guard.clone()
}; // ここでガードを解放
async_operation(value).await;
}

ブロッキング処理をasyncタスクで直接実行しない

Section titled “ブロッキング処理をasyncタスクで直接実行しない”

CPU負荷の高い処理や同期I/Oを非同期executor上で直接実行すると、他のタスクを止めることがあります。利用するランタイムが提供するブロッキング処理用APIや専用スレッドを使います。

ライブラリの公開APIでは、通常の入力でpanicせず Result / Option を返す設計を検討します。panicする条件がある場合は、ドキュメントの # Panics セクションに記載します。

/// 指定位置の値を返します。
///
/// # Panics
///
/// `index` が範囲外の場合にpanicします。
pub fn value_at(values: &[i32], index: usize) -> i32 {
values[index]
}
Terminal window
cargo fmt --check
cargo clippy --all-targets --all-features -- -D warnings
cargo test --all-features

Clippyの提案は有用ですが、意味や性能特性が変わらないか確認して適用します。プロジェクト固有のlint方針がある場合はそちらを優先します。



Rustでは、所有権・借用・型によって不正な状態を表現しにくくすることが重要です。 コンパイラのエラーは制約だけでなく、設計上の問題を早い段階で見つける手がかりとして活用できます。