- 技術 💻
Rust入門|所有権からCLI作成までコピペで動くコード付き
Rustとは、ガベージコレクション(使わなくなったメモリを自動で回収する仕組み)なしでメモリ安全性をコンパイル時に保証する、高速なプログラミング言語です。 その中心にあるのが「所有権」と「借用」というルールで、ここを理解すると Rust のコードがぐっと読みやすくなります。
この記事では、cargo new でプロジェクトを作るところから、基本文法・所有権・トレイト・イテレータまでをすべてコピペで動くコードと実際の実行結果で解説します。さらに、つまずきやすいコンパイルエラー(E0382・E0502・E0499・E0308・E0106)をわざと起こして、本物のエラー文と直し方を表にしました。最後に clap を使った小さなテキスト集計 CLI を作り、cargo test・cargo clippy・cargo fmt の結果まで載せています。検証環境は Rust 1.95.0(rustup)、edition 2024、Linux です。
Rust入門の早見表
文法の早見表
| やりたいこと | 書き方 | ポイント |
|---|---|---|
| 変数 | let x = 5; |
既定で変更不可 |
| 変更できる変数 | let mut x = 5; |
mut を付ける |
| 型を書く | let price: u32 = 1200; |
多くは推論されるので省略可 |
| 関数 | fn add(a: i32, b: i32) -> i32 { a + b } |
最後の式(; なし)が戻り値 |
| if(式) | let s = if ok { "A" } else { "B" }; |
値を返せる |
| 範囲ループ | for i in 1..=3 { } |
1..3 は 3 を含まない |
| 値を返す loop | let n = loop { break 8; }; |
break 値 |
| 構造体 | struct Item { name: String, price: u32 } |
impl でメソッド追加 |
| 列挙型 | enum Status { Draft, Ordered { id: u32 } } |
値を持たせられる |
| 分岐 | match s { Status::Draft => …, _ => … } |
全パターン網羅が必須 |
| 値がないかも | Option<T>(Some(v) / None) |
null の代わり |
| 失敗するかも | Result<T, E>(Ok(v) / Err(e)) |
例外の代わり |
| エラーを上に返す | let n: u32 = s.parse()?; |
Err ならその場で return |
| 借用(読む) | fn f(v: &[String]) |
所有権は移らない |
| 借用(書く) | fn f(s: &mut String) |
同時に1つだけ |
| 複製 | let b = a.clone(); |
中身ごとコピー |
| 共通の振る舞い | trait Summary { fn title(&self) -> String; } |
他言語のインターフェースに近い |
| 配列のような可変長 | let v = vec![1, 2, 3]; |
Vec<T> |
| 連想配列 | HashMap<K, V> |
use std::collections::HashMap; |
| 変換と集計 | v.iter().filter(…).map(…).collect() |
イテレータ |
cargo コマンドの早見表
| コマンド | 用途 |
|---|---|
cargo new 名前 |
新しいプロジェクト(実行ファイル)を作る |
cargo new --lib 名前 |
ライブラリを作る |
cargo run |
ビルドして実行(-- の後ろはプログラムへの引数) |
cargo build --release |
最適化ビルド(target/release/) |
cargo check |
ビルドせず型チェックだけ(速い) |
cargo test |
テスト実行 |
cargo add クレート名 |
依存を Cargo.toml に追加 |
cargo clippy |
よりよい書き方を提案するリンター |
cargo fmt |
コード整形(--check で差分確認のみ) |
cargo doc --open |
ドキュメント生成 |
rustc --explain E0382 |
エラー番号の詳しい解説 |
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Rust入門|所有権からCLI作成までコピペで動くコード付き」
Rustの最新版とedition 2024
edition(エディション)とは、Rust の文法の世代です。 3年ごとに区切りがあり(2015 / 2018 / 2021 / 2024)、edition が違うクレート(パッケージ)同士も組み合わせて使えます。edition 2024 は Rust 1.85 で安定化され、現在の cargo new は自動的に edition = "2024" を設定します。
| 項目 | 内容 |
|---|---|
| リリース周期 | 6週間ごとに新しい安定版 |
| 公式ブログで確認できた最新安定版 | 1.98.1(2026年9月3日公開。2026年10月1日時点) |
| 本記事の検証環境 | rustc 1.95.0 / cargo 1.95.0(edition 2024) |
| 最新の edition | 2024 |
本記事のコードは edition 2024 の基本機能だけを使っているため、1.95 以降の安定版でそのまま動きます。rustup update で最新版に更新できます。
edition 2024 で使えるようになった書き方の例が「let チェーン」です。if let を && でつなげられます。
fn main() {
let input = Some("12");
if let Some(s) = input
&& let Ok(n) = s.parse::<u32>()
&& n > 10
{
println!("10より大きい: {n}");
}
}
10より大きい: 12
同じコードを edition 2021 でコンパイルすると次のエラーになります。
error: let chains are only allowed in Rust 2024 or later
インストールとcargo newで最初のプログラム
# macOS / Linux(公式のインストーラー rustup)
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
# Windows は https://rustup.rs から rustup-init.exe を実行
rustc --version
cargo --version
rustc 1.95.0 (59807616e 2026-04-14)
cargo 1.95.0 (f2d3ce0bd 2026-03-21)
cargo new hello
cd hello
cargo run
Creating binary (application) `hello` package
Compiling hello v0.1.0 (…/hello)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 1.06s
Running `target/debug/hello`
Hello, world!
作られるファイルと Cargo.toml です。
.
|-- .gitignore
|-- Cargo.lock
|-- Cargo.toml
`-- src
`-- main.rs
[package]
name = "hello"
version = "0.1.0"
edition = "2024"
[dependencies]
以降のサンプルは、hello/examples/ フォルダに basics.rs などの名前で保存し、cargo run --example basics で実行できます。
変数・型・制御構文・関数
// examples/basics.rs
fn main() {
// 変数:既定で変更不可。mut で変更可
let name = "ノート";
let mut stock = 10;
stock -= 3;
println!("{name} の在庫: {stock}");
// シャドーイング:同じ名前で新しい変数を作る
let qty = "5";
let qty: i32 = qty.parse().unwrap();
println!("qty + 1 = {}", qty + 1);
// 型
let price: u32 = 1200;
let rate: f64 = 0.1;
let ok: bool = true;
let mark: char = '★';
let pair: (i32, &str) = (1, "one");
let arr: [i32; 3] = [10, 20, 30];
println!("{price} {rate} {ok} {mark} {:?} {:?} len={}", pair, arr, arr.len());
// if は式(値を返す)
let label = if stock > 5 { "十分" } else { "少ない" };
println!("在庫は{label}");
// ループ
for i in 1..=3 {
print!("{i} ");
}
println!();
let mut n = 0;
let found = loop {
n += 1;
if n * n > 50 {
break n;
}
};
println!("n*n > 50 となる最小の n = {found}");
println!("税込: {}", with_tax(price));
}
// 関数:最後の式(セミコロンなし)が戻り値
fn with_tax(price: u32) -> u32 {
price * 110 / 100
}
ノート の在庫: 7
qty + 1 = 6
1200 0.1 true ★ (1, "one") [10, 20, 30] len=3
在庫は十分
1 2 3
n*n > 50 となる最小の n = 8
税込: 1320
let で作った変数は既定で変更できず、変更するときだけ mut を付けます。 「どの変数が変わるか」がコードから一目でわかるのが利点です。
| 型 | 意味 | 例 |
|---|---|---|
i32 / i64 |
符号付き整数 | -5 |
u32 / u64 / usize |
符号なし整数(usize は長さ・添字) |
1200 |
f64 |
浮動小数点数 | 0.1 |
bool |
真偽値 | true |
char |
1文字(Unicode) | '★' |
&str |
文字列スライス(借りた文字列) | "ノート" |
String |
所有する文字列(伸び縮みできる) | String::from("ノート") |
(T, U) |
タプル | (1, "one") |
[T; N] |
固定長配列 | [10, 20, 30] |
Vec<T> |
可変長配列 | vec![1, 2] |
構造体・enumとmatch・OptionとResult
// examples/types.rs
#[derive(Debug, Clone, PartialEq)]
struct Item {
name: String,
price: u32,
qty: u32,
}
impl Item {
fn new(name: &str, price: u32, qty: u32) -> Self {
Self { name: name.to_string(), price, qty }
}
fn subtotal(&self) -> u32 {
self.price * self.qty
}
}
#[derive(Debug)]
enum Status {
Draft,
Ordered { id: u32 },
Cancelled(String),
}
fn describe(s: &Status) -> String {
match s {
Status::Draft => "下書き".to_string(),
Status::Ordered { id } if *id > 100 => format!("注文済み(大きい番号 {id})"),
Status::Ordered { id } => format!("注文済み #{id}"),
Status::Cancelled(reason) => format!("取消: {reason}"),
}
}
fn find_price(items: &[Item], name: &str) -> Option<u32> {
items.iter().find(|i| i.name == name).map(|i| i.price)
}
#[derive(Debug)]
enum OrderError {
Parse(std::num::ParseIntError),
Zero,
}
impl std::fmt::Display for OrderError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
OrderError::Parse(e) => write!(f, "数値ではありません: {e}"),
OrderError::Zero => write!(f, "0 は指定できません"),
}
}
}
// ? 演算子で ParseIntError を OrderError に自動変換するための実装
impl From<std::num::ParseIntError> for OrderError {
fn from(e: std::num::ParseIntError) -> Self {
OrderError::Parse(e)
}
}
fn parse_qty(s: &str) -> Result<u32, OrderError> {
let n: u32 = s.trim().parse()?; // 失敗したら Err を返して関数を抜ける
if n == 0 {
return Err(OrderError::Zero);
}
Ok(n)
}
fn main() {
let items = vec![Item::new("ノート", 300, 2), Item::new("付箋", 200, 1)];
println!("{:?}", items[0]);
println!("小計: {}", items[0].subtotal());
for s in [
Status::Draft,
Status::Ordered { id: 7 },
Status::Ordered { id: 120 },
Status::Cancelled("在庫切れ".into()),
] {
println!("{}", describe(&s));
}
println!("{:?} {:?}", find_price(&items, "付箋"), find_price(&items, "消しゴム"));
if let Some(p) = find_price(&items, "ノート") {
println!("ノートは {p} 円");
}
let p = find_price(&items, "消しゴム").unwrap_or(0);
println!("見つからなければ {p}");
for s in ["3", "0", "abc"] {
match parse_qty(s) {
Ok(n) => println!("{s} -> Ok({n})"),
Err(e) => println!("{s} -> Err: {e}"),
}
}
}
Item { name: "ノート", price: 300, qty: 2 }
小計: 600
下書き
注文済み #7
注文済み(大きい番号 120)
取消: 在庫切れ
Some(200) None
ノートは 300 円
見つからなければ 0
3 -> Ok(3)
0 -> Err: 0 は指定できません
abc -> Err: 数値ではありません: invalid digit found in string
match はすべてのパターンを書かないとコンパイルが通りません。 enum に値を追加したとき、処理を書き忘れた箇所をコンパイラが教えてくれます。
Option と Result の取り出し方の使い分けです。
| 書き方 | 動き | 向いている場面 |
|---|---|---|
match |
すべての場合を書き分ける | 処理が分岐する |
if let Some(v) = x |
ある場合だけ処理 | ない場合は何もしない |
x.unwrap_or(既定値) |
ない・失敗なら既定値 | 既定値で続行できる |
x? |
失敗なら呼び出し元へ返す | 関数の戻り値も Result / Option |
x.expect("理由") |
失敗なら理由を表示して停止 | 起動時の設定読み込みなど、続行できないとき |
x.unwrap() |
失敗なら停止 | 試し書き・テスト |
所有権・借用・ライフタイム
所有権のルールは3つです。 ①値にはそれぞれ「所有者」となる変数が1つある、②所有者は同時に1つだけ、③所有者がスコープ({ } の範囲)を抜けると値は解放される。
// examples/ownership.rs
// 借用(読むだけ)
fn total_len(words: &[String]) -> usize {
words.iter().map(|w| w.len()).sum()
}
// 可変の借用(書き換える)
fn add_suffix(s: &mut String) {
s.push('様');
}
// 所有権を受け取る(ムーブ)
fn take(s: String) -> usize {
s.len()
}
// ライフタイム注釈:戻り値の参照は a と b のどちらかから来る
fn longer<'a>(a: &'a str, b: &'a str) -> &'a str {
if a.len() >= b.len() { a } else { b }
}
fn main() {
let a = String::from("在庫");
let b = a; // ムーブ:a は使えなくなる
println!("b = {b}");
let c = b.clone(); // clone:中身ごと複製
println!("b = {b}, c = {c}");
let x = 5;
let y = x; // i32 は Copy:x も使える
println!("x = {x}, y = {y}");
let words = vec![String::from("note"), String::from("pen")];
println!("total_len = {}", total_len(&words));
println!("words はまだ使える: {:?}", words);
let mut name = String::from("山田");
add_suffix(&mut name);
println!("{name}");
let n = take(name);
println!("take の戻り値 = {n}");
println!("longer = {}", longer("apple", "kiwi"));
// スライス:所有せずに一部を参照
let s = String::from("hello world");
let first = &s[..5];
println!("first = {first}");
}
b = 在庫
b = 在庫, c = 在庫
x = 5, y = 5
total_len = 7
words はまだ使える: ["note", "pen"]
山田様
take の戻り値 = 9
longer = apple
first = hello
String を別の変数に代入すると「ムーブ(所有権の移動)」になり、元の変数は使えなくなります。 一方、i32 のような小さな値は Copy トレイトを持つので、代入しても両方使えます。take の戻り値が 9 なのは、len() が文字数ではなくバイト数(「山田様」は UTF-8 で 3文字×3バイト)を返すためです。
flowchart LR
A["let a = String::from(...)"] -->|"let b = a(ムーブ)"| B["b が所有者 / a は使えない"]
B -->|"&b(借用)"| C["読むだけ・何個でも同時に可"]
B -->|"&mut b(可変の借用)"| D["書き換え可・同時に1つだけ"]
B -->|"b.clone()"| E["別の所有者 c が誕生"]
所有権の判断表(引数の型をどれにするか)
| 関数の中でやること | 引数の型 | 呼び出し側 | 例 |
|---|---|---|---|
| 読むだけ | &str / &[T] / &T |
f(&x) |
長さを数える、検索する |
| 中身を書き換える | &mut T |
f(&mut x) |
末尾に追加する |
| 保存する・別スレッドへ渡す | T(所有権を受け取る) |
f(x)(以後 x は使えない) |
構造体のフィールドに入れる |
| 受け取りたいが元も使い続けたい | T |
f(x.clone()) |
コストが小さい、または一度きり |
| 小さな数値・bool・char | T(Copy) |
f(x)(x も使える) |
i32、f64 |
迷ったら「まず借用(&)で受け取り、コンパイラに求められたら所有権や clone を検討する」のが進めやすい順番です。引数は &String より &str、&Vec<T> より &[T] にすると、どちらの型からも呼べて便利です(後述の clippy でも指摘されます)。
借用のルール
| ルール | 内容 |
|---|---|
不変の借用 &T |
同時にいくつでも作れる |
可変の借用 &mut T |
同時に1つだけ。その間は &T も作れない |
| 借用の有効期間 | 最後に使われた位置まで(そこを過ぎれば次の借用ができる) |
ライフタイム 'a |
「参照がどの値から借りたものか」をコンパイラに伝える注釈 |
つまずきやすいコンパイルエラーと直し方
以下はわざとエラーを起こして rustc 1.95.0(edition 2024)で得たエラー文です。
E0382:ムーブ後に使った
fn main() {
let a = String::from("在庫");
let b = a;
println!("{a} {b}");
}
error[E0382]: borrow of moved value: `a`
--> e0382.rs:4:16
|
2 | let a = String::from("在庫");
| - move occurs because `a` has type `String`, which does not implement the `Copy` trait
3 | let b = a;
| - value moved here
4 | println!("{a} {b}");
| ^ value borrowed here after move
|
help: consider cloning the value if the performance cost is acceptable
|
3 | let b = a.clone();
| ++++++++
E0502:不変の借用中に変更した
fn main() {
let mut v = vec![1, 2, 3];
let first = &v[0];
v.push(4);
println!("{first}");
}
error[E0502]: cannot borrow `v` as mutable because it is also borrowed as immutable
--> e0502.rs:4:5
|
3 | let first = &v[0];
| - immutable borrow occurs here
4 | v.push(4);
| ^^^^^^^^^ mutable borrow occurs here
5 | println!("{first}");
| ----- immutable borrow later used here
push で Vec の領域が引っ越すと、first が指していた場所が無効になる可能性があるため、コンパイラが止めてくれています。
E0499:可変の借用を2つ同時に作った
fn main() {
let mut s = String::from("a");
let r1 = &mut s;
let r2 = &mut s;
r1.push('b');
r2.push('c');
}
error[E0499]: cannot borrow `s` as mutable more than once at a time
--> e0499.rs:4:14
|
3 | let r1 = &mut s;
| ------ first mutable borrow occurs here
4 | let r2 = &mut s;
| ^^^^^^ second mutable borrow occurs here
5 | r1.push('b');
| -- first borrow later used here
E0308:型が合わない
fn half(n: i32) -> i32 {
n / 2;
}
fn main() {
let price: u32 = "300";
println!("{} {}", half(4), price);
}
error[E0308]: mismatched types
--> e0308.rs:1:20
|
1 | fn half(n: i32) -> i32 {
| ---- ^^^ expected `i32`, found `()`
| |
| implicitly returns `()` as its body has no tail or `return` expression
2 | n / 2;
| - help: remove this semicolon to return this value
error[E0308]: mismatched types
--> e0308.rs:5:22
|
5 | let price: u32 = "300";
| --- ^^^^^ expected `u32`, found `&str`
| |
| expected due to this
E0106:ライフタイムが足りない
fn longer(a: &str, b: &str) -> &str {
if a.len() >= b.len() { a } else { b }
}
error[E0106]: missing lifetime specifier
--> e0106.rs:1:32
|
1 | fn longer(a: &str, b: &str) -> &str {
| ---- ---- ^ expected named lifetime parameter
|
= help: this function's return type contains a borrowed value, but the signature does not say whether it is borrowed from `a` or `b`
help: consider introducing a named lifetime parameter
|
1 | fn longer<'a>(a: &'a str, b: &'a str) -> &'a str {
| ++++ ++ ++ ++
エラー別の直し方まとめ
| エラー | 意味 | 直し方 | 直した後 |
|---|---|---|---|
| E0382 | ムーブした値を使った | 借用にする / clone() / 使う順番を変える |
let b = &a; または let b = a.clone(); |
| E0502 | 読んでいる最中に書き換えた | 先に値をコピーする / 借用の範囲を短くする | let first = v[0];(i32 はコピー) |
| E0499 | 書き換え用の借用が2つ | 1つずつ順番に使う | r1 を使い終えてから r2 を作る |
| E0308 | 型の不一致 | ; を外して戻り値にする / parse() などで変換 |
n / 2 / "300".parse::<u32>()? |
| E0106 | 戻り値の参照の出どころが不明 | ライフタイム注釈 'a を付ける / 所有する型 String を返す |
fn longer<'a>(a: &'a str, b: &'a str) -> &'a str |
Rust のエラー文には help: として直し方の候補が書かれています。 まず help: を読み、詳しく知りたいときは rustc --explain E0502 で公式の解説と例を表示しましょう。
トレイト(共通の振る舞い)
// examples/traits.rs
use std::fmt;
trait Summary {
fn title(&self) -> String;
// 既定の実装
fn summary(&self) -> String {
format!("[{}]", self.title())
}
}
struct Article {
headline: String,
}
struct Task {
name: String,
done: bool,
}
impl Summary for Article {
fn title(&self) -> String {
self.headline.clone()
}
}
impl Summary for Task {
fn title(&self) -> String {
self.name.clone()
}
fn summary(&self) -> String {
format!("{} {}", if self.done { "✔" } else { "□" }, self.name)
}
}
impl fmt::Display for Task {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "Task({})", self.name)
}
}
// トレイトオブジェクト:違う型をまとめて扱う
fn print_all(items: &[Box<dyn Summary>]) {
for i in items {
println!("{}", i.summary());
}
}
// ジェネリクス+トレイト境界:型ごとに最適化されたコードになる
fn show<T: Summary>(item: &T) {
println!("show: {}", item.summary());
}
fn main() {
let t = Task { name: "備品発注".into(), done: false };
show(&t);
println!("{t}");
let list: Vec<Box<dyn Summary>> = vec![
Box::new(Article { headline: "Rust入門".into() }),
Box::new(Task { name: "レビュー".into(), done: true }),
];
print_all(&list);
}
show: □ 備品発注
Task(備品発注)
[Rust入門]
✔ レビュー
Display トレイトを実装すると println!("{t}") で表示できるようになります。 #[derive(Debug, Clone, PartialEq)] は、よく使うトレイトの実装を自動で生成する書き方です。
| 書き方 | 決まるタイミング | 向いている場面 |
|---|---|---|
fn f<T: Summary>(x: &T) / fn f(x: &impl Summary) |
コンパイル時 | 1回の呼び出しで型が1つに決まる |
&dyn Summary / Box<dyn Summary> |
実行時 | 違う型を1つの Vec に入れたい |
コレクションとイテレータ
// examples/collections.rs
use std::collections::{HashMap, HashSet};
fn main() {
let mut v = vec![5, 3, 8, 1];
v.push(4);
v.sort();
println!("v = {:?}, len = {}, first = {:?}, get(10) = {:?}", v, v.len(), v.first(), v.get(10));
let mut stock: HashMap<&str, u32> = HashMap::new();
stock.insert("ノート", 10);
*stock.entry("ペン").or_insert(0) += 5;
*stock.entry("ペン").or_insert(0) += 2;
let mut keys: Vec<_> = stock.iter().collect();
keys.sort();
println!("stock = {:?}", keys);
let tags: HashSet<&str> = ["rust", "cli", "rust"].into_iter().collect();
println!("tags.len() = {}", tags.len());
// イテレータ
let nums = [1, 2, 3, 4, 5, 6];
let even_sq: Vec<i32> = nums.iter().filter(|&&n| n % 2 == 0).map(|n| n * n).collect();
let sum: i32 = nums.iter().sum();
let pos = nums.iter().position(|&n| n == 4);
println!("even_sq = {:?}, sum = {sum}, position(4) = {:?}", even_sq, pos);
for (i, w) in ["a", "b", "c"].iter().enumerate() {
print!("{i}:{w} ");
}
println!();
let words = "rust is fast and rust is safe";
let mut freq: HashMap<&str, usize> = HashMap::new();
for w in words.split_whitespace() {
*freq.entry(w).or_default() += 1;
}
let mut top: Vec<_> = freq.into_iter().collect();
top.sort_by(|a, b| b.1.cmp(&a.1).then(a.0.cmp(b.0)));
println!("top3 = {:?}", &top[..3]);
let s = String::from("こんにちは");
println!("len(bytes) = {}, chars = {}", s.len(), s.chars().count());
}
v = [1, 3, 4, 5, 8], len = 5, first = Some(1), get(10) = None
stock = [("ノート", 10), ("ペン", 7)]
tags.len() = 2
even_sq = [4, 16, 36], sum = 21, position(4) = Some(3)
0:a 1:b 2:c
top3 = [("is", 2), ("rust", 2), ("and", 1)]
len(bytes) = 15, chars = 5
v.get(10) は範囲外でも停止せず None を返します(v[10] は実行時に停止)。HashMap の entry().or_insert() は「なければ作って足す」を1行で書けるので、集計でよく使います。HashMap の順序は実行ごとに変わり得るため、表示前に並べ替えています。
| イテレータのメソッド | 用途 | 他言語でいうと |
|---|---|---|
iter() / iter_mut() / into_iter() |
借用 / 可変借用 / 所有権ごと取り出す | – |
map / filter |
変換 / 絞り込み | JS の map / filter |
collect |
Vec や HashMap にまとめる | list(...) |
sum / count / max / min |
集計 | sum / len / max |
find / position / any / all |
検索 | find / indexOf / some / every |
enumerate / zip |
番号付き / 2つを組にする | Python の enumerate / zip |
take / skip / rev |
先頭 N 件 / 読み飛ばし / 逆順 | slice / reverse |
clapで小さなCLIを作る(テキスト集計ツール)
ファイルまたは標準入力のテキストから、行数・単語数・文字数と頻出単語の上位 N 件を表示する CLI を作ります。
cargo new wordstat
cd wordstat
cargo add clap --features derive
# Cargo.toml(追加された部分)
[dependencies]
clap = { version = "4.6.7", features = ["derive"] }
src/lib.rs(集計ロジックとテスト)
use std::collections::HashMap;
/// テキストの集計結果
#[derive(Debug, PartialEq)]
pub struct Stats {
pub lines: usize,
pub words: usize,
pub chars: usize,
}
/// 行数・単語数・文字数を数える
pub fn count(text: &str) -> Stats {
Stats {
lines: text.lines().count(),
words: text.split_whitespace().count(),
chars: text.chars().count(),
}
}
/// 出現回数の多い単語を上位 n 件返す(同数は辞書順)
pub fn top_words(text: &str, n: usize, ignore_case: bool) -> Vec<(String, usize)> {
let mut freq: HashMap<String, usize> = HashMap::new();
for w in text.split_whitespace() {
let w = w.trim_matches(|c: char| !c.is_alphanumeric());
if w.is_empty() {
continue;
}
let key = if ignore_case {
w.to_lowercase()
} else {
w.to_string()
};
*freq.entry(key).or_default() += 1;
}
let mut v: Vec<(String, usize)> = freq.into_iter().collect();
v.sort_by(|a, b| b.1.cmp(&a.1).then_with(|| a.0.cmp(&b.0)));
v.truncate(n);
v
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn counts_lines_words_chars() {
let s = count("a b\nこんにちは\n");
assert_eq!(s, Stats { lines: 2, words: 3, chars: 10 });
}
#[test]
fn top_words_ignores_case_and_punctuation() {
let v = top_words("Rust, rust! Go go go.", 2, true);
assert_eq!(v, vec![("go".to_string(), 3), ("rust".to_string(), 2)]);
}
#[test]
fn empty_text() {
assert_eq!(count(""), Stats { lines: 0, words: 0, chars: 0 });
assert!(top_words("", 3, false).is_empty());
}
}
src/main.rs(引数の解析と入出力)
use clap::Parser;
use std::io::Read;
use std::path::PathBuf;
use std::process::ExitCode;
/// テキストの行数・単語数・文字数と頻出単語を集計する
#[derive(Parser, Debug)]
#[command(version, about)]
struct Args {
/// 入力ファイル(省略時は標準入力)
file: Option<PathBuf>,
/// 頻出単語を上位 N 件表示する
#[arg(short, long, default_value_t = 3)]
top: usize,
/// 大文字小文字を区別しない
#[arg(short = 'i', long)]
ignore_case: bool,
}
fn read_input(file: &Option<PathBuf>) -> std::io::Result<String> {
match file {
Some(path) => std::fs::read_to_string(path),
None => {
let mut buf = String::new();
std::io::stdin().read_to_string(&mut buf)?;
Ok(buf)
}
}
}
fn main() -> ExitCode {
let args = Args::parse();
let text = match read_input(&args.file) {
Ok(t) => t,
Err(e) => {
eprintln!("読み込みに失敗しました: {e}");
return ExitCode::FAILURE;
}
};
let s = wordstat::count(&text);
println!("lines: {} words: {} chars: {}", s.lines, s.words, s.chars);
for (i, (w, c)) in wordstat::top_words(&text, args.top, args.ignore_case)
.iter()
.enumerate()
{
println!("{:>2}. {w} ({c})", i + 1);
}
ExitCode::SUCCESS
}
ロジックを lib.rs、入出力を main.rs に分けると、ロジックだけを cargo test で確かめられます。 doc コメント(///)は clap のヘルプ文にそのまま使われます。
cargo run の結果
入力ファイル sample.txt の内容です。
Rust is fast.
Rust is safe, and rust is fun!
Cargo builds Rust code.
cargo run -q -- --help
テキストの行数・単語数・文字数と頻出単語を集計する
Usage: wordstat [OPTIONS] [FILE]
Arguments:
[FILE] 入力ファイル(省略時は標準入力)
Options:
-t, --top <TOP> 頻出単語を上位 N 件表示する [default: 3]
-i, --ignore-case 大文字小文字を区別しない
-h, --help Print help
-V, --version Print version
cargo run -q -- sample.txt
cargo run -q -- sample.txt -i --top 2
echo "go go rust" | cargo run -q -- -t 1
lines: 3 words: 14 chars: 69
1. Rust (3)
2. is (3)
3. Cargo (1)
lines: 3 words: 14 chars: 69
1. rust (4)
2. is (3)
lines: 1 words: 3 chars: 11
1. go (2)
-i を付けると Rust と rust がまとめて数えられ、4 になります。エラー時の動きも確認しました。
$ cargo run -q -- nofile.txt; echo "exit=$?"
読み込みに失敗しました: No such file or directory (os error 2)
exit=1
$ cargo run -q -- --topp 2; echo "exit=$?"
error: unexpected argument '--topp' found
tip: a similar argument exists: '--top'
Usage: wordstat --top <TOP> [FILE]
For more information, try '--help'.
exit=2
clap はオプション名の打ち間違いに「似た引数があります」と候補を出してくれます。 終了コードも 0(成功)・1(読み込み失敗)・2(引数エラー)と分かれるので、シェルスクリプトから使うときも判定しやすくなります。
cargo test の結果
running 3 tests
test tests::counts_lines_words_chars ... ok
test tests::empty_text ... ok
test tests::top_words_ignores_case_and_punctuation ... ok
test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
cargo clippy と cargo fmt の結果
clippy の指摘を確かめるため、わざと回りくどく書いた関数を追加しました。
// 直す前
pub fn sum_len(v: &Vec<String>) -> usize {
let mut total = 0;
for i in 0..v.len() { total = total + v[i].len(); }
return total;
}
warning: unneeded `return` statement
--> src/bad.rs:4:5
warning: writing `&Vec` instead of `&[_]` involves a new object where a slice will do
--> src/bad.rs:1:19
warning: the loop variable `i` is only used to index `v`
--> src/bad.rs:3:14
warning: manual implementation of an assign operation
--> src/bad.rs:3:27
warning: `wordstat` (lib) generated 4 warnings (run `cargo clippy --fix --lib -p wordstat -- ` to apply 2 suggestions)
cargo fmt --check は、整形が必要な箇所を差分で表示します(抜粋)。
- for i in 0..v.len() { total = total + v[i].len(); }
+ for i in 0..v.len() {
+ total = total + v[i].len();
+ }
// 直した後(clippy・fmt ともに指摘なし)
pub fn sum_len(v: &[String]) -> usize {
v.iter().map(|s| s.len()).sum()
}
clippy の指摘は「Rust らしい書き方」の教材になります。 &Vec<String> より &[String]、添字ループよりイテレータ、末尾の return は省略、が代表例です。cargo fmt で整形してから cargo clippy を通す習慣にすると、レビューで書き方の話をする時間が減ります。
| コマンド | 結果(本記事の CLI) |
|---|---|
cargo test |
3 passed |
cargo clippy(直した後) |
警告なし |
cargo fmt --check(整形後) |
差分なし(終了コード 0) |
他言語との違い早見表
| 項目 | Rust | Python | JavaScript / TypeScript | Go | Java |
|---|---|---|---|---|---|
| メモリ管理 | 所有権(コンパイル時) | GC | GC | GC | GC |
| null | なし(Option) |
None |
null / undefined |
nil |
null |
| エラー処理 | Result と ? |
例外 | 例外 | 戻り値 error |
例外 |
| 変数の既定 | 変更不可 | 変更可 | const / let |
変更可 | 変更可(final で不可) |
| 継承 | なし(トレイトで共通化) | あり | あり(class) | なし(interface) | あり |
| パッケージ管理 | cargo(crates.io) | pip / uv | npm | go mod | Maven / Gradle |
| 実行形態 | ネイティブバイナリ | インタプリタ | JIT | ネイティブバイナリ | JVM |
| 文字列の長さ | len() はバイト数 |
文字数 | UTF-16 単位 | バイト数 | UTF-16 単位 |
よくある質問
Q. Rustは難しいと聞きますが、どこから学べばよいですか?
A. 本記事の順番(基本文法 → enum と match → Option / Result → 所有権 → トレイト)がおすすめです。所有権は最初から完璧に理解しなくても、コンパイラのエラー文と help: に従って直すうちに身につきます。公式の「The Rust Programming Language」(日本語版あり)と、ブラウザで動かせる Rust Playground も役立ちます。
Q. String と &str はどう使い分けますか?
A. 関数の引数は &str(借りるだけ)、構造体のフィールドや戻り値で文字列を持ち続けたいときは String が基本です。&String は &str に自動で変換されるので、引数を &str にすれば両方から呼べます。
Q. unwrap() を使ってはいけないのですか?
A. 試し書きやテストでは便利です。値がない・失敗したときにプログラムが停止するので、本番のコードでは ? で呼び出し元に返すか、unwrap_or で既定値にするか、expect("理由") で停止理由を残すと、原因を追いやすくなります。
Q. edition を 2021 から 2024 に上げるには?
A. cargo fix --edition で自動修正できる箇所を直したあと、Cargo.toml の edition を "2024" に変更します。edition が違うクレート同士も一緒に使えるので、依存クレートの対応を待つ必要はありません。
まとめ
- Rust は所有権のルールでメモリ安全性をコンパイル時に保証する言語で、
cargo newですぐ始められます(現在の既定は edition 2024)。 - 変数は既定で変更不可、
ifやmatchは値を返す式です。 - null の代わりに
Option、例外の代わりにResultと?を使います。 - 引数はまず借用(
&str・&[T])で受け取り、必要なときだけ所有権やcloneを使うと進めやすくなります。 - E0382・E0502・E0499・E0308・E0106 は
help:に直し方が書かれているので、エラー文を読むのが近道です。 - ロジックを
lib.rsに分けるとcargo testで確かめやすく、cargo fmtとcargo clippyで Rust らしい書き方に整えられます。
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Rust入門|所有権からCLI作成までコピペで動くコード付き」
株式会社RJCでは、Rustの所有権やツールの使い方を仲間と学び合っています。こうした知識を共有しながら、日々技術を磨いています。
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。