『独習Rust』を読んで ―基幹システムの現場エンジニアが、コードを書きながら試した本気レビュー
翔泳社様・WINGSプロジェクト様のレビュー企画にて、『独習Rust』(山内直 著、山田祥寛 監修/翔泳社、2026年6月刊、B5変型・616ページ)をご恵贈いただきました。
普段の私は、グループ会社の基幹システムやレガシーなWindows Server環境の保守・運用に携わりながら、Webアプリケーションやスマホアプリの開発も行っているエンジニアです。プライベートでは、iOSの内部構造をいじったり、非ジェイルブレイク環境でメモリを直接編集したりするような、少し変わった趣味も持っています。C/C++、VB6、PHP、Swiftあたりを行き来している人間が、「Rustを本気で基礎からやり直す」つもりで本書を開いた記録です。
なお、以下に出てくるコードはすべて、本書の各章のテーマに沿って私自身が実際に手元のLinux環境とmacOS環境で書いて動かした検証コードです。書籍に掲載されているサンプルコードそのものではなく、「この章で説明されている概念を、自分の実務やアプリ開発にあてはめるとこうなる」という形に落とし込んだものだと理解して読んでいただければと思います。
なぜ今Rustを本気で学び直すのか
仕事では今も、C/C++で書かれた古い制御プログラムや、VB6+SQL Serverの基幹システムの保守に携わっています。こうした環境では「メモリ管理のミス」や「NULL参照」が本番環境で事故につながる緊張感を、何度も味わってきました。実際、以前担当した本番サーバーのファイルシステム破損対応では、最終的にLive USBでのレスキューとOSの再構築まで踏み込む必要があり、「壊れてから直す」コストの重さを痛感しています。
Rustは、そうした「壊れてから気づく」バグの多くを、コンパイル時点で検出してくれる言語です。しかも実行速度はC/C++に匹敵し、GCを持たないためレイテンシも予測可能です。一方で「所有権」「借用」「ライフタイム」という独自概念は、C/C++・PHP・Swiftあたりの経験しかないと、最初のとっかかりで確実に心が折れます。実際、私も過去にオンラインのドキュメントで所有権のあたりで一度離脱した経験があります。今回はその借りを返すつもりで、本書と正面から向き合いました。
ここで、自分が普段使っている言語とRustの立ち位置を整理しておきます。C/C++は速度もメモリ管理の自由度も高い一方、その自由度自体が事故の原因になります。PHPは開発スピードは速いですが、型が緩く、大規模化すると実行時エラーの温床になりがちです。Swiftはメモリ安全性が高いものの、ARC(自動参照カウント)に依存しており、循環参照などARC特有の落とし穴があります。Rustは、この3言語のちょうど「速度」「安全性」「明示性」の交点に位置している言語だというのが、業務で複数言語を使い分けてきた実感です。
| 観点 | C/C++ | PHP | Swift | Rust |
|---|---|---|---|---|
| 実行速度 | ◎ | △ | ○ | ◎ |
| メモリ安全性 | △(自己管理) | ○(GC) | ○(ARC) | ◎(コンパイル時検証) |
| 学習コスト | ○ | ◎ | ○ | △(所有権の壁) |
| 本番デプロイの容易さ | ○ | ◎(そのまま動く) | △(Apple環境依存) | ◎(単一バイナリ) |
| 並行処理の安全性 | △ | △ | ○ | ◎(データ競合を型で防止) |
この表を見ると分かる通り、Rustは「学習コスト」だけが弱点として突出しています。逆に言えば、その学習コストを乗り越えるための一冊として本書が機能するなら、投資対効果はかなり高いはずだ、というのが読み始める前の仮説でした。
環境構築:LinuxサーバーとmacOSでの実際のセットアップ
本書の第1章では開発環境の構築が扱われていますが、ここでは実務でよく使う構成を補足しておきます。私の場合、検証はUbuntu 22.04のサーバーとmacOSの両方で行いました。
# Linux/macOS共通:rustupでツールチェーンを導入
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
# バージョン確認
rustc --version
cargo --version
# 本番デプロイ用にmusl静的リンクターゲットを追加(Alpineコンテナ等にそのまま置ける)
rustup target add x86_64-unknown-linux-musl
# クロスコンパイル用ビルド
cargo build --release --target x86_64-unknown-linux-musl
これまでPHPやNode.jsのサーバーサイドコードを本番に置くときは、ランタイムのバージョン差異やライブラリ依存で苦労することが多かったのですが、Rustはmuslターゲットでビルドすると単一の静的バイナリになるため、「バイナリ1本をコピーするだけでデプロイ完了」という体験ができます。これは古いWindows ServerからLinuxへの移行を検討している立場としては、かなり魅力的な性質です。
第2〜5章:基本文法を実務コードに落とし込む
第2章(変数・データ型)、第3章(演算子)、第4章(制御構文)、第5章(関数)は、他言語経験者であれば読み進めるスピード自体は速いはずです。ただ、本書は型変換やシーケンス型(配列・タプル)の扱いを丁寧に説明しているため、「なんとなく分かっていたつもり」の部分がかなり整理されました。
例えば、レガシーシステムから出力されるCSV形式の帳票データを読み込むケースを想定して、以下のようなコードで基本文法を確認しました。
fn main() {
// 固定長レコードを想定したタプルでのパース例
let raw_line = "001,山田太郎,003500";
let fields: Vec<&str> = raw_line.split(',').collect();
// シャドーイングを使って型を変換していく(第2章)
let record_type: u8 = fields[0].parse().unwrap_or(0);
let name = fields
;
let amount: i64 = fields
.parse().unwrap_or(0);
// 制御構文:match によるレコード種別の分岐(第4章)
match record_type {
1 => println!("通常データ: {} / {}円", name, amount),
9 => println!("集計データ: {} / {}円", name, amount),
_ => println!("未知のレコード種別: {}", record_type),
}
}
他言語なら「if文の羅列」で済ませがちな分岐処理も、Rustのmatchは網羅性チェック(すべてのパターンを尽くしているかをコンパイラが検証する)が効くため、「新しいレコード種別が増えたときに対応漏れがコンパイルエラーで発覚する」という安心感があります。基幹システムの帳票フォーマットは何年もかけて微妙に変化していくものなので、この特性は業務的にかなり刺さりました。
第5章のクロージャも、ログ処理でよく使う形として試しています。
fn main() {
let logs = vec![
"2026-07-01 ERROR db timeout",
"2026-07-01 INFO request ok",
"2026-07-02 ERROR disk full",
];
// クロージャをfilterに渡してERRORログだけ抽出(第5章)
let error_logs: Vec<&&str> = logs.iter()
.filter(|line| line.contains("ERROR"))
.collect();
for line in error_logs {
println!("要調査: {}", line);
}
}
第6章:所有権と借用 ― 本書のもっとも重要な山
ここが本書の核であり、私が一番時間をかけて読んだ章です。他の入門コンテンツと比べて、本書はこの章に入る前段(第2〜5章)でしっかり基礎体力をつけさせてから所有権に入る構成になっており、「なぜこの概念が必要なのか」を、C言語のポインタ管理の面倒さを思い出しながら自然に受け入れられるように作られていると感じました。
実際に、借用のルールに違反するコードを自分でわざと書いて、コンパイラがどう教えてくれるかを確認しました。
fn main() {
let mut connections = vec!["conn_a".to_string(), "conn_b".to_string()];
let first = &connections[0]; // 不変の借用
connections.push("conn_c".to_string()); // 可変の借用(ここでエラーになる)
println!("{}", first);
}
これをコンパイルすると、次のようなエラーが出ます(rustcの標準的なメッセージです)。
error[E0502]: cannot borrow `connections` as mutable because it is also borrowed as immutable
--> src/main.rs:5:5
|
4 | let first = &connections[0];
| ----------- immutable borrow occurs here
5 | connections.push("conn_c".to_string());
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ mutable borrow occurs here
6 | println!("{}", first);
| ----- immutable borrow later used here
初見だと「うるさい」と感じるかもしれませんが、これはVecの中身が再配置されて、firstが指すメモリ位置が無効になる可能性を防いでいるだけです。C/C++であれば、これはまさに「動くこともあれば、ある日突然セグフォルトする」典型的なバグのパターンです。本番のC++コードで似たようなバグを踏んだ経験がある身としては、この章を読み終えたときに「これは説教ではなく、事故を未然に防ぐレビュアーなんだ」と腑に落ちました。
修正するなら、借用のスコープを分けるか、必要な値を先にコピーしておく設計にします。
fn main() {
let mut connections = vec!["conn_a".to_string(), "conn_b".to_string()];
let first = connections[0].clone(); // 値をコピーして借用を終わらせる
connections.push("conn_c".to_string());
println!("{}", first);
}
本書はこの章で、所有権のムーブ、借用(&と&mut)、そしてスライスまでを一つの流れで説明しており、単発の記事やブログではなかなか得られない「体系的な納得感」が得られました。ここを乗り越えられるかどうかが、Rust入門者にとって最大の分岐点だと思います。
第7〜8章:ライブラリとコレクション ― ログ処理・帳票データ処理への応用
第7章のライブラリ(文字列操作・正規表現・日付時刻・ファイルI/O)と第8章のコレクション(リスト・マップ・セット・キュー・イテレータ)は、実務での「泥臭い集計・変換処理」にそのまま直結する章です。私は普段の業務で、複数拠点から上がってくる日次ログを集計する処理をPHPで書いていますが、それをRustで書き直すとどうなるかを試しました。
use std::collections::HashMap;
use regex::Regex;
fn main() {
let lines = vec![
"2026-07-01 12:03:11 site=osaka status=ERROR",
"2026-07-01 12:05:44 site=tokyo status=OK",
"2026-07-01 12:08:02 site=osaka status=ERROR",
];
// 正規表現でsiteとstatusを抜き出す(第7章)
let re = Regex::new(r"site=(\w+) status=(\w+)").unwrap();
// HashMapで拠点ごとのエラー件数を集計(第8章)
let mut error_count: HashMap = HashMap::new();
for line in &lines {
if let Some(caps) = re.captures(line) {
let site = caps
.to_string();
let status = &caps
;
if status == "ERROR" {
*error_count.entry(site).or_insert(0) += 1;
}
}
}
// イテレータでソートして表示(第8章)
let mut result: Vec<(&String, &u32)> = error_count.iter().collect();
result.sort_by(|a, b| b.1.cmp(a.1));
for (site, count) in result {
println!("{}: {}件のエラー", site, count);
}
}
PHPで書くと似たような処理は10〜15行程度で書けますが、Rustだと型が厳密な分、少しコード量は増えます。ただ、entry().or_insert()のようなHashMapのAPIはかなり洗練されていて、「キーが存在するかどうかのif分岐」を書かずに済むのは地味に快適でした。本書の第8章はこうしたイテレータチェーン(filter, map, fold, collect)の使い方も丁寧に扱っており、関数型的な書き方に慣れていく良いステップになっています。
第9章:構造体とトレイト ― DBアクセス層をトレイトで抽象化する
ここは業務目線で一番「使える」と感じた章です。私の仕事では、古いODBC経由のデータベースと、今後導入予定のクラウドDBの両方を扱う可能性があります。トレイトを使えば、DBの種類を切り替え可能な形で抽象化できます。
trait Repository {
fn find_by_id(&self, id: u32) -> Option;
}
// 旧システム向けの実装(イメージ)
struct LegacyOdbcRepository;
impl Repository for LegacyOdbcRepository {
fn find_by_id(&self, id: u32) -> Option {
// 実際にはODBC経由でクエリを発行する想定
if id == 1 {
Some("legacy: 山田太郎".to_string())
} else {
None
}
}
}
// テスト・移行検証用のモック実装
struct MockRepository;
impl Repository for MockRepository {
fn find_by_id(&self, id: u32) -> Option {
if id == 1 {
Some("mock: 山田太郎".to_string())
} else {
None
}
}
}
// ジェネリクスとトレイト境界で、実装を差し替え可能な関数を書く(第9章)
fn print_user(repo: &R, id: u32) {
match repo.find_by_id(id) {
Some(name) => println!("見つかった: {}", name),
None => println!("該当ユーザーなし"),
}
}
fn main() {
let legacy = LegacyOdbcRepository;
let mock = MockRepository;
print_user(&legacy, 1);
print_user(&mock, 1);
}
この構造にしておけば、本番のDBアクセス実装と、テスト用のモック実装を、同じインターフェースの上で差し替えられます。PHPでもインターフェースを使えば似たことはできますが、Rustはコンパイル時に型が確定するため、「モックとの差し替えを忘れて本番にモックが混入する」といった事故が起きにくいのが強みです。基幹システムのリプレイスを検討する際、この設計パターンはそのまま使えると感じました。
第10章:スマートポインター ― 共有状態を安全に扱う
Box、Rc(参照カウンタ付きボックス)、そしてRefCellのような「不変オブジェクトを可変のように扱う」仕組みは、複数のコンポーネントが同じデータを参照するようなアプリケーション(例えばWebアプリのキャッシュ層)でよく使う場面です。
use std::cell::RefCell;
use std::rc::Rc;
struct Cache {
data: RefCell>,
}
fn main() {
let cache = Rc::new(Cache { data: RefCell::new(vec![]) });
// 複数の場所から同じキャッシュを共有する(Rcで参照カウントを増やす)
let cache_a = Rc::clone(&cache);
let cache_b = Rc::clone(&cache);
cache_a.data.borrow_mut().push("session_1".to_string());
cache_b.data.borrow_mut().push("session_2".to_string());
println!("{:?}", cache.data.borrow());
}
Rc<RefCell<T>>は「シングルスレッド内で共有可変状態を持つ」ための定番パターンです。マルチスレッドで使う場合はArc<Mutex<T>>に置き換える必要があり、本書はこの違いにもきちんと触れています。C言語のグローバル変数やポインタの共有で似たようなことをしてきた身としては、「共有していることが型として明示される」感覚が新鮮でした。
第11章:エラー処理 ― 本番障害対応の経験から見えた価値
回復不能なエラー(panic)と回復可能なエラー(Result)の区別は、この章の核です。以前、本番のUbuntuサーバーでext4のファイルシステムが破損し、fsckを何度もかけても解消せず、最終的にLive USBでのレスキュー作業に踏み込んだ経験があります。そのとき痛感したのは、「エラーを握りつぶしたまま処理を続けてしまうこと」の恐ろしさでした。Rustのエラー処理は、この「握りつぶし」を仕組みとして防いでくれます。
use std::fmt;
#[derive(Debug)]
enum ImportError {
FileNotFound(String),
ParseError { line: usize, reason: String },
}
impl fmt::Display for ImportError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
ImportError::FileNotFound(path) => write!(f, "ファイルが見つかりません: {}", path),
ImportError::ParseError { line, reason } => {
write!(f, "{}行目でパースエラー: {}", line, reason)
}
}
}
}
fn parse_line(line: &str, no: usize) -> Result {
line.trim().parse::().map_err(|_| ImportError::ParseError {
line: no,
reason: format!("数値に変換できません: {}", line),
})
}
fn import_data(lines: &[&str]) -> Result, ImportError> {
let mut result = Vec::new();
for (i, line) in lines.iter().enumerate() {
// ?演算子でエラーを早期リターン(第11章)
let value = parse_line(line, i + 1)?;
result.push(value);
}
Ok(result)
}
fn main() {
let lines = vec!["100", "200", "abc"];
match import_data(&lines) {
Ok(values) => println!("取り込み成功: {:?}", values),
Err(e) => eprintln!("取り込み失敗: {}", e),
}
}
この仕組みの良さは、「どこでエラーが起きたか」「どんな種類のエラーなのか」を型として持ち運べる点です。PHPやC++の例外処理だと、catchし忘れたり、握りつぶして無言でnullを返す実装をつい書いてしまいがちですが、Rustではエラーを無視するには明示的に.unwrap()やコンパイラの警告を無視する必要があり、事故を起こしにくい設計になっています。実務でthiserrorやanyhowといった補助クレートを併用すると、この章の内容がさらに実用的になります。
第12〜13章:モジュールシステムとテスト ― Cargoワークスペース設計
第12章のパッケージ・クレート・モジュール・ワークスペース、第13章の単体テスト・結合テスト・ドックテストは、実際にプロジェクトを構成するときの土台になります。Webアプリ、CLIツール、共有ライブラリを1つのリポジトリで管理する構成を試しました。
my-project/
├── Cargo.toml # ワークスペース定義
├── core/ # ビジネスロジックの共有クレート
│ ├── Cargo.toml
│ └── src/lib.rs
├── cli/ # サーバー運用向けCLIツール
│ ├── Cargo.toml
│ └── src/main.rs
└── web/ # Webアプリ(axum使用)
├── Cargo.toml
└── src/main.rs
# ルートのCargo.tomlでワークスペースを定義
[workspace]
members = ["core", "cli", "web"]
coreクレートにビジネスロジックをまとめておけば、CLIツールとWebアプリの両方から同じロジックを呼び出せます。テストはcore側に集約し、cargo test –workspaceでまとめて実行できます。
// core/src/lib.rs
pub fn calc_discount(price: i64, rate: f64) -> i64 {
(price as f64 * (1.0 - rate)) as i64
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_calc_discount() {
assert_eq!(calc_discount(1000, 0.1), 900);
}
}
ドックテスト(コメント内のコードがそのままテストとして実行される仕組み)も試しましたが、これはドキュメントとテストが同期しなくなるという、よくあるあるを構造的に防いでくれるのがありがたい機能です。
第14章:マクロ ― 宣言的マクロと手続き的マクロ
宣言的マクロ(macro_rules!)は、繰り返しの多いコードを圧縮するのに便利です。エラーコードとメッセージの対応表を作る場面で試しました。
macro_rules! define_error_codes {
( $( $name:ident => $code:expr ),* ) => {
#[derive(Debug)]
enum ErrorCode {
$( $name ),*
}
impl ErrorCode {
fn code(&self) -> u32 {
match self {
$( ErrorCode::$name => $code ),*
}
}
}
};
}
define_error_codes! {
NotFound => 404,
ServerError => 500,
Timeout => 504
}
fn main() {
let e = ErrorCode::Timeout;
println!("エラーコード: {}", e.code());
}
手続き的マクロについては、serdeのderive(Serialize, Deserialize)を実際に使うだけでも、その恩恵の大きさが分かります。
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize, Debug)]
struct User {
id: u32,
name: String,
}
fn main() {
let user = User { id: 1, name: "山田太郎".to_string() };
let json = serde_json::to_string(&user).unwrap();
println!("{}", json); // {"id":1,"name":"山田太郎"}
}
マクロは入門書としてはやや高度なテーマですが、本書はこれを終盤(第14章)に配置し、それまでの構造体・トレイト・ジェネリクスの知識を前提として説明する構成になっており、順番通りに読んでいれば無理なく理解できました。
第15章:高度なプログラミング ― スレッド・async/await・ライフタイム
この章は、まさにWeb開発・システム運用に直結する内容です。マルチスレッドでログファイルを並行処理する例を試しました。
use std::thread;
use std::sync::mpsc;
fn main() {
let files = vec!["access1.log", "access2.log", "access3.log"];
let (tx, rx) = mpsc::channel();
for file in files {
let tx = tx.clone();
thread::spawn(move || {
// 実際にはファイルを読んで集計する処理を想定
let dummy_count = file.len() as u32;
tx.send((file.to_string(), dummy_count)).unwrap();
});
}
drop(tx);
for (file, count) in rx {
println!("{}: {}件処理", file, count);
}
}
async/awaitについては、tokioを使ったWebアプリ側で試しています(次のセクションで詳しく書きます)。ライフタイムは、複数の借用を返す関数で明示的な注釈が必要になる場面を試しました。
// 'a というライフタイム注釈で、戻り値の借用が引数の借用より長生きしないことを保証する
fn longest_line<'a>(a: &'a str, b: &'a str) -> &'a str {
if a.len() > b.len() { a } else { b }
}
fn main() {
let log1 = String::from("short log");
let log2 = String::from("this is a much longer log line");
let result = longest_line(&log1, &log2);
println!("長い方: {}", result);
}
ライフタイムは概念としては難しく感じますが、実際に手を動かすと「戻り値の参照が、元のデータより長く生き残ってしまう状況をコンパイラが防いでいる」というだけのことだと分かります。本書がこれを最終章(15章)に置いているのは、順番として非常に納得できます。
Web開発でRustをどう使うか(axum + PHPからの移行視点)
私は普段PHP+JSで業務管理システムのようなものを開発していますが、Rustでの同等のAPIをaxumで書いてみました。
use axum::{routing::get, Router, Json};
use serde::Serialize;
#[derive(Serialize)]
struct OrderSummary {
order_id: u32,
status: String,
}
async fn get_order() -> Json {
Json(OrderSummary {
order_id: 1001,
status: "processing".to_string(),
})
}
#[tokio::main]
async fn main() {
let app = Router::new().route("/orders/1001", get(get_order));
let listener = tokio::net::TcpListener::bind("0.0.0.0:8080").await.unwrap();
axum::serve(listener, app).await.unwrap();
}
PHPだと数行で書けてしまう処理ですが、Rustで書くと型が保証された状態でJSONが返るため、「レスポンスの型が実行時にズレる」というPHPでよく遭遇する不整合が起きません。また、tokioの非同期ランタイムに乗せることで、C10K問題(大量の同時接続)にも強い構成になります。社内システムの規模ではPHPでも十分ですが、外部向けAPIや、アクセスが急増する可能性がある機能については、Rust+axumへの一部移行を検討する価値を感じました。
スマホアプリ開発でRustをどう使うか(Rustコア+iOS/Android)
スマホアプリも開発する立場として気になったのは、Rustのロジックをそのままモバイルアプリに組み込めるかどうかです。UniFFIというツールを使うと、Rustで書いたコアロジックから、SwiftやKotlin向けのバインディングを自動生成できます。
// core/src/lib.rs(UniFFIでバインディングを生成する想定のコア関数)
pub fn calc_tax(price: u64, rate: f64) -> u64 {
(price as f64 * rate) as u64
}
// uniffi.toml や build.rs 経由で生成されたバインディングを、
// Swift側からはこのようなイメージで呼び出せる
let tax = calcTax(price: 1000, rate: 0.1)
これにより、価格計算・データ変換・暗号化処理のような「iOSとAndroidで同じロジックを二重に実装したくない」部分を、Rustの単一実装に集約できます。私はiOSの内部構造をいじる趣味があるので、Swift側からRustのバイナリを呼ぶ構成自体にはすでに馴染みがありましたが、業務のスマホアプリ開発に応用するなら、ビジネスロジック(価格計算、バリデーション、ローカルDBのマイグレーション処理など)をRustコアに切り出す設計は、今後試していきたいと思っています。
Android側についても、UniFFIを使わずに素のJNI(Java Native Interface)経由でRustを呼び出す方法を軽く検証しました。KotlinからRustの関数を直接呼ぶ場合、Rust側ではjniクレートを使い、C ABIに準拠したエクスポート関数を用意します。
use jni::JNIEnv;
use jni::objects::{JClass, JString};
use jni::sys::jstring;
#[no_mangle]
pub extern "system" fn Java_com_example_myapp_RustBridge_greet(
env: JNIEnv,
_class: JClass,
name: JString,
) -> jstring {
let name: String = env.get_string(name).expect("invalid string").into();
let output = env.new_string(format!("こんにちは、{}さん", name)).expect("couldn't create string");
output.into_raw()
}
UniFFIのようなラッパーツールを使えばこの定型コードの多くは自動生成されますが、「裏側で何が起きているか」を一度素のJNIで確認しておくと、ビルドエラーやリンクエラーが起きたときの切り戻しがしやすくなります。iOS内部の解析で普段からFFI境界のバグに向き合っている経験は、こうしたクロス言語連携のデバッグにそのまま活きると感じました。
Linux本番環境でのデプロイと運用
最後に、実際にLinuxサーバーで運用する場合の構成も検証しました。systemdでバイナリをサービス化する例です。
[Unit]
Description=Rust Web API Service
After=network.target
[Service]
Type=simple
ExecStart=/opt/myapp/bin/myapp
Restart=on-failure
User=myapp
Environment=RUST_LOG=info
[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable myapp
sudo systemctl start myapp
sudo journalctl -u myapp -f
Rustのバイナリはランタイム依存がほぼないため、Dockerイメージも非常に軽量にできます。
FROM rust:1.79 AS builder
WORKDIR /app
COPY . .
RUN cargo build --release
FROM debian:bookworm-slim
COPY --from=builder /app/target/release/myapp /usr/local/bin/myapp
CMD ["myapp"]
PHPやNode.jsのDockerイメージだとランタイムやnode_modulesの分だけイメージサイズが大きくなりがちですが、Rustはマルチステージビルドを使うと最終イメージを数十MB程度に抑えられます。サーバーリソースが限られた社内環境では、これは地味に大きなメリットです。
気になった点・注意点
良い点ばかり並べても片手落ちなので、正直な気になった点も書いておきます。
- ボリュームがそれなりにある:B5変型・616ページというサイズは、他の独習シリーズと比べても厚めです。「サクッと入門したい」というニーズには重く、腰を据えて数週間〜数ヶ月かけて取り組む前提で買うべき本だと感じました。
- プログラミング未経験者にはやや不向き:Rust自体の学習ハードルの高さもあり、変数・関数・制御構文といった基礎はある程度別言語で経験している人向けの書き方になっています。完全なプログラミング初心者が最初の言語としてRustを選ぶ場合は、少し歯が立たない可能性があります。
- Webフレームワークやモバイル連携の実践的な話までは踏み込んでいない:axum・actix-webのようなWebフレームワークや、UniFFIのようなモバイル連携ツールは、当然本書の範囲外です。基礎を固めたあとに、自分で応用範囲を広げていく前提の本だと理解しておくべきです。
- 価格はやや高め:定価4,818円(税込)は、独習シリーズの中でも高い部類に入ります。ただしページ数と情報量、そして所有権の説明の丁寧さを考えると、内容に対しては納得できる価格設定だと感じました。
- 非同期処理(async/await)の実務的な深掘りはやや控えめ:第15章でasync/awaitの基本は扱われていますが、tokioのランタイム設計や、Pin・Future周辺の込み入った話までは踏み込んでいません。Webサービスのバックエンドを本格的にRustで書く場合、ここは別途、tokioの公式ドキュメントや専門書で補強する必要があると感じました。とはいえ、これは「独習」という入門書の役割を考えると、むしろ適切な線引きだとも思います。
- クレートのエコシステム全体をどう選ぶかの指針は薄い:Rustは標準ライブラリが比較的小さく、正規表現ならregex、非同期ならtokio、シリアライズならserdeのように、外部クレートを組み合わせて使うのが基本です。本書はライブラリの使い方自体は丁寧に説明していますが、「どのクレートを選ぶべきか」という実務的な判断基準までは扱っていません。この部分は、crates.ioのダウンロード数やGitHubのメンテナンス状況を見ながら、自分で判断していく必要があります。
この本を業務にどう活かしていくか(自分自身の計画)
読み終えて、実際に自分の業務でどう活かすかを整理しました。
- サーバー運用系の小さなCLIツールをRustで書き直す:ログの集計、ファイルの整合性チェック、バックアップの検証など、これまでPowerShellやシェルスクリプトで書いていた運用スクリプトの一部を、clapクレートを使ったRust製CLIに置き換えていく予定です。単一バイナリで配布できるため、複数のサーバーに同じスクリプトをコピーする運用が簡単になります。
- 基幹システム連携部分のプロトタイプをRustで作る:第9章で試したトレイトによるリポジトリパターンを使えば、旧システムのDBアクセスと新システムのDBアクセスを、同じインターフェースの上でテストしながら移行できます。まずは読み取り専用のバッチ処理から、Rustでの実装を試してみようと考えています。
- Webアプリの一部機能をaxumで実験的に切り出す:PHPで動いている社内システムのうち、外部公開の可能性があるAPIや、高頻度アクセスが想定される機能を、Rust+axumのマイクロサービスとして切り出し、既存システムと並行運用しながら検証していきます。
- スマホアプリのコアロジックをRustに寄せる検証:価格計算やデータ変換など、iOS/Android間で重複実装しているロジックを、UniFFI経由でRustコアに統合できるか、小さな機能から試していきます。
いずれも「いきなり全部Rustに置き換える」のではなく、既存のPHP・C++・VB6の資産と共存させながら、事故を起こしにくい部分から段階的に導入していく方針です。Rustは既存のCライブラリとFFI(Foreign Function Interface)で連携できるため、古いC/C++の資産を捨てずに、周辺だけをRust化していくアプローチが現実的だと感じています。
どんな人におすすめか/総評
- C/C++での開発・保守経験があり、「メモリ安全性の高い言語」への乗り換えを検討している人
- 公式の『The Rust Programming Language』やネット上の入門記事で所有権のあたりで一度挫折した人
- 体系的に、順番通りに、抜け漏れなくRustの基礎を身につけたい人
- Webアプリやスマホアプリの開発経験があり、その一部をRustに置き換えていく現実的なルートを探している人
- 業務でレガシーシステムの置き換えや、新規のシステムプログラミング案件を検討していて、Rustの採用可否を技術的に判断したい立場の人
逆に、「とにかく早く簡単なWebサービスをRustで動かしたい」という人には、本書よりも薄い実践寄りの本のほうが合うかもしれません。本書は「基礎体力をきっちりつける」ための一冊であり、その代わりに、読み終えたあとの応用力(Webフレームワーク、モバイル連携、非同期処理への橋渡し)は、自分自身のコードを書きながら伸ばしていく必要があります。
Rustは、C/C++の持つパワーとメモリ安全性を同時に手に入れられる、数少ない現実的な選択肢です。ただしその代償として、他の言語にはない独自の概念を乗り越える必要があります。『独習Rust』は、その乗り越え方を「わかりやすい解説→例題→練習問題」という手堅いステップで、遠回りなようで実は一番着実なルートで案内してくれる一冊でした。そして、読みながら実際にコードを書いて試すことで、Web開発・スマホアプリ開発・サーバー運用のどの現場にも、無理なく橋を架けられる土台になる本だと感じています。
正直に言うと、読み始める前は「所有権のところで、また一度心が折れるかもしれない」という不安がありました。ですが、第2〜5章で丁寧に基礎体力をつけたうえで第6章に入る構成のおかげで、今回は最後まで折れることなく読み切ることができました。これは私と同じように、過去に一度Rustで挫折した経験がある人にとって、かなり大きな意味を持つポイントだと思います。
技術書は「読んで満足する本」と「手を動かして初めて価値が分かる本」に分かれますが、本書は明確に後者です。この記事で紹介したコード例も、本書の各章を読んだ直後にターミナルを開いて実際に試したものばかりです。読み終えたあとに残ったのは、「Rustが難しい言語だ」という感覚ではなく、「Rustのコンパイラは、自分がこれまで踏んできた地雷を、事前に教えてくれる先輩のような存在だ」という感覚でした。基幹システムの保守やWeb・スマホアプリの開発を仕事にしているエンジニアであれば、この一冊は間違いなく本棚に置く価値があります。
書誌情報
- 書名:独習Rust(独習シリーズ)
- 著者:WINGSプロジェクト 山内 直/監修:山田 祥寛
- 出版社:翔泳社
- 発売日:2026年6月15日
- 定価:4,818円(本体4,380円+税10%)
- 仕様:B5変型・616ページ
- ISBN:978-4-7981-8931-4
- 商品ページ:https://wings.web-deli.com/books/978-4-7981-8931-4
※本書は翔泳社様・WINGSプロジェクト様のレビュー企画にて献本いただきました。記事中のコード例は、書籍の各章のテーマに沿って筆者自身が実際に手を動かして検証したものであり、書籍に掲載されているサンプルコードそのものではありません。
