Rust

Rust入門

コードを実行し、コンパイラーの指摘を読みながら、Rustの基礎を一歩ずつ学びましょう。

Rust stable(Edition 2021以降、Rust 1.58以降)を対象にしています。

01

Rustとは

Rustは、ネイティブコードの高速さとメモリ安全性を両立することを目指すプログラミング言語です。Webサーバー、CLIツール、組み込み、ゲーム、WebAssemblyなどで利用できます。

Rustの特徴
  • 所有権の仕組みで、解放済みメモリの利用などをコンパイル時に防ぐ
  • 型を明示しつつ、型推論で読みやすく書ける
  • Cargoがビルド、依存関係、テストをまとめて管理する

Rustは「エラーをなくす魔法」ではありません。安全でない可能性をコンパイラーが知らせてくれるので、指摘を読みながら設計を改善する言語です。

02

環境構築と実行

公式のRustupを使うと、RustコンパイラーとCargoを導入できます。導入後、バージョンを確認します。

ターミナルCOMMAND
rustc --version
cargo --version

Cargoでプロジェクトを作り、src/main.rsを実行します。

作成と実行COMMAND
cargo new hello-rust
cd hello-rust
cargo run
src/main.rsRUST
fn main() {
    println!("こんにちは、Rust!");
}
実行結果OUTPUT
こんにちは、Rust!
コマンドの役割Cargoがコンパイルして実行します。配布用にはcargo build --release、テストにはcargo testを使います。
03

基本の書き方

Rustの文は基本的にセミコロンで終わります。関数の最後の式は、セミコロンを付けなければ戻り値になります。

式とコメントRUST
fn main() {
    // これはコメントです
    let total = add(12, 8);
    println!("合計: {total}");
}

fn add(left: i32, right: i32) -> i32 {
    left + right
}
実行結果OUTPUT
合計: 20

println!の末尾の!はマクロであることを示します。{total}のような書式指定で値を埋め込めます。

04

変数と型

letで変数を宣言します。Rustの変数はデフォルトで変更不可です。変更したいときだけmutを付けます。

型とmutRUST
fn main() {
    let name: &str = "Mity";
    let mut score: i32 = 70;
    score += 15;
    let passed = score >= 60;
    println!("{name}: {score}点、合格={passed}");
}
実行結果OUTPUT
Mity: 85点、合格=true

整数にはi32、小数にはf64、真偽値にはbool、文字列リテラルには&strをよく使います。型を省略しても推論できる場合があります。

数値の型に注意異なる整数型を暗黙に混ぜることはできません。asは値が範囲外で切り詰められる可能性があるため、変換元が確実に範囲内のときに使います。失敗を扱うならu32::try_from(value)のような変換を選びます。
05

条件分岐

ifの条件は必ずboolです。Rustではifも式なので、値を変数へ代入できます。

点数を判定RUST
fn main() {
    let score = 82;
    let result = if score >= 80 { "たいへんよくできました" } else if score >= 60 { "合格" } else { "再挑戦" };
    println!("{result}");
}
実行結果OUTPUT
たいへんよくできました

条件によって返す値の型は揃える必要があります。数値と文字列を同じif式から返すことはできません。

06

繰り返し

Rustのforは範囲やコレクションを順番に処理します。1..=4は4を含み、1..4は4を含みません。

forとwhileRUST
fn main() {
    let mut total = 0;
    for number in 1..=4 { total += number; }
    let mut count = 3;
    while count > 0 {
        println!("あと{count}回");
        count -= 1;
    }
    println!("合計: {total}");
}
実行結果OUTPUT
あと3回
あと2回
あと1回
合計: 10
無限ループを防ぐwhileでは条件に関係する値を必ず更新します。途中で終えるbreak、今回だけ飛ばすcontinueも使えます。
07

関数

関数はfnで定義し、引数の型と戻り値の型を書きます。値を返すだけなら最後の式にします。

関数RUST
fn shipping_fee(weight: u32) -> u32 {
    if weight <= 2 { 300 } else { 600 }
}
fn main() { println!("送料: {}円", shipping_fee(3)); }
実行結果OUTPUT
送料: 600円

関数を小さく分けると、入力・処理・出力を個別にテストできます。表示を担当する関数と、計算して値を返す関数を分けましょう。

08

所有権

Rustの各値には所有者が一人います。所有者がスコープを抜けると値は自動的に片付けられます。Stringを別の変数へ代入すると、二重解放を防ぐため所有権が移動します。

