19 4月 2025
Rustは常に開発者の関心を集めており、Stack Overflowの調査で複数年連続で「最も愛されている」プログラミング言語の称号を獲得しています。これは単なる誇大広告ではありません。Rustは、他のシステムプログラミング言語に見られる一般的な問題点に対処する、パフォーマンス、安全性、そして現代的な言語機能の魅力的な組み合わせを提供します。Rustが特別な理由に興味があり、その旅を始めたいと考えているなら、この初心者向けガイドは、あなたが始めるための基礎知識を提供します。コアとなる構文、所有権のようなユニークな概念、そしてRustエコシステムを支える必須のツールを探求します。
Rustは、信頼性が高く効率的なソフトウェアを構築するための言語として位置づけられています。その主な利点は、ガベージコレクタに頼ることなくメモリ安全性を実現し、恐れることのない並行性を可能にすることにあります。Rustを学ぶべき理由は以下の通りです:
最初のRustコードを書く前に、Rustツールチェインをセットアップする必要があります。Rustをインストールする標準的かつ推奨される方法は、Rustツールチェインインストーラーであるrustup
を使用することです。
rustup
: このコマンドラインツールはRustのインストールを管理します。これにより、異なるRustバージョン(stable、beta、nightlyビルドなど)をインストール、更新、そして簡単に切り替えることができます。お使いのオペレーティングシステム固有のインストール手順については、公式Rustウェブサイト(https://www.rust-lang.org/tools/install)をご覧ください。rustc
: これはRustコンパイラです。.rs
ファイルにRustソースコードを書いた後、rustc
はそれらをコンピュータが理解できる実行可能バイナリやライブラリにコンパイルします。重要ではありますが、日常のワークフローでrustc
を直接呼び出すことはあまりありません。cargo
: これはRustのビルドシステム兼パッケージマネージャであり、最も頻繁にやり取りするツールです。Cargoは多くの一般的な開発タスクを調整します:
cargo new
)。cargo build
)。cargo run
)。cargo test
)。Cargo.toml
ファイルにリストされた外部ライブラリ、すなわちクレートを自動的に取得してコンパイルする)。ローカルインストールなしで素早く実験したい場合は、公式のRust PlaygroundのようなオンラインプラットフォームやReplitのような統合開発環境が優れた選択肢です。
新しい言語を学ぶ際の通過儀礼である、伝統的な「Hello, world!」プログラムから始めましょう。main.rs
という名前のファイルを作成し、次のコードを追加します:
fn main() {
// この行はコンソールにテキストを出力します
println!("Hello, Rust!");
}
基本的なツールを使ってこの単純なプログラムをコンパイルして実行するには:
main.rs
を含むディレクトリに移動して、次を実行します:
rustc main.rs
このコマンドはRustコンパイラ(rustc
)を呼び出し、実行可能ファイル(Linux/macOSではmain
、Windowsではmain.exe
など)を作成します。./main
# Windowsの場合: .\main.exe を使用
しかし、単一ファイルを超えるものについては、Cargoを使用するのが標準的で、はるかに便利なアプローチです:
cargo new hello_rust
cd hello_rust
Cargoはhello_rust
という名前の新しいディレクトリを作成し、その中にsrc
サブディレクトリ(すでに「Hello, Rust!」コードが入力されたmain.rs
を含む)とCargo.toml
という名前の設定ファイルが含まれます。cargo run
Cargoはコンパイルステップを処理し、その後、結果のプログラムを実行して、コンソールに「Hello, Rust!」と表示します。コードスニペットを分解してみましょう:
fn main()
: これはmain関数を定義します。fn
キーワードは関数宣言を示します。main
は特別な関数名であり、すべての実行可能なRustプログラムが実行を開始するエントリーポイントです。括弧()
は、この関数が入力パラメータを取らないことを示します。{}
: 波括弧はコードブロックまたはスコープを定義します。関数に属するすべてのコードは、これらの括弧内に入ります。println!("Hello, Rust!");
: この行はコンソールにテキストを出力するアクションを実行します。
println!
はRustのマクロです。マクロは関数に似ていますが、重要な違いがあります:感嘆符!
で終わります。マクロはコンパイル時のコード生成を実行し、通常の関数よりも強力で柔軟性を提供します(println!
が行うような可変数の引数の処理など)。println!
マクロは提供されたテキストをコンソールに出力し、最後に自動的に改行文字を追加します。"Hello, Rust!"
は文字列リテラルです – テキストを表す固定された文字シーケンスで、二重引用符で囲まれています。;
: セミコロンは文の終わりを示します。実行可能なRustコードのほとんどの行(文)はセミコロンで終わります。それでは、Rustプログラミング言語の基本的な構成要素について詳しく見ていきましょう。
変数はデータ値を格納するために使用されます。Rustでは、let
キーワードを使用して変数を宣言します。
let apples = 5;
let message = "Take five";
Rustの核心的な概念は、変数がデフォルトで不変であるということです。これは、一度値が変数名に束縛されると、後でその値を変更できないことを意味します。
let x = 10;
// x = 15; // この行はコンパイル時エラーを引き起こします!不変変数 `x` には二度と代入できません。
println!("The value of x is: {}", x); // {} は x の値のプレースホルダーです
このデフォルトの不変性は、データの偶発的な変更を防ぐことで、より安全で予測可能なコードを書くのに役立つ意図的な設計上の選択です。これはバグの一般的な原因となり得ます。値が変更可能な変数が必要な場合は、宣言時にmut
キーワードを使用して明示的に可変としてマークする必要があります。
let mut count = 0; // 'count'を可変として宣言
println!("Initial count: {}", count);
count = 1; // 'count'が'mut'で宣言されたため、これは許可されます
println!("New count: {}", count);
Rustはシャドーイングも許可します。同じスコープ内で以前の変数と同じ名前の新しい変数を宣言できます。新しい変数は古い変数を「シャドーイング」し、その名前の後続の使用は新しい変数を参照することを意味します。これは、完全に新しい変数を作成しているため、ミューテーションとは異なります。新しい変数は異なる型を持つことさえ可能です。
let spaces = " "; // 'spaces' は最初は文字列スライス (&str)
let spaces = spaces.len(); // 'spaces' は長さを保持する新しい変数(整数、usize)によってシャドーイングされた
println!("Number of spaces: {}", spaces); // 整数値を出力
Rustは静的型付け言語です。これは、すべての変数の型がコンパイル時にコンパイラによって知られている必要があることを意味します。しかし、Rustには優れた型推論があります。多くの場合、明示的に型を書き出す必要はありません。コンパイラはしばしば値と使用方法に基づいて型を推測できます。
let quantity = 10; // コンパイラは i32 (デフォルトの符号付き整数型) を推論
let price = 9.99; // コンパイラは f64 (デフォルトの浮動小数点型) を推論
let active = true; // コンパイラは bool (真偽値) を推論
let initial = 'R'; // コンパイラは char (文字) を推論
明示的にしたい場合や必要な場合(例:明確さのため、またはコンパイラが助けを必要とする場合)、コロン :
に続けて型名を指定して型アノテーションを提供できます。
let score: i32 = 100; // 明示的に符号付き32ビット整数
let ratio: f32 = 0.5; // 明示的に単精度浮動小数点数
let is_complete: bool = false; // 明示的に真偽値
let grade: char = 'A'; // 明示的に文字 (Unicodeスカラー値)
Rustにはいくつかの組み込みスカラー型(単一の値を表す)があります:
i8
, i16
, i32
, i64
, i128
, isize
) は正と負の両方の整数を格納します。符号なし整数 (u8
, u16
, u32
, u64
, u128
, usize
) は非負の整数のみを格納します。isize
とusize
型はコンピュータのアーキテクチャ(32ビットまたは64ビット)に依存し、主にコレクションのインデックス付けに使用されます。f32
(単精度)とf64
(倍精度)。デフォルトはf64
です。bool
型にはtrue
またはfalse
の2つの可能な値があります。char
型は単一のUnicodeスカラー値(単なるASCII文字よりも包括的)を表し、シングルクォートで囲まれます(例:'z'
, 'π'
, '🚀'
)。String
vs &str
Rustでテキストを扱う際には、しばしば2つの主要な型が関与し、初心者にとっては混乱を招くことがあります:
&str
(「文字列スライス」と発音):これは、メモリ内のどこかに格納されたUTF-8エンコードされたバイトシーケンスへの不変参照です。文字列リテラル("Hello"
など)は&'static str
型であり、プログラムのバイナリに直接格納され、プログラムの全期間中存在します。スライスは、データを所有することなく文字列データへのビューを提供します。サイズは固定です。String
:これは、所有され、成長可能で、可変な、UTF-8エンコードされた文字列型です。String
データはヒープに格納され、サイズ変更が可能です。通常、文字列データを変更する必要がある場合、または文字列が自身のデータを所有し、自身のライフタイムを管理する必要がある場合(関数から文字列を返す場合や構造体に格納する場合など)にString
を使用します。// 文字列リテラル(プログラムバイナリに格納され、不変)
let static_slice: &'static str = "I am immutable";
// リテラルから所有されたヒープ割り当ての String を作成
let mut dynamic_string: String = String::from("Start");
// String を変更(可変であり、データを所有しているため可能)
dynamic_string.push_str(" and grow");
println!("{}", dynamic_string); // 出力: Start and grow
// String の一部を参照する文字列スライスを作成
// このスライスは dynamic_string からデータを借用する
let slice_from_string: &str = &dynamic_string[0..5]; // "Start" を参照
println!("Slice: {}", slice_from_string);
関数は、コードを名前付きの再利用可能な単位に整理するための基本です。特別なmain
関数には既に遭遇しました。
// 関数定義
fn greet(name: &str) { // 1つのパラメータ 'name' を取る。これは文字列スライス (&str)
println!("Hello, {}!", name);
}
// 2つの i32 パラメータを取り、i32 を返す関数
fn add(a: i32, b: i32) -> i32 {
// Rustでは、関数本体の最後の式は、
// セミコロンで終わっていなければ自動的に返される。
a + b
// これは return a + b; と書くのと同じ。
}
fn main() {
greet("Alice"); // 'greet' 関数を呼び出す
let sum = add(5, 3); // 'add' を呼び出し、返された値を 'sum' に束縛する
println!("5 + 3 = {}", sum); // 出力: 5 + 3 = 8
}
関数に関するキーポイント:
fn
キーワードを使用します。->
の後に指定します。関数が値を返さない場合、その戻り値の型は暗黙的に()
(空のタプル、しばしば「ユニット型」と呼ばれる)になります。;
で終わる)で構成され、オプションで式(値に評価されるもの)で終わります。return
キーワードは、関数内のどこからでも明示的に早期リターンするために使用できます。Rustは、コードが実行される順序を決定するための標準的な制御フロー構造を提供します:
if
/else
/else if
: 条件付き実行に使用されます。let number = 6;
if number % 4 == 0 {
println!("number is divisible by 4");
} else if number % 3 == 0 {
println!("number is divisible by 3"); // この分岐が実行される
} else {
println!("number is not divisible by 4 or 3");
}
// 重要なことに、'if' は Rust では式であり、値に評価されることを意味します。
// これにより、'let' 文で直接使用できます。
let condition = true;
let value = if condition { 5 } else { 6 }; // value は 5 になります
println!("The value is: {}", value);
// 注意: 'if' 式の両方の分岐は同じ型に評価される必要があります。
loop
: 無限ループを作成します。通常、ループを終了するためにbreak
を使用し、オプションで値を返します。while
: 指定された条件が真である限りループします。for
: コレクションの要素または範囲を反復処理します。これはRustで最も一般的に使用され、しばしば最も安全なループタイプです。// loop の例
let mut counter = 0;
let result = loop {
counter += 1;
if counter == 10 {
break counter * 2; // ループを終了し 'counter * 2' を返す
}
};
println!("Loop result: {}", result); // 出力: Loop result: 20
// while の例
let mut num = 3;
while num != 0 {
println!("{}!", num);
num -= 1;
}
println!("LIFTOFF!!!");
// for の例(配列の反復処理)
let a = [10, 20, 30, 40, 50];
for element in a.iter() { // .iter() は配列の要素に対するイテレータを作成する
println!("the value is: {}", element);
}
// for の例(範囲の反復処理)
// (1..4) は 1, 2, 3 を含む範囲を作成する(4 は含まない)
// .rev() はイテレータを逆順にする
for number in (1..4).rev() {
println!("{}!", number); // 3!, 2!, 1! を出力
}
println!("LIFTOFF AGAIN!!!");
コンパイラが無視する説明やメモをコードに追加するためにコメントを使用します。
// これは単一行コメントです。行末まで続きます。
/*
* これは複数行のブロックコメントです。
* 数行にまたがることができ、長い説明に
* 便利です。
*/
let lucky_number = 7; // 行末にコメントを置くこともできます。
所有権はRustの最も特徴的で中心的な機能です。これは、Rustがガベージコレクタを必要とせずにコンパイル時にメモリ安全性を保証することを可能にするメカニズムです。所有権を把握することがRustを理解する鍵となります。それは3つのコアなルールに従います:
{ // s はここでは有効ではありません。まだ宣言されていません
let s = String::from("hello"); // s はこの時点から有効です。
// s はヒープに割り当てられた String データを「所有」します。
// ここで s を使用できます
println!("{}", s);
} // スコープはここで終了します。's' はもはや有効ではありません。
// Rust は 's' が所有する String のために特別な 'drop' 関数を自動的に呼び出し、
// そのヒープメモリを解放します。
所有される値(String
、Vec
、または所有される型を含む構造体など)を別の変数に代入するか、値渡しで関数に渡すと、所有権はムーブされます。元の変数は無効になります。
let s1 = String::from("original");
let s2 = s1; // String データの所有権は s1 から s2 へムーブ(移動)されます。
// s1 はこの時点以降、有効とは見なされなくなります。
// println!("s1 is: {}", s1); // コンパイル時エラー!ムーブ後にここで値が借用されています。
// s1 はもはやデータを所有していません。
println!("s2 is: {}", s2); // s2 が現在の所有者であり、有効です。
このムーブの挙動は、「二重解放」エラー(2つの変数がスコープ外に出るときに誤って同じメモリ位置を解放しようとする)を防ぎます。整数、浮動小数点数、真偽値、文字などのプリミティブ型はCopy
トレイトを実装しており、代入されたり関数に渡されたりするときにムーブされる代わりに単純にコピーされることを意味します。
所有権を移転せずに、関数に値を使用させたい場合はどうすればよいでしょうか? 参照を作成できます。参照を作成することは借用と呼ばれます。参照を使用すると、所有権を取得せずに、別の変数が所有するデータにアクセスできます。
// この関数は String への参照 (&) を取ります。
// String を借用しますが、所有権は取りません。
fn calculate_length(s: &String) -> usize {
s.len()
} // ここで s (参照) はスコープ外になります。しかし、それは String データを所有していないため、
// 参照がスコープ外になってもデータは破棄されません。
fn main() {
let s1 = String::from("hello");
// '&' シンボルを使用して s1 への参照を渡します。
// s1 は依然として String データを所有しています。
let len = calculate_length(&s1);
// 所有権は決してムーブされなかったため、s1 はここでまだ有効です。
println!("The length of '{}' is {}.", s1, len);
}
参照は、変数と同様にデフォルトで不変です。借用したデータを変更したい場合は、&mut
で示される可変参照が必要です。ただし、Rustはデータ競合を防ぐために、可変参照に関して厳格なルールを強制します:
借用ルール:
&mut T
)。&T
)。これらのルールはコンパイラによって強制されます。
// この関数は String への可変参照を取ります
fn change(some_string: &mut String) {
some_string.push_str(", world");
}
fn main() {
// 可変の借用を許可するために 's' は可変として宣言する必要がある
let mut s = String::from("hello");
// 借用ルールの強制の例(エラーを見るには行のコメントを解除):
// let r1 = &s; // 不変の借用 - OK
// let r2 = &s; // 別の不変の借用 - OK(複数の不変の借用は許可される)
// let r3 = &mut s; // エラー!不変の借用がアクティブな間は `s` を可変として借用できません
// // Rust は強制します:複数のリーダー(&T)か単一のライター(&mut T)のいずれかであり、両方は不可
// println!("{}, {}", r1, r2); // r1/r2 を使用するとアクティブなままになり、エラーを引き起こす
// // この println がなければ、Rust の NLL(Non-Lexical Lifetimes)は r1/r2 を早期に解放し、
// // ここで &mut s を有効にする
// 他の借用がアクティブでないため、ここでは可変の借用が許可される
change(&mut s);
println!("{}", s); // 出力: hello, world
}
Rustは、複数の値をより複雑な型にグループ化する方法を提供します。
構造体(structuresの略)を使用すると、関連するデータフィールドを単一の名前の下にグループ化することにより、カスタムデータ型を定義できます。
// User という名前の構造体を定義
struct User {
active: bool,
username: String, // 所有される String 型を使用
email: String,
sign_in_count: u64,
}
fn main() {
// User 構造体のインスタンスを作成
// インスタンスはすべてのフィールドに値を提供する必要がある
let mut user1 = User {
email: String::from("someone@example.com"),
username: String::from("someusername123"),
active: true,
sign_in_count: 1,
};
// ドット表記を使用して構造体フィールドにアクセス
// フィールド値を変更するにはインスタンスが可変である必要がある
user1.email = String::from("anotheremail@example.com");
println!("User email: {}", user1.email);
// User インスタンスを作成するためのヘルパー関数を使用
let user2 = build_user(String::from("user2@test.com"), String::from("user2"));
println!("User 2 active status: {}", user2.active);
// 構造体更新構文:既存のインスタンスから一部のフィールドを使用して
// 残りのフィールド用に新しいインスタンスを作成する。
let user3 = User {
email: String::from("user3@domain.com"),
username: String::from("user3"),
..user2 // user2 から 'active' と 'sign_in_count' の値を取る
};
println!("User 3 sign in count: {}", user3.sign_in_count);
}
// User インスタンスを返す関数
fn build_user(email: String, username: String) -> User {
User {
email, // フィールド初期化省略記法:パラメータ名がフィールド名と一致する場合
username,
active: true,
sign_in_count: 1,
}
}
Rustはまた、タプル構造体(名前付きタプル、例:struct Color(i32, i32, i32);
)と、フィールドがなく、型にトレイトを実装する必要があるがデータを格納する必要がない場合に役立つユニット様構造体(例:struct AlwaysEqual;
)もサポートしています。
列挙型(enumerations)を使用すると、可能なバリアントを列挙することによって型を定義できます。列挙型の値は、その可能なバリアントのうちの1つにしかなれません。
// IPアドレスの種類を定義する単純な列挙型
enum IpAddrKind {
V4, // バリアント 1
V6, // バリアント 2
}
// 列挙型のバリアントは関連データも保持できる
enum IpAddr {
V4(u8, u8, u8, u8), // V4 バリアントは4つの u8 値を保持
V6(String), // V6 バリアントは String を保持
}
// Rustの標準ライブラリで非常に一般的で重要な列挙型: Option<T>
// 存在するかもしれないし、存在しないかもしれない値の概念をエンコードする。
// enum Option<T> {
// Some(T), // T 型の値が存在することを表す
// None, // 値が存在しないことを表す
// }
// もう一つの重要な標準ライブラリ列挙型: Result<T, E>
// 成功(Ok)または失敗(Err)する可能性のある操作に使用される。
// enum Result<T, E> {
// Ok(T), // 成功を表し、T 型の値を含む
// Err(E), // 失敗を表し、E 型のエラー値を含む
// }
fn main() {
let four = IpAddrKind::V4;
let six = IpAddrKind::V6;
// 関連データを持つ IpAddr 列挙型のインスタンスを作成
let home = IpAddr::V4(127, 0, 0, 1);
let loopback = IpAddr::V6(String::from("::1"));
// Option<T> の例
let some_number: Option<i32> = Some(5);
let no_number: Option<i32> = None;
// 'match' 制御フロー演算子は列挙型を扱うのに最適です。
// 列挙型のバリアントに基づいて異なるコードを実行できます。
match some_number {
Some(i) => println!("Got a number: {}", i), // Some の場合、内部値を i に束縛
None => println!("Got nothing."), // None の場合
}
// 'match' は網羅的でなければなりません:すべての可能なバリアントを処理する必要があります。
// アンダースコア '_' は、明示的にリストされていないバリアントをキャッチするための
// ワイルドカードパターンとして使用できます。
}
Option<T>
とResult<T, E>
は、潜在的に欠落している値やエラーを堅牢に処理するためのRustのアプローチの中心です。
CargoはRustエコシステムの不可欠な部分であり、Rustプロジェクトのビルド、テスト、管理のプロセスを効率化します。これは開発者がしばしば賞賛する機能の1つです。
Cargo.toml
: これはRustプロジェクトのマニフェストファイルです。TOML(Tom's Obvious, Minimal Language)形式で書かれています。プロジェクトに関する必須のメタデータ(名前、バージョン、作者など)と、決定的に重要な依存関係(プロジェクトが依存する他の外部クレート)のリストが含まれています。
[package]
name = "my_project"
version = "0.1.0"
edition = "2021" # 使用するRustエディションを指定(言語機能に影響)
# 依存関係は以下にリストされます
[dependencies]
# 例:乱数生成のために 'rand' クレートを追加
# rand = "0.8.5"
# ビルドすると、Cargo は 'rand' とその依存関係をダウンロードしてコンパイルします。
cargo new <project_name>
: 新しいバイナリ(実行可能)アプリケーションプロジェクト構造を作成します。cargo new --lib <library_name>
: 新しいライブラリ(他のプログラムで使用されることを意図したクレート)プロジェクト構造を作成します。cargo build
: プロジェクトとその依存関係をコンパイルします。デフォルトでは、最適化されていないデバッグビルドを作成します。出力はtarget/debug/
ディレクトリに置かれます。cargo build --release
: 配布やパフォーマンステストに適した、最適化を有効にしてプロジェクトをコンパイルします。出力はtarget/release/
ディレクトリに置かれます。cargo run
: バイナリプロジェクトをコンパイル(必要な場合)して実行します。cargo check
: 最終的な実行可能ファイルを実際に生成せずに、コードのコンパイルエラーを迅速にチェックします。これは通常cargo build
よりもはるかに高速で、開発中の迅速なフィードバックに役立ちます。cargo test
: プロジェクト内で定義されたテスト(通常はsrc
ディレクトリまたは別のtests
ディレクトリに配置)を実行します。Cargo.toml
ファイルに依存関係を追加し、cargo build
やcargo run
のようなコマンドを実行すると、Cargoは中央リポジトリcrates.ioから必要なクレート(およびその依存関係)をダウンロードし、すべてをまとめてコンパイルする処理を自動的に行います。
この入門ガイドでは、あなたが始めるための絶対的な基本をカバーしました。Rust言語は、学習を進めるにつれて探求すべき、さらに多くの強力な機能を提供します:
Result
列挙型の習得、?
演算子を使用したエラー伝播、カスタムエラー型の定義。Vec<T>
(動的配列/ベクター)、HashMap<K, V>
(ハッシュマップ)、HashSet<T>
などの一般的なデータ構造の効果的な使用。<T>
)を使用して、異なるデータ型に対して操作できる柔軟で再利用可能なコードの記述。'a
)が必要になることの理解。Mutex
とArc
による共有状態、そして非同期プログラミングのためのRustの強力なasync
/await
構文の探求。Rustはユニークで魅力的な提案を提示します:C++のような低レベル言語に期待される生のパフォーマンスと、特にメモリ管理と並行性に関する一般的なバグのクラス全体を排除する強力なコンパイル時の安全性保証の組み合わせです。学習曲線には所有権や借用のような新しい概念を内在化すること(そして時には厳格なコンパイラに耳を傾けること)が含まれますが、その見返りは、非常に信頼性が高く、効率的で、長期的にはしばしば保守が容易なソフトウェアです。Cargoによる優れたツールと支援的なコミュニティにより、Rustは学ぶ価値のある言語です。
小さく始め、Cargoを使って実験し、コンパイラの役立つ(時には詳細すぎる)エラーメッセージをガイダンスとして受け入れれば、この強力でますます人気が高まっている言語を習得する道を着実に歩むことになるでしょう。楽しいRustプログラミングを!