Rust入門|安全性と速度を両立するプログラミング言語の基本と特徴

kento_morota 12分で読めます

Rustは、Mozillaが開発したシステムプログラミング言語で、メモリ安全性とパフォーマンスを両立する独自のアプローチで注目を集めています。Stack Overflowの開発者調査で「最も愛されている言語」に何年も連続で選ばれ、Linux カーネル、Android、Windows、Chromiumなどの大規模プロジェクトでの採用も進んでいます。

本記事では、Rustの特徴と設計思想、環境構築、基本文法、そしてRust独自の概念である所有権システムまで、入門に必要な知識を実践的に解説します。

Rustの特徴と設計思想

Rustはなぜ「安全性と速度の両立」を実現できるのか、その設計思想を理解しましょう。

ゼロコスト抽象化

Rustは「ゼロコスト抽象化(Zero-Cost Abstractions)」を掲げています。高レベルの抽象化を使っても、手書きの低レベルコードと同等のパフォーマンスが得られるという原則です。イテレーター、クロージャー、ジェネリクスなどの高レベル機能を使っても、コンパイル時に最適化され、ランタイムのオーバーヘッドはありません。

コンパイル時のメモリ安全性保証

C/C++ではメモリに関するバグ(ダングリングポインタ、バッファオーバーフロー、二重解放など)がセキュリティ脆弱性の主要な原因です。Rustは「所有権システム」によって、これらの問題をコンパイル時に検出・防止します。ガベージコレクタ(GC)を使わずにメモリ安全性を保証する、独自のアプローチです。

GCなしでの高パフォーマンス

JavaやGoなどのGC付き言語では、GCの実行中にプログラムが一時停止(Stop the World)することがあります。Rustにはそもそもの値のライフタイムがコンパイル時に決定されるため、GCが不要です。リアルタイム性が求められるシステムや、高スループットが必要なサービスに適しています。

Rustが活躍する分野

Webブラウザのエンジン、OS・デバイスドライバ、ゲームエンジン、WebAssembly、コマンドラインツール、バックエンドサービスなど、パフォーマンスと信頼性が求められる領域で採用が広がっています。

環境構築と最初のプログラム

Rustの開発環境を構築し、最初のプログラムを動かしましょう。

rustupによるインストール

Rustの公式インストーラーであるrustupを使います。ターミナルでcurl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | shを実行するだけで、Rustコンパイラ(rustc)、パッケージマネージャー(Cargo)、標準ライブラリがインストールされます。

rustupは複数のRustバージョンを管理でき、stable、beta、nightlyの各チャンネルを切り替えられます。通常の開発ではstableチャンネルを使用します。

Cargoによるプロジェクト管理

Cargoは、Rustの公式ビルドツール兼パッケージマネージャーです。cargo new プロジェクト名で新しいプロジェクトを作成すると、以下のディレクトリ構造が生成されます。

Cargo.tomlがプロジェクトの設定ファイル(依存関係、バージョンなどを管理)、src/main.rsがエントリーポイントです。cargo runでビルドと実行、cargo build --releaseで最適化されたリリースビルドを生成します。

Hello Worldの実行

生成されたsrc/main.rsには既にHello Worldのコードが含まれています。fn main()がエントリーポイントで、println!マクロで文字列を出力します。Rustでは!が付いている呼び出しはマクロです。

cargo runを実行すると、コンパイルされて「Hello, world!」と出力されます。

基本文法:変数・型・制御構文

Rustの基本文法を確認しましょう。独特の概念がいくつかありますが、他の言語の経験があれば理解しやすいです。

変数と不変性

Rustの変数はデフォルトで不変(イミュータブル)です。let x = 5;で宣言した変数xに後から別の値を代入しようとすると、コンパイルエラーになります。可変にしたい場合はlet mut x = 5;mutキーワードを付けます。

デフォルト不変という設計により、意図しない値の変更を防ぎ、コードの安全性と可読性が向上します。

