コーディング · プログラミング言語
Rust入門チュートリアル(初心者向け)
結論
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, usize | let n: i32 = 42; | 符号付き(i)と符号なし(u)。インデックスには usize |
f64 | let x = 3.14; | デフォルトの浮動小数点型 |
bool | let ok = true; | true または false |
char | let c = 'é'; | Unicodeスカラー値(4バイト) |
&str | let s = "hi"; | 借用された文字列スライス |
String | String::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は、学習曲線が急になる代わりに、より強い保証のもとで、より高い制御と性能を得られます。