所有権の移動RUST
fn main() {
    let first = String::from("Rust");
    let second = first; // 所有権がsecondへ移る
    println!("{second}");
}
実行結果OUTPUT
Rust

この直後にprintln!("{first}")と書くと、borrow of moved valueというコンパイルエラーになります。let second = first;let second = first.clone();へ変えると、値を複製できます。ただし、借用で済むかを先に考えましょう。

moveとCopy整数などの単純な型はCopyなので代入後も使えます。一方、StringVecは通常moveします。
09

借用とスライス

参照(&)を渡すと、所有権を移動せずに値を読み取れます。読み取り参照は複数作れますが、変更可能な参照は同時に一つだけです。

文字列を借用するRUST
fn length(text: &str) -> usize { text.len() }
fn main() {
    let message = String::from("こんにちは");
    println!("{}バイト", length(&message));
    println!("{message}");
}
実行結果OUTPUT
15バイト
こんにちは

読み取るだけなら&Stringより&strを受け取るほうが、Stringと文字列リテラルの両方に使えて柔軟です。

変更可能な借用RUST
fn add_mark(text: &mut String) { text.push('!'); }
fn main() {
    let mut message = String::from("Hello");
    add_mark(&mut message);
    println!("{message}");
}
実行結果OUTPUT
Hello!

スライスで一部を参照する

スライスは、配列やベクターなどの連続した要素の一部を、所有権を持たずに参照する型です。整数の列なら&[i32]のように表します。範囲の終わりを含まない1..3では、位置1と2を取り出します。

ベクターのスライスRUST
fn show_middle(values: &[i32]) {
    println!("{:?}", &values[1..3]);
}
fn main() {
    let values = vec![10, 20, 30, 40];
    show_middle(&values);
}
実行結果OUTPUT
[20, 30]
10

構造体とメソッド

構造体は関連するデータを一つにまとめます。implブロックでメソッドを定義できます。

商品を表す構造体RUST
struct Product { name: String, price: u32 }
impl Product {
    fn discounted_price(&self, percent: u32) -> u32 {
        self.price * (100 - percent) / 100
    }
}
fn main() {
    let book = Product { name: String::from("Rust本"), price: 3000 };
    println!("{}: {}円", book.name, book.discounted_price(10));
}
実行結果OUTPUT
Rust本: 2700円

&selfは読み取り専用、&mut selfは変更可能、selfは所有権を受け取るメソッドです。

11

列挙型とmatch

列挙型は値が取りうる種類を表します。matchは全パターンを扱う必要があるため、処理漏れに気付きやすくなります。

注文状態RUST
enum Status { Preparing, Shipped { tracking_number: String }, Delivered }
fn message(status: Status) -> String {
    match status {
        Status::Preparing => String::from("準備中"),
        Status::Shipped { tracking_number } => format!("発送済み: {tracking_number}"),
        Status::Delivered => String::from("配達済み"),
    }
}
fn main() { println!("{}", message(Status::Shipped { tracking_number: String::from("ABC123") })); }
実行結果OUTPUT
発送済み: ABC123
12

コレクション

Vec<T>は伸縮する配列、HashMap<K, V>はキーと値の組、Stringは所有するUTF-8文字列です。

ベクターと集計RUST
use std::collections::HashMap;
fn main() {
    let scores = vec![70, 85, 90];
    let average: u32 = scores.iter().sum::<u32>() / scores.len() as u32;
    let mut counts = HashMap::new();
    counts.insert("Rust", 3);
    println!("平均: {average}、学習回数: {}", counts["Rust"]);
}
実行結果OUTPUT
平均: 81、学習回数: 3
整数除算と空のベクター245 / 3のような整数除算は小数部分を切り捨てるため、平均は81になります。scores.len()が0のときは0で割ることになりパニックするので、空の場合を先に判定しましょう。
添字アクセスの注意scores[10]は範囲外でパニックになります。存在しない可能性があるならget(10)Optionとして扱います。
13

OptionとResult

値がない状態をOption<T>、失敗する可能性をResult<T, E>で表します。nullや例外に任せず、呼び出し側へ判断を促す設計です。

OptionRUST
fn first_word(text: &str) -> Option<&str> {
    text.split_whitespace().next()
}
fn main() {
    let word = first_word("").unwrap_or("単語なし");
    println!("{word}");
}
実行結果OUTPUT
単語なし
ResultRUST
fn parse_age(text: &str) -> Result<u32, std::num::ParseIntError> { text.parse() }
fn main() {
    match parse_age("20") {
        Ok(age) => println!("{age}歳"),
        Err(error) => println!("入力エラー: {error}"),
    }
}
実行結果OUTPUT
20歳