また、Rustにはシャドーイング(Shadowing)という機能があり、同じ変数名で新しい変数を宣言できます。型の変換などに便利ですが、既存の変数を変更するのではなく、新しい変数を作成する操作です。

データ型

Rustは静的型付け言語で、コンパイル時にすべての変数の型が決定されます。

スカラー型
整数型(i8、i16、i32、i64、i128、u8〜u128)、浮動小数点型(f32、f64)、論理型(bool)、文字型(char、4バイトのUnicode)があります。

複合型
タプル(異なる型の値を固定長でまとめる)と配列(同じ型の値を固定長でまとめる)があります。可変長のコレクションとしてVec(ベクター)がよく使われます。

文字列型
Rustの文字列は2種類あります。String(ヒープ上に確保される可変の文字列)と&str(文字列スライス、他の文字列への参照)です。この区別は所有権の概念と密接に関連しています。

制御構文

if式
Rustのifは「式」です。値を返せるため、let result = if condition { value1 } else { value2 };のように書けます。三項演算子の代わりに使えます。

ループ
loop(無限ループ)、while(条件付きループ)、for(イテレーションループ)の3種類があります。forは主にイテレーターと組み合わせてfor item in collection { ... }の形で使います。

match式
Rustのパターンマッチングは非常に強力です。match式は値に対して複数のパターンを照合し、マッチしたパターンに対応する処理を実行します。すべてのケースを網羅する必要があり(exhaustive matching)、漏れがあるとコンパイルエラーになります。

所有権(Ownership):Rust最大の特徴

所有権はRustの最も重要で独自の概念です。メモリ安全性を保証する仕組みの根幹を成しています。

所有権の3つのルール

ルール1:Rustの各値には「所有者」と呼ばれる変数が1つだけ存在する
let s = String::from("hello");と書くと、変数sが文字列"hello"の所有者になります。

ルール2:所有者は同時に1つだけ
let s2 = s;と書くと、所有権がsからs2に「移動(ムーブ)」します。この後、変数sは使えなくなります。これがRustの「ムーブセマンティクス」です。

ルール3:所有者がスコープを外れたとき、値は破棄される
変数がスコープの終わり(})に達すると、Rustは自動的にその変数が所有するメモリを解放します。これがGCなしでメモリを管理する仕組みです。

借用(Borrowing)

所有権を移動せずに値を参照したい場合は「借用」を使います。&で不変参照を、&mutで可変参照を作成します。

借用には重要なルールがあります。不変参照はいくつでも同時に存在できますが、可変参照は同時に1つだけしか存在できません。また、不変参照と可変参照は同時に存在できません。このルールにより、データ競合がコンパイル時に防止されます。

ライフタイム(Lifetime)

ライフタイムは、参照が有効な期間を示すアノテーションです。多くの場合、コンパイラが自動的に推論しますが、関数の引数と戻り値の参照の関係が曖昧な場合は、明示的なライフタイム指定が必要になります。

ライフタイムの主な目的は、ダングリング参照(解放されたメモリへの参照)を防ぐことです。参照が参照先の値よりも長生きしないことをコンパイラが検証します。

エラーハンドリング

Rustのエラーハンドリングは、安全性を重視した設計です。例外(try-catch)の代わりに、型システムを使ってエラーを扱います。

Result型とOption型

Result<T, E>
操作が成功(Ok(T))または失敗(Err(E))する可能性がある場合に使います。ファイルのオープン、ネットワーク通信、パースなど、失敗しうる操作の戻り値として使われます。

Option<T>
値が存在する(Some(T))または存在しない(None)場合に使います。他の言語のnullに相当しますが、型システムで安全に扱えるためnull参照エラーが発生しません。

?演算子

エラーハンドリングをシンプルに書くための?演算子があります。Result型を返す関数呼び出しに?を付けると、Okの場合は値を取り出し、Errの場合は呼び出し元にエラーを伝播させます。

これにより、エラーの伝播チェーンを簡潔に記述でき、Go言語のif err != nilの連鎖よりもコンパクトなコードが書けます。

