rustを学んで[rustlings]

·
#CLI#Rust#Terminal#rustlings

Rustの学習記録: Rustのmatch構文と文字列型の理解 - 初心者が躓いたポイント

はじめに

Rustlingsでmatch構文と文字列型(&strString)を学習する中で、多くの「なぜ?」に直面しました。この記事では、私が実際に躓いたポイント、勘違いしていた概念、そして最終的にどう理解したかを時系列で記録します。


1. match構文:パターンの書き方がわからない

最初の疑問

rustlingsの問題でこのようなコードに出会いました:

fn process(&mut self, message: Message) {
    match message {
        Message::Resize { width, height } => {
            self.resize(width, height);
        }
        Message::Move(point) => {
            self.move_position(point);
        }
        Message::Echo(s) => {
            self.echo(s);
        }
        Message::ChangeColor(r, g, b) => {
            self.change_color(r, g, b);
        }
        Message::Quit => {
            self.quit();
        }
    }
}

私の疑問:なぜ{}()で書き方が違うの?何か深い理由があるのか?パッとしない…なんとなくタプルや何かを使ってるんだろうとわかるけど…スッキリしない

解決:Enum定義との1対1対応

答えはシンプルでした。Enumの定義の形と、matchのパターンは完全に一致するというルールです。

// Enum定義
enum Message {
    Quit,                          // データ無し
    Move { x: i32, y: i32 },      // 名前付きフィールド(構造体形式)
    Echo(String),                  // タプル構造体形式
    ChangeColor(i32, i32, i32),   // タプル構造体形式(複数値)
}

// match パターン(定義の形をコピーして型を変数名に置き換える)
match message {
    Message::Quit => { ... }                    // データ無し → そのまま
    Message::Move { x, y } => { ... }          // {} → {}で受け取る
    Message::Echo(s) => { ... }                // () → ()で受け取る
    Message::ChangeColor(r, g, b) => { ... }   // () → ()で受け取る
}

コツ:Enum定義の型名部分を変数名に置き換えるだけ。

定義側:ChangeColor(i32, i32, i32)
           ↓  ↓  ↓
パターン:ChangeColor(r,  g,  b)   ← 型を変数名に置き換え

私の気づき

Claudeへと質問、「enumで型を作って、matchで変数定義でしょうか?{}()についても引き継ぎというか、当たり前だよねって感じでしょうか?」

この直感は正しかったです。matchはEnumの種類判定 + データ取り出しを同時に行う構文で、定義の形を「引き継ぐ」という理解が本質でした。


2. ->=> の違い

最初の疑問

「なんとなくわかる気がするけど言語化できない」という状態でした。

解決:役割が全く違う

// -> は「戻り値の型」を示す
fn add(a: i32, b: i32) -> i32 {
//                     ^^
//                  「何を返すか」の型宣言
    a + b
}

// => は「パターンと処理の対応」を示す
match x {
    1 => println!("one"),
//  ↑  ^^
// パターン  「このパターンなら、この処理」
}

整理

  • -> : 「何を返すか」へ流れる(型へ向かう)
  • => : 「何をするか」へ流れる(処理へ向かう)

見た目も->は細く=>は太い。太い方が「処理の塊へ」という視覚的なヒントにもなっています。


3. trim()で躓いた:&mut strが必要だと思った

問題コード

最初に書いたコード:

fn trim_me(input: &mut str) -> &mut str {
    input.trim()  // エラー!
}

エラー内容

error[E0308]: mismatched types
expected `&mut str`, found `&str`

私の勘違い

Claudeへの質問、「元の文字列から空白を除いた部分を指す新しいスライスなので&mutが必要と勘違いしました。というよりそう考えるのがスムーズかと、なぜなら所有権の問題で…しかし変更したコピーということなのかな?」

何を誤解していたか

  • 「新しいスライス」=「何か新しいものを作る」=「変更が必要」=「&mutが必要」という連想
  • 「所有権」という言葉に引っ張られて、新しく何かを作るには可変参照が必要だと思い込んでいた

