cv.cm

コーディング · プログラミング言語

Rust入門チュートリアル(初心者向け)

約11分で読めます

結論

Rustはコンパイル型の静的型付けシステムプログラミング言語で、CやC++と同等に高速でありながら、ガベージコレクタなしにメモリエラーをコンパイル時に防ぎます。rustupでインストールし、cargo new hello と cargo run を実行します。所有権と借用(借用チェッカーが検査します)、match を使う構造体と列挙型、Vec や HashMap などのコレクション、Result と ? 演算子によるエラー処理が中心的な内容です。

バイナリ変換を開く

Rustとは何か、何に使われるか

Rustは、Graydon Hoareの個人プロジェクトとして始まり、Mozillaの支援を受けて、2015年にバージョン1.0に到達しました。現在はRust Foundationが運営を担っています。Rustは、速度と信頼性の両方が求められる分野で使われています。コマンドラインツール(ripgrep)、Webサービス、Firefoxのブラウザコンポーネント、LinuxカーネルやWindowsの一部、組み込み機器、WebAssemblyなどです。コンパイラは厳格ですが、その代わりに、解放後使用(use-after-free)やデータ競合といった種類のバグが、安全なRustのコードでは起こりえません。

Rustのインストールと Hello, world の実行

rustup.rsからrustupでツールチェーンをインストールします。rustc(コンパイラ)とCargo(ビルドツール兼パッケージマネージャー)が入ります。プロジェクトを作成します。

cargo new hello
cd hello
cargo run
# Hello, world!

Cargoは src/main.rs を生成します。

fn main() {
    println!("Hello, world!");
}

println! の末尾が ! なのは、関数ではなくマクロだからです。マクロはコンパイル時にコードを生成します。ここでは、書式文字列がコンパイル時に検査されます。

変数と基本の型

変数はデフォルトで不変です。変更を許すには mut を付けます。型は通常推論されますが、すべての値はちょうど1つの静的な型を持ち、数値の型変換は as で明示的に行います。

型例備考
i32, i64, u8, usizelet n: i32 = 42;符号付き(i)と符号なし(u)。インデックスには usize
f64let x = 3.14;デフォルトの浮動小数点型
boollet ok = true;true または false
charlet c = 'é';Unicodeスカラー値(4バイト)
&strlet s = "hi";借用された文字列スライス
StringString::from("hi")所有された、伸縮するUTF-8テキスト
タプル、配列(1, "a"), [0; 3]固定サイズ
fn main() {
    let name = "Ada";
    let mut count = 7;
    count += 1;
    let avg = count as f64 / 2.0;
    println!("{name} has {count} items, avg {avg}"); // Ada has 8 items, avg 4
    let parsed: i32 = "12".parse().unwrap();
    println!("{}", parsed + 1); // 13
}

制御構文と関数

if、match、ブロックは式なので、値を生成します。関数の最後の式にセミコロンを付けなければ、それが戻り値になります。

fn square(x: i32) -> i32 {
    x * x
}

fn main() {
    let score = 72;
    let grade = if score >= 90 { "A" } else if score >= 70 { "B" } else { "C" };
    println!("{grade}"); // B

    for i in 0..3 {
        print!("{} ", square(i)); // 0 1 4
    }
    println!();

    let mut n = 10;
    while n > 1 {
        n /= 2;
    }
    let label = loop {
        break "done"; // loop can return a value with break
    };
    println!("{n} {label}"); // 1 done
}

特徴的な機能:所有権と借用

すべての値には、ちょうど1つの所有者がいます。所有者がスコープを抜けると、値はドロップ(解放)されます。String のようなヒープ上の値を別の変数に代入するとムーブされ、元の名前は使えなくなります。所有権を取らずに値を使うには、参照で借用します。同時に存在できるのは、共有参照 &T がいくつでも、または可変参照 &mut T がちょうど1つのどちらかです。

fn length(s: &str) -> usize {
    s.len() // borrows, does not take ownership
}

fn add_exclamation(s: &mut String) {
    s.push('!');
}

fn main() {
    let a = String::from("hello");
    let b = a; // ownership moves to b
    // println!("{a}"); // error: borrow of moved value: `a`
    let mut c = b.clone(); // explicit deep copy
    add_exclamation(&mut c);
    println!("{} {} {}", b, c, length(&c)); // hello hello! 6
}

これらの規則は借用チェッカーがコンパイル時に強制します。これが、ガベージコレクタなしでメモリ安全性を実現する仕組みです。整数のような単純な型は Copy を実装しているため、ムーブされずにコピーされます。

構造体、列挙型、パターンマッチング

構造体はフィールドをまとめ、impl ブロックでメソッドを追加します。列挙型は各バリアントにデータを持たせられ、match はすべてのバリアントを処理する必要があり、これはコンパイラが検査します。標準の Option<T> 列挙型(Some(value) または None)が、nullの代わりになります。

#[derive(Debug)]
struct Point {
    x: f64,
    y: f64,
}

impl Point {
    fn distance(&self) -> f64 {
        (self.x * self.x + self.y * self.y).sqrt()
    }
}

enum Shape {
    Circle(f64),
    Rect { w: f64, h: f64 },
}

fn area(s: &Shape) -> f64 {
    match s {
        Shape::Circle(r) => std::f64::consts::PI * r * r,
        Shape::Rect { w, h } => w * h,
    }
}

