rustを学んで③

·
#CLI#Rust#Terminal#rustlings

Rustの学習記録:躓いたポイントと疑問解決まとめ③

ズブの素人の私が、Rustを学び始めて最初に躓いたポイントと、その解決方法をまとめました。同じような疑問を持つ方の参考になれば幸いです。


1. ベクタ(Vec)と配列の違い

最初の疑問:「vecって要するにJSの配列みたいなもの?」

Rustを学び始めて最初に出会ったVec型。「可変長でヒープに格納される」と聞いて、「これってJavaScriptの普通の配列と同じでは?」と思いました。

結論:似ているが、重要な違いがある

共通点

// Rust
let mut v = vec![1, 2, 3];
v.push(4);
v.pop();
let len = v.len();
// JavaScript
let v = [1, 2, 3];
v.push(4);
v.pop();
let len = v.length;

どちらも動的にサイズを変更でき、要素の追加・削除が可能です。

決定的な違い:型の制約

// Rust: 同じ型しか入らない
let v: Vec<i32> = vec![1, 2, 3];
// v.push("文字列");  // コンパイルエラー!
// JavaScript: 何でも混在できる
let v = [1, 2, "文字列", true];  // OK

学び:RustのVecは「型が統一されたJavaScriptの配列」と理解すると良い


2. 範囲外アクセスで実行時パニック

躓いたコード

let mut vect = vec![10, 20, 30];
vect.push(40);
println!("{} {} {} {}", vect[0], vect[1], vect[2], vect[4]); // パニック!

エラーメッセージ:

thread 'main' panicked at src/main.rs:4:60:
index out of bounds: the len is 4 but the index is 4

疑問:「なぜコンパイル時にエラーにならないの?」

JavaScriptでも実行時エラーになるのは分かっていましたが、Rustは静的型付け言語なのに、なぜコンパイル時に検出できないのか疑問でした。

答え:Vecのサイズは実行時に決まるから

  • Vecは可変長なので、pushpopで動的にサイズが変わる
  • コンパイラは実行時の値を予測できない
  • だから実行時までエラーを検出できない

固定長配列ならコンパイルエラーになる

let arr = [10, 20, 30, 40];
println!("{}", arr[4]);  // コンパイルエラー!

配列はサイズが固定なので、コンパイラが範囲外アクセスを検出できます。

学び:柔軟性(サイズ変更可能)のトレードオフとして、範囲外アクセスは実行時まで検出できない

安全なアクセス方法

// 危険:パニックの可能性
let value = vect[4];

// 安全:Optionで返ってくる
match vect.get(4) {
    Some(value) => println!("値: {}", value),
    None => println!("その要素は存在しません"),
}

3. 型を混在させたい時の方法

疑問:「Rustで異なる型を1つのVecに入れたい時はどうするの?」

JavaScriptなら[1, "hello", true]のように何でも入れられますが、Rustでは同じ型しか入りません。

解決策:Enum(列挙型)を使う

enum Value {
    Int(i32),
    Float(f64),
    Text(String),
}

let mut v = vec![
    Value::Int(42),
    Value::Text(String::from("hello")),
    Value::Float(3.14),
];

// 使うときはパターンマッチング
for item in &v {
    match item {
        Value::Int(n) => println!("整数: {}", n),
        Value::Float(f) => println!("浮動小数点: {}", f),
        Value::Text(s) => println!("文字列: {}", s),
    }
}

学び:Enumは型安全を保ちつつ、異なる種類のデータを扱える最も推奨される方法


4. スライスは構文、メソッドではない

疑問:「&s[0..5]ってメソッドじゃないの?」

JavaScriptではs.slice(0, 5)のようにメソッドを使うので、Rustでも同じだと思っていました。

答え:スライスは言語組み込みの構文

let s = String::from("hello world");
let slice = &s[0..5];  // これは構文(メソッド呼び出しではない)

なぜ構文なのか?

配列やVecでも同じ書き方ができるようにするため:

let arr = [1, 2, 3, 4, 5];
let slice = &arr[1..3];  // [2, 3]

let v = vec![10, 20, 30, 40];
let slice = &v[0..2];  // [10, 20]

統一されたインターフェースなので、言語レベルでサポートされています。

でも、メソッドもたくさんある

let s = String::from("hello world");

s.split(' ')           // メソッド
s.split_whitespace()   // メソッド
s.len()                // メソッド

ドット(.)で呼び出すものはメソッドです。

学び:スライス[..]だけが特別な構文で、その他は普通にメソッドがある


5. insertの使い分け

疑問:「insertってHashMapのものじゃないの?」

HashMapでinsertを見た後、Vecでもinsertが出てきて混乱しました。

答え:同じ名前でも、型によって意味が違う

// Vecのinsert
let mut v = vec![1, 2, 3];
v.insert(1, 99);  // インデックス1に99を挿入
// [1, 99, 2, 3]

// HashMapのinsert
let mut map = HashMap::new();
map.insert("key", 100);  // キーと値を挿入
  • Vec: insert(インデックス, 値)
  • HashMap: insert(キー, 値)

学び:静的型付け言語では、型によってメソッドの動作が決まる。JavaScriptでは名前が違うが、Rustは「挿入する」という共通概念で同じ名前を使っている


6. HashMapは配列ではない

疑問:「HashMapって配列になるの?」

キーと値のペアを保存すると聞いて、配列の一種かと思いました。

答え:HashMapと配列は別物

配列/Vec

  • インデックス(数値)でアクセス: v[0], v[1]
  • 順序が保証される
  • 連続したメモリ

HashMap

  • キー(任意の型)でアクセス: map["Blue"]
  • 順序は保証されない
  • ハッシュテーブルで実装