真実:trim()はコピーも変更もしない

trim()の実際の動作:

let s = "  hello  ";
//      012345678  ← メモリ上の位置
let trimmed = s.trim();
//      ^^^^^^  ← インデックス2〜7を指す「窓」

重要な気づき

s[..]s[2,7]をtrimmedに代入して表示しているだけか」

まさにこれが正解でした。trim()は:

  • 新しいデータのコピーを作らない
  • 元のデータを変更しない
  • 「見る範囲」だけを変える(ポインタをずらす)

スライスの内部構造

スライス&strの実体:

struct StrSlice {
    ptr: *const u8,  // どこから
    len: usize,      // 何バイト分
}

実際の動作:

s       = { ptr: 0番地, len: 9 }   // "  hello  " 全体
trimmed = { ptr: 2番地, len: 5 }   // "hello" 部分だけ

実際の文字列データ" hello "はメモリ上に1つだけあって、strimmed同じデータの違う部分を指している窓

正しいコード

fn trim_me(input: &str) -> &str {
    input.trim()
}
  • 元のデータは不変 → &mut不要
  • 新しいスライスも読み取り専用 → &strで十分

4. compose_meで再び躓いた:&mut strpush_strできない

問題コード

fn compose_me(input: &mut str) -> String {
    input.push_str(" world!")  // エラー!
}

エラー内容

error[E0599]: no method named `push_str` found for mutable reference `&mut str`

私の勘違い

&mut strなら変更できるはずだからpush_strが使えるはず」と思っていました。

真実:&mut strは長さを変えられない

重要な違い

  • push_strString型のメソッド
  • &mut strはスライス(固定長)なので、データを追加できない
// ✗ &mut str → 長さ固定、追加不可
// ○ String  → 長さ可変、追加可能

&mut strは「既存の文字の中身を書き換える」ことはできても、「長さを変える(追加する)」ことはできません。

正しいコード

関数のシグネチャを見ると-> Stringを返すようになっているので、新しいStringを作って返すのが正解:

fn compose_me(input: &str) -> String {
    input.to_string() + " world!"
    // または
    // format!("{} world!", input)
}

私の気づき

「関数で返しているからそれは新しいものを返す(作成)形で行えるので&でもいいって感じでしょうか?」

この理解は完璧でした:

  • 引数(input):元のデータを見るだけ → &strで十分
  • 戻り値(String):新しいデータを作って返す → 所有権ごと渡す

5. replace_me:メソッドを知らなかった

問題

fn replace_me(input: &str) -> String {
    // TODO: "cars"という文字列を"balloons"に置換してください。
    if input == "cars"  // ← 間違ったアプローチ
}

私の勘違い

if input == "cars"と書いてしまいました。これは:

  • 文字列全体"cars"かどうかをチェックしている
  • でも求められているのは「文字列の中に含まれる"cars"を置き換える」

正しいコード

fn replace_me(input: &str) -> String {
    input.replace("cars", "balloons")
}

replaceメソッドは文字列の中のどこに"cars"があっても置き換えてくれます。

反省:メソッドを知らないと詰む

Rustの文字列メソッドをある程度知っておく必要があると痛感しました。

よく使うメソッド(覚えるべき)

s.trim()              // 前後の空白削除
s.split(" ")          // 分割
s.replace("a", "b")   // 置換
s.contains("hello")   // 含むか確認
s.starts_with("hi")   // 前方一致
s.to_string()         // String に変換
s.to_uppercase()      // 大文字化
s.len()               // 長さ

参考ドキュメント


6. .into()の警告:無駄な型変換

問題コード

string_slice("nice weather".into());

警告内容

warning: useless conversion to the same type: `&str`

私の疑問

into()って何をしてるんですか?」

解決:into()は型変換メソッド

let s: String = "hello".into();
//              ^^^^^^^^^^^^^^^
//          &str を String に変換

「このデータを、目的の型に変換してください」という意味。

いつ使うのか

型が合わないときに使います:

