Rustとは
Rustは、ネイティブコードの高速さとメモリ安全性を両立することを目指すプログラミング言語です。Webサーバー、CLIツール、組み込み、ゲーム、WebAssemblyなどで利用できます。
- 所有権の仕組みで、解放済みメモリの利用などをコンパイル時に防ぐ
- 型を明示しつつ、型推論で読みやすく書ける
- Cargoがビルド、依存関係、テストをまとめて管理する
Rustは「エラーをなくす魔法」ではありません。安全でない可能性をコンパイラーが知らせてくれるので、指摘を読みながら設計を改善する言語です。
環境構築と実行
公式のRustupを使うと、RustコンパイラーとCargoを導入できます。導入後、バージョンを確認します。
rustc --version
cargo --versionCargoでプロジェクトを作り、src/main.rsを実行します。
cargo new hello-rust
cd hello-rust
cargo runfn main() {
println!("こんにちは、Rust!");
}こんにちは、Rust!cargo build --release、テストにはcargo testを使います。基本の書き方
Rustの文は基本的にセミコロンで終わります。関数の最後の式は、セミコロンを付けなければ戻り値になります。
fn main() {
// これはコメントです
let total = add(12, 8);
println!("合計: {total}");
}
fn add(left: i32, right: i32) -> i32 {
left + right
}合計: 20println!の末尾の!はマクロであることを示します。{total}のような書式指定で値を埋め込めます。
変数と型
letで変数を宣言します。Rustの変数はデフォルトで変更不可です。変更したいときだけmutを付けます。
fn main() {
let name: &str = "Mity";
let mut score: i32 = 70;
score += 15;
let passed = score >= 60;
println!("{name}: {score}点、合格={passed}");
}Mity: 85点、合格=true整数にはi32、小数にはf64、真偽値にはbool、文字列リテラルには&strをよく使います。型を省略しても推論できる場合があります。
asは値が範囲外で切り詰められる可能性があるため、変換元が確実に範囲内のときに使います。失敗を扱うならu32::try_from(value)のような変換を選びます。条件分岐
ifの条件は必ずboolです。Rustではifも式なので、値を変数へ代入できます。
fn main() {
let score = 82;
let result = if score >= 80 { "たいへんよくできました" } else if score >= 60 { "合格" } else { "再挑戦" };
println!("{result}");
}たいへんよくできました条件によって返す値の型は揃える必要があります。数値と文字列を同じif式から返すことはできません。
繰り返し
Rustのforは範囲やコレクションを順番に処理します。1..=4は4を含み、1..4は4を含みません。
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}");
}あと3回
あと2回
あと1回
合計: 10whileでは条件に関係する値を必ず更新します。途中で終えるbreak、今回だけ飛ばすcontinueも使えます。関数
関数はfnで定義し、引数の型と戻り値の型を書きます。値を返すだけなら最後の式にします。
fn shipping_fee(weight: u32) -> u32 {
if weight <= 2 { 300 } else { 600 }
}
fn main() { println!("送料: {}円", shipping_fee(3)); }送料: 600円関数を小さく分けると、入力・処理・出力を個別にテストできます。表示を担当する関数と、計算して値を返す関数を分けましょう。
所有権
Rustの各値には所有者が一人います。所有者がスコープを抜けると値は自動的に片付けられます。Stringを別の変数へ代入すると、二重解放を防ぐため所有権が移動します。
fn main() {
let first = String::from("Rust");
let second = first; // 所有権がsecondへ移る
println!("{second}");
}Rustこの直後にprintln!("{first}")と書くと、borrow of moved valueというコンパイルエラーになります。let second = first;をlet second = first.clone();へ変えると、値を複製できます。ただし、借用で済むかを先に考えましょう。
Copyなので代入後も使えます。一方、StringやVecは通常moveします。借用とスライス
参照(&)を渡すと、所有権を移動せずに値を読み取れます。読み取り参照は複数作れますが、変更可能な参照は同時に一つだけです。
fn length(text: &str) -> usize { text.len() }
fn main() {
let message = String::from("こんにちは");
println!("{}バイト", length(&message));
println!("{message}");
}15バイト
こんにちは読み取るだけなら&Stringより&strを受け取るほうが、Stringと文字列リテラルの両方に使えて柔軟です。
fn add_mark(text: &mut String) { text.push('!'); }
fn main() {
let mut message = String::from("Hello");
add_mark(&mut message);
println!("{message}");
}Hello!スライスで一部を参照する
スライスは、配列やベクターなどの連続した要素の一部を、所有権を持たずに参照する型です。整数の列なら&[i32]のように表します。範囲の終わりを含まない1..3では、位置1と2を取り出します。
fn show_middle(values: &[i32]) {
println!("{:?}", &values[1..3]);
}
fn main() {
let values = vec![10, 20, 30, 40];
show_middle(&values);
}[20, 30]構造体とメソッド
構造体は関連するデータを一つにまとめます。implブロックでメソッドを定義できます。
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));
}Rust本: 2700円&selfは読み取り専用、&mut selfは変更可能、selfは所有権を受け取るメソッドです。
列挙型とmatch
列挙型は値が取りうる種類を表します。matchは全パターンを扱う必要があるため、処理漏れに気付きやすくなります。
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") })); }発送済み: ABC123コレクション
Vec<T>は伸縮する配列、HashMap<K, V>はキーと値の組、Stringは所有するUTF-8文字列です。
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"]);
}平均: 81、学習回数: 3245 / 3のような整数除算は小数部分を切り捨てるため、平均は81になります。scores.len()が0のときは0で割ることになりパニックするので、空の場合を先に判定しましょう。scores[10]は範囲外でパニックになります。存在しない可能性があるならget(10)でOptionとして扱います。OptionとResult
値がない状態をOption<T>、失敗する可能性をResult<T, E>で表します。nullや例外に任せず、呼び出し側へ判断を促す設計です。
fn first_word(text: &str) -> Option<&str> {
text.split_whitespace().next()
}
fn main() {
let word = first_word("").unwrap_or("単語なし");
println!("{word}");
}単語なし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}"),
}
}20歳?演算子を使うと、途中の処理が失敗した時点で、そのエラーを呼び出し元へ返せます。成功した値だけを次の処理へ渡せるため、エラー処理の分岐を何度も書かずに済みます。
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"));
}Ok(23)unwrapは失敗時に停止するため、学習用の確認以外ではmatch、unwrap_or、?演算子などで処理します。
トレイトとジェネリクス
トレイトは型が持つ共通の振る舞いを定義します。ジェネリクスと組み合わせると、特定の型に依存しない関数を書けます。
fn larger<T: PartialOrd>(left: T, right: T) -> T {
if left > right { left } else { right }
}
fn main() { println!("{}", larger(12, 8)); }12PartialOrdという境界があるので比較できる型だけを受け取れます。必要な能力をトレイト境界で表すことが、汎用的で安全なコードにつながります。
Cargoとテスト
Cargo.tomlはプロジェクト名や依存クレートを管理し、src以下のモジュールでコードを分割します。整形はcargo fmt、静的な確認はcargo clippy、テストはcargo testです。
cargo new todo-cli
cargo check
cargo fmt
cargo clippy
cargo test
cargo build --releasefn 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); }
}5外部クレートを追加するときは用途・ライセンス・更新状況を確認し、標準ライブラリで十分かも考えます。
練習問題
答えを見ずに、入力と出力を自分で決めて実装しましょう。各問題は複数の章を組み合わせる練習です。
問題1:偶数だけの合計
[3, 4, 8, 11]から偶数だけを取り出して合計し、12を表示する関数を作ってください。
fn even_sum(numbers: &[i32]) -> i32 {
numbers.iter().copied().filter(|number| number % 2 == 0).sum()
}
fn main() { println!("{}", even_sum(&[3, 4, 8, 11])); }12問題2:安全な割り算
0で割る場合はErr、それ以外は商を返すdivide関数を作ってください。
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)); }Err("0では割れません")問題3:単語の出現回数
文章を空白で分割し、HashMapで各単語の回数を数えてください。
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"]);
}rust: 2問題4:テストを書く
問題1のeven_sum関数が同じファイルにある状態で、空の配列、すべて奇数、負数を含むケースの#[test]を書き、cargo testで確認してください。
#[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);
}
}test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s次に学ぶこと
基礎を終えたら、CLIツールを一つ完成させましょう。ファイル入出力、コマンドライン引数、エラー処理、テストを組み合わせると、学んだ内容がつながります。
その先には、非同期処理、Web API、データベース、WebAssembly、組み込みなどがあります。目的を一つ選び、公式ドキュメントのサンプルを自分の課題へ置き換えてみてください。