fn main() {
    let p = Point { x: 3.0, y: 4.0 };
    println!("{:?} {}", p, p.distance()); // Point { x: 3.0, y: 4.0 } 5
    println!("{}", area(&Shape::Rect { w: 2.0, h: 3.0 })); // 6
    let nums = [10, 20];
    if let Some(v) = nums.first() {
        println!("first is {v}"); // first is 10
    }
}

トレイトは、インターフェースに似た共通の振る舞いを定義します。#[derive(Debug)] は Debug トレイトを実装するようコンパイラに頼むもので、{:?} で構造体を表示できるようになります。

コレクション:VecとHashMap

use std::collections::HashMap;

fn main() {
    let mut nums = vec![3, 1, 2];
    nums.push(5);
    nums.sort();
    println!("{:?} {}", nums, nums.len()); // [1, 2, 3, 5] 4

    let evens: Vec<i32> = nums.iter().filter(|n| *n % 2 == 0).map(|n| n * n).collect();
    println!("{:?}", evens); // [4]

    let mut ages: HashMap<&str, u32> = HashMap::new();
    ages.insert("ann", 31);
    ages.insert("bob", 25);
    *ages.entry("ann").or_insert(0) += 1;
    println!("{:?} {:?}", ages.get("ann"), ages.get("zed")); // Some(32) None
}

filter、map、collect のようなクロージャ(|n| ...)を使うイテレータは、手書きのループと同じ速度のコードにコンパイルされます。

Resultと?によるエラー処理

失敗しうる関数は Result<T, E> を返します。Ok(value) か Err(error) のどちらかです。? 演算子はエラーを呼び出し側に早期に返すので、エラー処理のコードが短く保てます。回復できないバグには panic! を使います。

use std::num::ParseIntError;

fn parse_age(text: &str) -> Result<u32, ParseIntError> {
    let age: u32 = text.trim().parse()?; // returns Err early on bad input
    Ok(age)
}

fn main() {
    match parse_age("41") {
        Ok(age) => println!("age {age}"), // age 41
        Err(e) => println!("error: {e}"),
    }
    match parse_age("abc") {
        Ok(age) => println!("age {age}"),
        Err(e) => println!("error: {e}"), // error: invalid digit found in string
    }
}

小さな完成プログラム:単語の出現頻度

これを src/main.rs に置き、cargo run -- notes.txt を実行します。

use std::collections::HashMap;
use std::{env, fs, process};

fn main() {
    let path = env::args().nth(1).unwrap_or_else(|| {
        eprintln!("usage: words FILE");
        process::exit(1);
    });
    let text = match fs::read_to_string(&path) {
        Ok(t) => t.to_lowercase(),
        Err(e) => {
            eprintln!("cannot read {path}: {e}");
            process::exit(1);
        }
    };

    let mut counts: HashMap<&str, usize> = HashMap::new();
    for word in text.split(|c: char| !c.is_alphanumeric()).filter(|w| !w.is_empty()) {
        *counts.entry(word).or_insert(0) += 1;
    }

    let mut top: Vec<(&str, usize)> = counts.into_iter().collect();
    top.sort_by(|a, b| b.1.cmp(&a.1).then(a.0.cmp(b.0)));
    for (word, n) in top.iter().take(5) {
        println!("{n:5}  {word}");
    }
}

HashMap は text から借用した &str スライスを保持するので、単語はコピーされません。text が十分長く生きていることは、借用チェッカーが保証します。

初心者がよくやる間違い

  • ムーブした後の値を使う。借用(&value)するか、意図してクローンします。
  • String を s[0] でインデックス指定しようとする。文字列はUTF-8なので、s.chars() を使うか、文字の境界でバイトスライスを使います。
  • Vec への参照を持ったまま、その Vec に push する。push で再確保が起こりうるため、コンパイラが禁止します。
  • String(所有)と &str(借用)を混同する。関数の引数では &str を受け取ると、両方を渡せます。

次のステップ

doc.rust-lang.orgで無料公開されている「The Rust Programming Language」(通称「ザ・ブック」)を読み、短く実行できるサンプルが載ったRust by Exampleも試しましょう。その後、トレイトとジェネリクスの詳細、ライフタイム、cargo test によるテスト、シリアライズ用の serde や非同期プログラム用の tokio などcrates.ioのクレートを学びます。cargo clippy を実行すると、追加のリント提案が得られます。

よくある質問

Rustは習得が難しいですか?

所有権と借用が新しい考え方なので、最初の数週間はほかの多くの言語より大変です。慣れると、コンパイラが多くのバグを早い段階で捕まえてくれ、Rustのコードは保守しやすいと感じる開発者も多くいます。

Rustにガベージコレクタはありますか?

いいえ。メモリは、所有者がスコープを抜けたときに自動的に解放されます。これは所有権の規則によってコンパイル時に決まります。共有所有権のための参照カウント型 Rc と Arc もあります。

unsafe Rustとは何ですか?

unsafe ブロックの中では、生ポインタの参照外しやC関数の呼び出しなど、コンパイラが検証できない操作ができます。使用は最小限にとどめ、通常は安全なAPIで包みます。

RustとGoのどちらを学ぶべきですか?

Goは学びやすくシンプルで、ネットワークサービスで人気があります。Rustは、学習曲線が急になる代わりに、より強い保証のもとで、より高い制御と性能を得られます。

コーディング講座の一覧