fn print_string(s: String) {  // String を要求
    println!("{}", s);
}

// これはエラー
print_string("hello");  // &str を渡そうとしている

// これはOK
print_string("hello".into());  // &str → String に変換
// または
print_string("hello".to_string());  // 明示的に変換

警告が出た理由

fn string_slice(arg: &str) { ... }

string_slice("nice weather".into());
//           ^^^^^^^^^^^^^^ すでに &str
//                     ^^^^^ &str → &str に変換(意味がない)

"nice weather"はすでに&strなので、into()で変換する必要がありませんでした。

正しいコード

string_slice("nice weather");  // .into() を削除

7. &strStringの違い:最大の混乱ポイント

私の疑問

「初級な質問を失礼します。&strStringとは?違いは参照だけど所有ということですか?」

私の理解は正しかった

「参照 vs 所有」がまさに違いでした:

&str    // 借りている(参照)
String  // 持っている(所有)

本で例える

&str   → 図書館で本を借りて読んでいる(参照)
String → 本を買って自分で持っている(所有)
  • &strは「見るだけ」なので変更できない
  • Stringは「自分のもの」なので変更できる
let s1: &str = "hello";
// s1.push_str(" world");  // ✗ エラー:借りてるだけなので変更不可

let mut s2: String = String::from("hello");
s2.push_str(" world");  // ○ OK:自分で持ってるので変更可能

メモリの違い

&str
├─ スタックに「ポインタ + 長さ」だけ
└─ 実際のデータは別の場所(プログラムのコード領域など)

String
├─ スタックに「ポインタ + 長さ + 容量」
└─ ヒープに実際のデータを確保

まとめ

&str   = 参照 = 借りてる = 読み取り専用 = 軽い
String = 所有 = 持ってる = 変更可能   = 重い(メモリ確保)

8. to_string()String::fromの混乱

私の疑問1

Claudeへの質問、「確かにStringと言われるとString::fromが連想されるんですが、まだまだ構文が頭に入ってないので混乱しますね」

私の疑問2

Claudeへの質問、「into()では所有権を変えれるということですか?これは流れがわからなくなりバグを引き起こしそうですね」

誤解:into()は「所有権を変える」?

実は違いました。into()は「型を変換する」のであって、「所有権を移動する」わけではありません:

let s1: &str = "hello";
let s2: String = s1.into();
//               ^^^^^^^^^
//            新しい String を作る
//            s1 は &str のまま(変わらない)

into()は:

  • 元の&strから新しいStringを作る
  • 元の&strは変わらず残る
  • 所有権を「移動」するのではなく「新しいデータを生成」

所有権が「移動」するのは別のケース

let s1 = String::from("hello");
let s2 = s1;  // ← ここで所有権が移動
// println!("{}", s1);  // ✗ エラー:s1 はもう使えない

これは型変換ではなく、同じデータの所有権が移るパターンです。

to_string()も型を変えるだけ

Claudeへの質問、「to_string()も型を変えるだけですか?」

はい、その通りです:

let s: &str = "hello";
let owned: String = s.to_string();
//          ^^^^^^
//       新しい String 型のデータを作る

to_string()は:

  • 新しいString型のデータを作る
  • 元の&strからデータをコピーして、所有権を持つStringを生成

9. String::fromとは何か

私の疑問

claudeへの質問、「ちょっと待ってください。Stringって所有権ありでの型定義と思ってました」

この理解は正しかったです。

再び質問、「ではString::fromはなんでしょうか?」

::の意味

String::from("hello")
^^^^^^  ^^^^
型名    関数名

::は「型(やモジュール)に紐づいた関数」を呼び出す記号です。

  • String::from = String型に紐づいたfromという関数
  • String::new = String型に紐づいたnewという関数

String::fromto_string()の違い

実はやってることはほぼ同じです:

// 方法1
let s1 = String::from("hello");

// 方法2
let s2 = "hello".to_string();

// どちらも:&str → String に変換

違いは書き方だけ:

String::from("hello")  // 型側から呼ぶ(関数スタイル)
"hello".to_string()    // 値側から呼ぶ(メソッドスタイル)

::は関数、.はメソッド

String::from("hello")  // 関数:型名::関数名
"hello".to_string()    // メソッド:値.メソッド名
  • ::→ 型や名前空間に属する関数
  • . → 値に対して呼ぶメソッド

10. 型注釈の省略

私の疑問

claudeへの質問、 「let s1: String = String::from("hello");とはならないんですね」

真実:なります!

let s1: String = String::from("hello");  // ○ 明示的
let s1 = String::from("hello");          // ○ 推論に任せる

両方とも正しいです。Rustでは型が明らかなとき、型注釈を省略できます。

いつ型注釈が必要か

型が推論できないときは必須です:

// これはエラー
let s = "hello".into();
//              ^^^^^ 何の型に変換するか分からない

// これはOK
let s: String = "hello".into();
//     ^^^^^^ 型を指定したので into() が String に変換できる

11. 逆変換:Stringから&str

私の気づき

「つまり逆なら?」

let s: &str = String::from("hello").into();
//     ^^^^
//   &str に変換してと指定

理論上は可能ですが、実用的には参照&を使う方が自然:

let owned = String::from("hello");
let borrowed: &str = &owned;  // これが一般的

まとめ:型注釈で変換先を指定できる

// 型注釈で into() の変換先を指定できる
let s1: String = "hello".into();  // &str → String
let s2: &str = owned.into();      // String → &str(あまり使わない)

まとめ:今日学んだこと

1. match構文

  • Enum定義の形とmatchパターンは1対1対応
  • 「定義をコピーして型を変数名に置き換える」だけ

2. &strStringの本質

  • &str = 参照 = 借りてる = 読み取り専用 = 軽い
  • String = 所有 = 持ってる = 変更可能 = 重い

3. スライスは「窓」

  • trim()はデータをコピーしない
  • 「見る範囲」を変えるだけ
  • だから&mutは不要

4. &mut strの限界

  • 既存の文字を書き換えることはできる
  • でも長さを変える(追加する)ことはできない
  • 長さを変えるにはStringが必要

5. 型変換の方法

  • String::from("hello") - 型側から呼ぶ
  • "hello".to_string() - 値側から呼ぶ
  • "hello".into() - 型注釈で変換先を指定

6. 所有権の「移動」と「変換」は別物

  • into()は新しいデータを作る(変換)
  • let s2 = s1は所有権が移る(移動)

7. メソッドを知ることの重要性

  • trim(), replace(), push_str()などの基本メソッドは覚える必要がある
  • 公式ドキュメントは日本語版もある

反省と次のステップ

反省点

  1. 「新しいものを作る」=「&mutが必要」という思い込み

    • Rustでは「見方を変える」だけなら変更じゃない
    • この概念の理解が不足していた
  2. メソッドの知識不足

    • replace()を知らずにifで解決しようとした
    • 基本的なメソッドは暗記する必要がある
  3. 型注釈の役割を理解していなかった

    • into()が型注釈で変換先を決めることを知らなかった
    • 型推論と型注釈の使い分けが曖昧だった

次のステップ

  1. スライスの概念をさらに深める

    • 配列のスライス、ベクタのスライスなど
    • 「窓」の概念を完全に自分のものにする
  2. 文字列メソッドの暗記

    • 公式ドキュメントの頻出メソッドを一通り試す
    • 実際に手を動かして覚える
  3. 所有権・借用の理解を深める

    • 今日の学びは表面的な理解
    • より複雑なケースに対応できるよう練習

おわりに

今日は多くの躓きがありましたが、その一つ一つが重要な気づきにつながりました。特に「スライスは窓」という概念、「&strStringの違いは参照と所有」という理解は、今後のRust学習の土台になると感じています。

躓いたポイントを丁寧に振り返ることで、自分の理解の穴が明確になりました。この記録が、同じように学習している人の役に立てば幸いです。