let mut map = HashMap::new();
map.insert("apple", 100);
map.insert("banana", 200);
map.insert("cherry", 300);

// 出力順序は不定!
for (k, v) in &map {
    println!("{}: {}", k, v);  // apple, banana, cherryの順とは限らない
}

学び:HashMapはJavaScriptのMapObjectに近い。順序が必要なら別のデータ構造を使う


7. 構造体と所有権

疑問:「なぜbuild_user関数でキーに所有権を渡すの?」

scores.insert(String::from("Blue"), 10);

答え:HashMapのinsertは所有権を奪う設計だから

let key = String::from("Blue");
scores.insert(key, 10);
// この後、keyは使えない!(所有権が移動した)

なぜこの設計?

  • キーが外部で変更されてハッシュ値が変わるのを防ぐ
  • メモリ安全性を保証

より実用的なパターン

// パターン1:引数をStringにする
fn build_user(email: String, username: String) -> User {
    User { email, username, /* ... */ }
}
build_user("email".to_string(), "username".to_string())

// パターン2:引数を&strにする(より一般的)
fn build_user(email: &str, username: &str) -> User {
    User {
        email: email.to_string(),
        username: username.to_string(),
        /* ... */
    }
}
build_user("email", "username")  // 呼び出し側が楽

学び:パターン2の方が実用的。呼び出し側がシンプルで、柔軟性も高い


8. 構造体更新構文の間違い

躓いたコード

let user2 = User {
    username: String::from("uni"),
    email: String::from("email2"),
    .user1  // エラー!
};

エラー:

expected identifier, found `.`

正しい書き方:

let user2 = User {
    username: String::from("uni"),
    email: String::from("email2"),
    ..user1  // ドットが2つ!
};

..user1は「残りのフィールドはuser1から取る」という意味です。

学び:構造体更新構文は..(ドット2つ)。1つだと構文エラーになる


9. Debugトレイトでの表示

躓いたコード

#[derive(Degug)]  // タイポ
struct User { /* ... */ }

println!("{}", user1);  // エラー

エラー:

`User` doesn't implement `std::fmt::Display`

正しい書き方:

#[derive(Debug)]  // Degug → Debug
struct User { /* ... */ }

println!("{:?}", user1);  // {:?} を使う
  • {}: Displayトレイトが必要(ユーザー向けの表示)
  • {:?}: Debugトレイトで表示(デバッグ用)
  • {:#?}: 整形されたDebug表示

学び:自作の構造体を表示するには#[derive(Debug)]を付けて、{:?}で表示する


10. データ構造の使い分け

疑問:「配列、タプル、HashMap...いつ何を使えばいいの?」

JavaScriptやPythonでも似たようなデータ構造があり、しっかり理解できていない気がしていました。

基本的な分類:

1. 順序付きコレクション(インデックスでアクセス)

  • Vec/配列: [1, 2, 3]
  • 用途:順序が重要、番号で管理

2. キーと値のペア(キーでアクセス)

  • HashMap: {"name": "太郎", "age": 20}
  • 用途:名前で管理したい

3. 固定の複数の値(位置で意味が決まる)

  • タプル: (名前, 年齢, 住所)
  • 用途:関連する異なる型のデータをまとめる

使い分けの目安:

リスト of 同じ種類のもの → Vec/配列
  例:スコアのリスト、ユーザーのリスト

名前で引きたい → HashMap
  例:設定値、ユーザー情報の検索

2〜3個の関連データ → タプル
  例:座標(x, y)、関数の複数の戻り値

学び:これは言語を超えた共通概念。Rustだけでなく、他の言語でも同じ考え方が使える


11. 構文とメソッドの違い

疑問:「そもそも構文って何?」

スライスの話で「構文」という言葉が出てきて、メソッドとの違いが分からなくなりました。

構文(シンタックス)とは: プログラミング言語のルールや書き方そのもの。言語が最初から提供している「文法」。

// 構文の例
if x > 10 { }       // if文
for i in 0..10 { }  // for文
let x = 5;          // 変数宣言
&s[0..5]            // スライス
fn main() { }       // 関数定義

メソッドとは: 型に紐付いた関数。既存の型に対して呼び出せる機能。

// メソッドの例
s.len()
s.split(' ')
v.push(10)

違いのまとめ:

構文メソッド
誰が提供?言語自体型(標準ライブラリや自作)
自分で作れる?❌ 不可✅ 可能
if, for, let, [..].len(), .push(), .split()

学び:基本ルールが構文、便利機能がメソッド。JavaScriptでも同じ区別がある


まとめ

Rustを学び始めて最初に躓いたポイントをまとめました。特に印象的だったのは:

  1. 型の制約が厳しいが、安全性のため:JavaScriptのような自由度はないが、コンパイル時に多くのエラーを防げる
  2. 所有権システムは独特だが理にかなっている:最初は戸惑うが、メモリ安全性を保証するための仕組み
  3. 静的型付け言語の特性を理解すること:型によってメソッドの動作が決まる、実行時ではなくコンパイル時に多くのチェックが入る
  4. 構文とメソッドの区別:言語の基本機能と、型に付属する機能を区別して理解する

今後の学習方針:

  • ルールが複雑なので、まずはシンプルな書き方から始める
  • とほほのRust入門や公式Docで基礎を固める
  • その後rustlingsで演習に取り組む

ADHDっぽく色々なところに興味が飛びますが、それがプログラミング学習には良い方向に働いている気がします。(前向きですね… 疑問が出たらすぐに調べ、手を動かして確認することで、着実に理解が深まっていると感じています。


この記事が、同じようにRustを学び始めた方の参考になれば幸いです。間違いや改善点があれば、コメントで教えていただけると嬉しいです!