?演算子を使うと、途中の処理が失敗した時点で、そのエラーを呼び出し元へ返せます。成功した値だけを次の処理へ渡せるため、エラー処理の分岐を何度も書かずに済みます。

?で2つの値を処理するRUST
fn add_ages(left: &str, right: &str) -> Result<u32, std::num::ParseIntError> {
    let left_age = left.parse::<u32>()?;
    let right_age = right.parse::<u32>()?;
    Ok(left_age + right_age)
}
fn main() {
    println!("{:?}", add_ages("20", "3"));
}
実行結果OUTPUT
Ok(23)

unwrapは失敗時に停止するため、学習用の確認以外ではmatchunwrap_or?演算子などで処理します。

14

トレイトとジェネリクス

トレイトは型が持つ共通の振る舞いを定義します。ジェネリクスと組み合わせると、特定の型に依存しない関数を書けます。

比較できる型を受け取るRUST
fn larger<T: PartialOrd>(left: T, right: T) -> T {
    if left > right { left } else { right }
}
fn main() { println!("{}", larger(12, 8)); }
実行結果OUTPUT
12

PartialOrdという境界があるので比較できる型だけを受け取れます。必要な能力をトレイト境界で表すことが、汎用的で安全なコードにつながります。

15

Cargoとテスト

Cargo.tomlはプロジェクト名や依存クレートを管理し、src以下のモジュールでコードを分割します。整形はcargo fmt、静的な確認はcargo clippy、テストはcargo testです。

よく使うコマンドCOMMAND
cargo new todo-cli
cargo check
cargo fmt
cargo clippy
cargo test
cargo build --release
src/main.rs(cargo newで作るバイナリ)RUST
fn add(a: i32, b: i32) -> i32 { a + b }

fn main() {
    println!("{}", add(2, 3));
}

#[cfg(test)]
mod tests {
    use super::*;
    #[test]
    fn add_two_numbers() { assert_eq!(add(2, 3), 5); }
}
実行結果OUTPUT
5

外部クレートを追加するときは用途・ライセンス・更新状況を確認し、標準ライブラリで十分かも考えます。

16

練習問題

答えを見ずに、入力と出力を自分で決めて実装しましょう。各問題は複数の章を組み合わせる練習です。

問題1:偶数だけの合計

[3, 4, 8, 11]から偶数だけを取り出して合計し、12を表示する関数を作ってください。

解答例RUST
fn even_sum(numbers: &[i32]) -> i32 {
    numbers.iter().copied().filter(|number| number % 2 == 0).sum()
}
fn main() { println!("{}", even_sum(&[3, 4, 8, 11])); }
実行結果OUTPUT
12
問題2:安全な割り算

0で割る場合はErr、それ以外は商を返すdivide関数を作ってください。

解答例RUST
fn divide(a: i32, b: i32) -> Result<i32, &'static str> {
    if b == 0 { Err("0では割れません") } else { Ok(a / b) }
}
fn main() { println!("{:?}", divide(10, 0)); }
実行結果OUTPUT
Err("0では割れません")
問題3:単語の出現回数

文章を空白で分割し、HashMapで各単語の回数を数えてください。

解答例RUST
use std::collections::HashMap;
fn main() {
    let mut counts = HashMap::new();
    for word in "rust rust cargo".split_whitespace() {
        *counts.entry(word).or_insert(0) += 1;
    }
    println!("rust: {}", counts["rust"]);
}
実行結果OUTPUT
rust: 2
問題4:テストを書く

問題1のeven_sum関数が同じファイルにある状態で、空の配列、すべて奇数、負数を含むケースの#[test]を書き、cargo testで確認してください。

解答例RUST
#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn edge_cases() {
        assert_eq!(even_sum(&[]), 0);
        assert_eq!(even_sum(&[1, 3]), 0);
        assert_eq!(even_sum(&[-4, 3, 6]), 2);
    }
}
実行結果OUTPUT
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
学習のコツ解答を写したあと、入力値を変える、失敗ケースを増やす、という順で改造すると理解が深まります。
17

次に学ぶこと

基礎を終えたら、CLIツールを一つ完成させましょう。ファイル入出力、コマンドライン引数、エラー処理、テストを組み合わせると、学んだ内容がつながります。

その先には、非同期処理、Web API、データベース、WebAssembly、組み込みなどがあります。目的を一つ選び、公式ドキュメントのサンプルを自分の課題へ置き換えてみてください。