panic!とunwrap

panic!はプログラムを即座にクラッシュさせるマクロです。回復不能なエラー(プログラミングのバグ)に使います。unwrap()はResult/OptionのOk/Some値を取り出し、Err/Noneの場合はpanicします。

プロダクションコードではunwrapの使用を避け、適切なエラーハンドリングを行うべきです。unwrapは主にプロトタイプやテストコードで使用します。

構造体・列挙型・トレイト

Rustの型システムの中核を成す3つの要素を解説します。

構造体(struct)

Goと同様、Rustもクラスの代わりに構造体を使います。structでデータの構造を定義し、implブロックでメソッドを実装します。

implブロック内でfn new(...) -> Selfのような関連関数(selfを引数に取らない関数)を定義することで、コンストラクタとして使えます。

列挙型(enum)

Rustのenumは非常に強力です。他の言語のenumとは異なり、各バリアントにデータを持たせられます。Result型やOption型もenumとして定義されています。

match式と組み合わせることで、すべてのバリアントを漏れなく処理できます。新しいバリアントが追加された場合、matchが網羅的でなくなるとコンパイルエラーが発生するため、処理漏れを防げます。

トレイト(trait)

トレイトは、型が実装すべきメソッドの集合を定義する仕組みです。他の言語のインターフェースに相当しますが、デフォルト実装を持てる点が異なります。

標準ライブラリにはDisplay(表示用のフォーマット)、Clone(値の複製)、Iterator(イテレーション)など、多くのトレイトが定義されています。これらを自分の型に実装することで、標準ライブラリの機能をフル活用できます。

実践:CLIツールの作成

RustはCLIツールの開発にも適しています。簡単なファイル検索ツールの作成を通じて、実践的なRustの使い方を見てみましょう。

引数の処理

標準ライブラリのstd::env::args()でコマンドライン引数を取得できます。より本格的な引数解析にはclapクレート(ライブラリ)が広く使われています。clapを使うとサブコマンド、フラグ、ヘルプメッセージの自動生成などが簡単に実現できます。

ファイル操作

std::fsモジュールでファイルの読み書きを行います。fs::read_to_string("file.txt")でファイルの内容を文字列として読み込めます。Result型が返るため、ファイルが存在しない場合のエラーハンドリングも自然に行えます。

大きなファイルを扱う場合はBufReaderを使った行単位の読み込みが効率的です。

クレート(crate)の活用

Rustのパッケージはクレート(crate)と呼ばれ、crates.io(https://crates.io)で公開されています。Cargo.tomlの[dependencies]セクションにクレート名とバージョンを記述するだけで、cargo build時に自動的にダウンロード・ビルドされます。

よく使われるクレートとして、serde(シリアライゼーション/デシリアライゼーション)、tokio(非同期ランタイム)、reqwest(HTTPクライアント)、anyhow(エラーハンドリング)などがあります。

まとめ

Rustは、所有権システムによるコンパイル時のメモリ安全性保証と、GCなしのネイティブパフォーマンスを両立した言語です。学習曲線は他の言語より急ですが、所有権・借用・ライフタイムの概念を理解すれば、安全で高速なプログラムを書けるようになります。

Result型とOption型によるエラーハンドリング、強力なenumとパターンマッチング、トレイトによる多態性など、型システムを最大限に活用した設計が、バグの少ない堅牢なコードを支えます。

まずはCargoでプロジェクトを作成し、基本文法と所有権の基礎を学びましょう。簡単なCLIツールの作成は、Rustの実力を体感する良い出発点です。コンパイラのエラーメッセージが非常に親切なので、コンパイラを先生として対話しながら学習を進めてください。

#Rust#プログラミング#安全性
共有:
無料メルマガ

週1回、最新の技術記事をお届け

AI・クラウド・開発の最新記事を毎週月曜にメールでお届けします。登録は無料、いつでも解除できます。

プライバシーポリシーに基づき管理します

起業準備に役立つ情報、もっとありますよ。

まずは話だけ聞いてもらう