完全自分用:Rustの基本 - TypeScriptと比較して

に公開

Geminiが書いた


ステップ1: 環境構築とHello, World!

概要:
Rustの開発を始めるための最初のステップです。コンパイラやビルドツールをインストールし、最初のプログラムを作成して実行します。

主要な概念と手順:

  1. Rustのインストール (rustup):

    • Rustの公式インストーラー兼バージョン管理ツールである rustup を使います。これにより、Rustコンパイラ (rustc)、標準ライブラリ、ビルドツール兼パッケージマネージャー (cargo) などがインストールされます。
    • 公式サイト (https://www.rust-lang.org/tools/install) の指示に従ってインストールします。通常はターミナルで以下のコマンドを実行します(OSによって異なる場合があります)。
      curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
      
    • インストール後、ターミナルを再起動するか、指示に従ってPATHを通します。
    • 確認:
      rustc --version
      cargo --version
      rustup --version
      
  2. プロジェクトの作成 (cargo new):

    • cargo はプロジェクトの作成、ビルド、テスト、依存関係管理などを行う非常に強力なツールです。
    • 新しいプロジェクトを作成するには、ターミナルで以下を実行します。
      cargo new hello_rust
      cd hello_rust
      
    • これにより、hello_rust というディレクトリが作成され、以下の構造が生成されます。
      hello_rust/
      ├── Cargo.toml  # プロジェクトの設定ファイル (メタデータ、依存関係など)
      └── src/        # ソースコードディレクトリ
          └── main.rs # メインのソースファイル
      
  3. Cargo.toml ファイル:

    • プロジェクトの設定ファイルです。TOML (Tom's Obvious, Minimal Language) 形式で書かれます。
    • 初期状態では、プロジェクト名、バージョン、エディション、作者などの基本情報と、依存関係を記述する [dependencies] セクションが含まれます。
    • TypeScriptにおける package.json に相当します。
  4. src/main.rs ファイル:

    • プログラムのエントリーポイントとなるファイルです。
    • 初期状態では、"Hello, world!" を出力する簡単なコードが書かれています。
      fn main() {
          println!("Hello, world!");
      }
      
    • fn main(): プログラム実行時に最初に呼び出される関数です。
    • println!: 文字列をコンソールに出力するマクロです。関数ではなくマクロであるため、末尾に ! が付きます。マクロはコンパイル時により高度なコード生成を行います。
  5. プロジェクトのビルド (cargo build):

    • ソースコードをコンパイルして実行可能ファイルを生成します。
    • プロジェクトのルートディレクトリ (hello_rust/) で以下を実行します。
      cargo build
      
    • コンパイルが成功すると、target/debug/ ディレクトリ内に実行可能ファイル (hello_rust または hello_rust.exe) が生成されます。
    • --release フラグを付けると、最適化されたリリースビルドが target/release/ に生成されます (cargo build --release)。
  6. プロジェクトの実行 (cargo run):

    • コンパイルと実行を一度に行います。
      cargo run
      
    • 内部的には cargo build を実行し、成功すれば生成された実行可能ファイルを実行します。コンソールに Hello, world! と表示されるはずです。
  7. 高速なチェック (cargo check):

    • 実行可能ファイルを生成せずに、コードのコンパイルエラーチェックだけを高速に行います。開発中に頻繁に使うと便利です。
      cargo check
      

TypeScriptとの比較:

  • パッケージマネージャー/ビルドツール:
    • TypeScript: npmyarn で依存関係管理、tsc でコンパイル、node で実行、と複数のツールを組み合わせることが多い。
    • Rust: cargo がプロジェクト管理、ビルド、テスト、依存関係管理、ドキュメント生成などを一手に担う統合ツール。
  • 設定ファイル:
    • TypeScript: package.json (依存関係、スクリプト), tsconfig.json (コンパイラ設定)。
    • Rust: Cargo.toml (プロジェクトメタデータ、依存関係、プロファイル設定など)。
  • エントリーポイント:
    • TypeScript (Node.js): package.jsonmain フィールドで指定されたファイルや、実行時に指定したファイル。
    • Rust: src/main.rsmain 関数 (バイナリの場合)。ライブラリの場合は src/lib.rs が基本。

ステップ2: 基本的な構文

概要:
変数、データ型、関数など、Rustプログラムを構成する基本的な要素を学びます。

主要な概念:

  1. 変数束縛 (let, mut):

    • Rustでは let キーワードを使って変数を作成します。これは「束縛 (binding)」と呼ばれます。
    • デフォルトで不変 (Immutable): let で束縛された変数は、一度値を設定すると再代入できません。
      let x = 5;
      // x = 6; // これはコンパイルエラー!
      println!("The value of x is: {}", x); // 出力: The value of x is: 5
      
    • 可変性 (mut): 変数の値を変更可能にするには mut キーワードを使います。
      let mut y = 5;
      println!("The value of y is: {}", y); // 出力: The value of y is: 5
      y = 6;
      println!("The value of y is: {}", y); // 出力: The value of y is: 6
      
    • シャドーイング (Shadowing): 同じスコープ内で let を使って同じ名前の変数を再宣言できます。これは新しい変数を作成するもので、型を変えることも可能です。元の変数は「隠され (shadowed)」ます。
      let z = 5;
      let z = z + 1; // シャドーイング (zは6になる)
      {
          let z = z * 2; // 内側のスコープでさらにシャドーイング (zは12になる)
          println!("The value of z in the inner scope is: {}", z); // 出力: 12
      }
      println!("The value of z is: {}", z); // 出力: 6 (内側のスコープを抜けると元に戻る)
      
      let spaces = "   ";
      let spaces = spaces.len(); // 型を変えることも可能 (String -> usize)
      println!("Spaces count: {}", spaces); // 出力: 3
      
  2. データ型 (Data Types):

    • Rustは静的型付け言語であり、コンパイル時にすべての変数の型がわかっている必要があります。
    • 型推論があるため、多くの場合、型を明示的に書く必要はありません。
    • スカラー型 (Scalar Types): 単一の値を表現します。
      • 整数 (Integer): 符号付き (i8, i16, i32, i64, i128, isize) と 符号なし (u8, u16, u32, u64, u128, usize) があります。数字はビット数を表します。isize/usize はコンピュータのアーキテクチャに依存します(32bit/64bit)。デフォルトは i32
        let age: u8 = 30; // 型を明示
        let count = -10; // 型推論 (i32)
        
      • 浮動小数点数 (Floating-Point): f32 (単精度), f64 (倍精度)。デフォルトは f64
        let pi = 3.14; // f64
        let temperature: f32 = 36.5; // f32
        
      • 論理値 (Boolean): bool 型。true または false
        let is_rust_fun = true;
        let is_learning_done: bool = false;
        
      • 文字 (Character): char 型。シングルクォート (') で囲みます。Unicodeスカラー値を表現するため、ASCIIだけでなく絵文字なども含みます(4バイト)。
        let c = 'z';
        let heart_eyed_cat = '😻';
        
    • 複合型 (Compound Types): 他の型の値をグループ化します。
      • タプル (Tuple): 固定長の、異なる型の値の集まり。カンマ区切りで () で囲みます。
        let tup: (i32, f64, u8) = (500, 6.4, 1);
        let (x, y, z) = tup; // 分解 (destructuring)
        println!("The value of y is: {}", y); // 出力: 6.4
        println!("The first value is: {}", tup.0); // インデックスでアクセス
        
      • 配列 (Array): 固定長の、同じ型の値の集まり。[] で囲みます。ヒープではなくスタックに確保されることが多いです。
        let a = [1, 2, 3, 4, 5]; // 型は [i32; 5] (要素の型; 要素数)
        let months = ["January", "February", /* ... */]; // [&str; 12]
        
        let first = a[0];
        // let element = a[10]; // コンパイルは通るが、実行時にパニック (境界外アクセス)
        
  3. 関数 (Functions):

    • fn キーワードで定義します。
    • 引数と戻り値には型アノテーションが必須です。
    • 関数名はスネークケース (snake_case) が慣習です。
      fn another_function(x: i32, y: f64) { // 引数には型指定が必要
          println!("The value of x is: {}", x);
          println!("The value of y is: {}", y);
      }
      
      fn five() -> i32 { // 戻り値の型は `->` の後に指定
          5 // 最後に評価された式が暗黙の戻り値になる (セミコロンなし)
      }
      
      fn plus_one(x: i32) -> i32 {
          x + 1 // セミコロンを付けると文になり、() (ユニット型) を返すことになるので注意
          // return x + 1; // `return` キーワードで明示的に返すことも可能
      }
      
      fn main() {
          another_function(5, 6.0);
          let f = five();
          println!("Five: {}", f); // 出力: 5
          let six = plus_one(5);
          println!("Six: {}", six); // 出力: 6
      }
      
  4. コメント (Comments):

    • 一行コメント: //
    • 複数行コメント: /* ... */
    • ドキュメンテーションコメント: /// (後続のアイテム用), //! (囲んでいるアイテム用)。Markdown形式で記述し、cargo doc でドキュメントを生成できます。

TypeScriptとの比較:

  • 変数宣言:
    • TypeScript: const (再代入不可), let (再代入可)。
    • Rust: let (デフォルト不変), let mut (可変)。Rustのデフォルト不変性は安全性を重視する思想の表れ。シャドーイングはTypeScriptにはない概念(スコープが異なれば可能だが、Rustのように同じスコープで意図的に行うことは少ない)。
  • データ型:
    • TypeScript: number, string, boolean, null, undefined, symbol, bigint, object, array, tuple, enum など。
    • Rust: より厳密な数値型(サイズ、符号有無)、char (Unicode)、固定長配列。null/undefined はなく、Option<T> (ステップ8) で代替。文字列は String&str (ステップ4, 6) を区別。可変長配列は Vec<T> (後述)。
  • 型アノテーション:
    • TypeScript: variable: type。型推論あり。
    • Rust: variable: type。型推論あり。関数のシグネチャ(引数、戻り値)では必須。
  • 関数:
    • TypeScript: function name(...) {}, const name = (...) => {} など。型アノテーションは任意(noImplicitAny で必須にできる)。
    • Rust: fn name(...) -> ReturnType {}。型アノテーション必須。暗黙のreturnが特徴的。
  • アクセス:
    • TypeScript: 配列/タプルは arr[index]。オブジェクトは obj.prop または obj['prop']
    • Rust: 配列は arr[index]。タプルは tuple.index。構造体は struct.field

ステップ3: 制御フロー

概要:
条件分岐 (if/else) や繰り返し処理 (loop, while, for) など、プログラムの流れを制御する方法を学びます。

主要な概念:

  1. if/else 式:

    • Rustの if式 (expression) であり、文 (statement) ではありません。つまり、値を返すことができます。
    • 条件式は必ず bool 型でなければなりません。他の言語のような "truthy" / "falsy" の概念はありません。
      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 if number % 2 == 0 {
          println!("number is divisible by 2");
      } else {
          println!("number is not divisible by 4, 3, or 2");
      }
      
      // `if` は式なので、`let` 文の右辺で使える
      let condition = true;
      let value = if condition { 5 } else { 6 }; // value は 5 になる
      println!("The value is: {}", value);
      
      // 型が一致しないとコンパイルエラー
      // let mismatch = if condition { 5 } else { "six" }; // エラー!
      
  2. ループ (loop, while, for):

    • loop: 無限ループを作成します。break キーワードでループを抜けます。break は値を返すこともできます。continue で現在のイテレーションをスキップし、次のイテレーションを開始します。
      let mut counter = 0;
      let result = loop {
          counter += 1;
          if counter == 10 {
              break counter * 2; // ループを抜け、値を返す
          }
      };
      println!("The result is {}", result); // 出力: The result is 20
      
    • while: 条件が true の間、ループを繰り返します。
      let mut number = 3;
      while number != 0 {
          println!("{}!", number);
          number -= 1;
      }
      println!("LIFTOFF!!!");
      
    • for: コレクションの各要素に対してループを実行します。Rustではイテレータ (iterator) を使うのが最も一般的で安全な方法です。
      let a = [10, 20, 30, 40, 50];
      
      // 各要素に対してループ (安全で効率的)
      for element in a.iter() { // `iter()` はコレクションへの参照のイテレータを返す
          println!("the value is: {}", element);
      }
      
      // C言語風のループは推奨されない (が、Rangeを使えば可能)
      // `1..4` は 1, 2, 3 を含む範囲 (Range)
      for number in 1..4 { // 4は含まない
          println!("{}!", number);
      }
      // `1..=4` は 1, 2, 3, 4 を含む範囲
      for number in (1..=4).rev() { // `.rev()` で逆順にできる
          println!("{}!", number);
      }
      

TypeScriptとの比較:

  • if/else:
    • TypeScript: if/else は文。値を返すには三項演算子 (condition ? val1 : val2) を使う。条件式は "truthy"/"falsy" が許容される。
    • Rust: if/else は式。値を返せる。条件式は厳密に bool 型のみ。
  • ループ:
    • TypeScript: while, do...while, for, for...in, for...of
    • Rust: loop, while, for (イテレータベース)。
      • loopwhile(true) に似ているが、break value で値を返せる点がユニーク。
      • while はほぼ同じ。
      • Rustの for は TypeScriptの for...of に最も近い。インデックスを使った従来の for (let i=0; ...) スタイルのループは、Rustでは for i in 0..len {} のようにRangeと組み合わせて書くのが一般的だが、直接イテレータを使う方がよりRustらしいとされることが多い。

ステップ4: Rustの核心:所有権 (Ownership)

概要:
Rustの最もユニークで中心的な機能です。ガベージコレクタ (GC) を使わずにメモリ安全性を保証するための仕組みです。理解するには時間がかかるかもしれませんが、Rustを使いこなす上で不可欠です。

主要な概念:

  1. スタック (Stack) と ヒープ (Heap):

    • スタック: 関数呼び出しやローカル変数が格納されるメモリ領域。後入れ先出し (LIFO)。サイズが固定(コンパイル時にわかる)で高速にアクセスできるデータ(整数、浮動小数点数、ブール値、文字、タプルや固定長配列(要素がスタックに置ける場合))が置かれます。
    • ヒープ: プログラム実行時に動的に確保されるメモリ領域。サイズが可変またはコンパイル時に不明なデータ(String, Vec<T> など)が置かれます。スタックよりアクセスが低速。ヒープへのアクセスは、スタックに置かれたポインタ(メモリアドレス)を経由します。
  2. 所有権のルール (Rules of Ownership):
    Rustコンパイラは以下の3つのルールをコンパイル時に強制します。

    1. 各値は「所有者 (owner)」と呼ばれる変数をただ1つ持つ。
    2. 同時に所有者は1人だけ。
    3. 所有者がスコープ (scope) を抜けたら、その値は破棄 (drop) される。 (メモリが自動的に解放される)
  3. 変数スコープ:
    他の多くの言語と同様に、変数が有効な範囲(スコープ)があります。

    {                      // s はここでは有効ではない
        let s = "hello";   // s はここから有効になる
        // s を使った処理
    }                      // このスコープは終わり。s はもう有効ではない
    
  4. ムーブ (Move):

    • ヒープにデータを持つ型(例: String)の場合、変数間で代入が行われると、所有権が「移動 (move)」します。これは浅いコピー (shallow copy) に似ていますが、元の変数は無効化されます。これにより、「ダブルフリー」(同じメモリ領域を2回解放しようとする)エラーを防ぎます。
      let s1 = String::from("hello"); // s1 が "hello" の所有者
      let s2 = s1; // s1 の所有権が s2 に「ムーブ」する。
                   // s1 はこれ以降、無効な変数となる。
      
      // println!("{}, world!", s1); // エラー! s1はムーブされた後なので使えない
      println!("{}, world!", s2); // OK. s2が所有者。出力: hello, world!
      
    • Drop トレイト: 型がスコープを抜けるときに実行される特別な処理(デストラクタのようなもの)を定義します。String はスコープを抜けるときにヒープメモリを解放する処理を持っています。
  5. コピー (Copy):

    • スタックのみにデータを持つ型(例: i32, bool, f64, char, これらの型のみを含むタプルなど)は、Copy トレイトを実装しています。
    • Copy トレイトを実装する型の場合、代入時に値が単純にビット単位でコピーされ、新しい変数が作成されます。所有権の移動は起こらず、元の変数も引き続き有効です。
      let x = 5; // i32 は Copy トレイトを持つ
      let y = x; // x の値が y にコピーされる
      
      println!("x = {}, y = {}", x, y); // 両方有効。出力: x = 5, y = 5
      
    • ある型が Drop トレイトを実装している場合、Copy トレイトを実装することはできません(リソース解放の責任が曖昧になるため)。
  6. クローン (Clone):

    • ヒープデータを含む型の値を意図的に深くコピー(ディープコピー)したい場合は、clone() メソッドを使います。これにより、ヒープデータも複製され、新しい所有者が生まれます。clone() はコストがかかる可能性があります。
      let s1 = String::from("hello");
      let s2 = s1.clone(); // s1のヒープデータも複製される
      
      println!("s1 = {}, s2 = {}", s1, s2); // 両方有効。出力: s1 = hello, s2 = hello
      
  7. 所有権と関数:

    • 値を関数に渡すと、ムーブまたはコピーが発生します(その値の型によります)。
    • 関数から値を返すと、所有権が呼び出し元に移動します。
      fn main() {
          let s = String::from("hello"); // s が所有者
          takes_ownership(s); // s の所有権が関数にムーブされる
          // println!("{}", s); // エラー! s はムーブされた
      
          let x = 5; // x は i32 (Copy)
          makes_copy(x); // x の値が関数にコピーされる
          println!("{}", x); // OK. x はまだ有効
      }
      
      fn takes_ownership(some_string: String) { // some_string が所有権を得る
          println!("{}", some_string);
      } // ここで some_string はスコープを抜け、`drop` が呼ばれる
      
      fn makes_copy(some_integer: i32) { // some_integer は値のコピー
          println!("{}", some_integer);
      } // ここで some_integer はスコープを抜けるが、何も特別なことは起きない
      

TypeScriptとの比較:

  • メモリ管理:
    • TypeScript (JavaScript): ガベージコレクション (GC) が不要になったオブジェクトを自動的に検出し、メモリを解放します。開発者はメモリ管理をほとんど意識しません。
    • Rust: 所有権システムにより、コンパイル時にメモリ管理ルールを強制します。GCのランタイムオーバーヘッドがなく、より予測可能なパフォーマンスが得られますが、学習コストが高くなります。
  • 値の受け渡し:
    • TypeScript: プリミティブ型(number, string, boolean など)は値渡しのように振る舞います。オブジェクトや配列は参照渡しのように振る舞います(実際には参照の値渡し)。
    • Rust: 型が Copy を実装していればコピー、そうでなければムーブ(所有権の移動)が基本です。これにより、意図しないデータの共有や変更を防ぎます。

所有権はRustの安全性の根幹です。最初は難しく感じるかもしれませんが、この仕組みのおかげで、データ競合のない並行処理や、メモリリーク・ダングリングポインタの心配が少ないコードを書くことができます。


ステップ5: 参照 (References) と 借用 (Borrowing)

概要:
所有権を移動させずに、値にアクセスする方法を学びます。これにより、データを効率的に共有しつつ、所有権ルールによる安全性を維持できます。

主要な概念:

  1. 参照 (References):

    • 参照は、ある値へのアクセスを提供するポインタのようなものですが、所有権は持ちません。
    • & 記号を使って参照を作成します。参照が指す値を使うことを「参照外し (dereferencing)」といい、* 演算子を使いますが、多くの場合(メソッド呼び出しなど)自動的に行われます。
      let s1 = String::from("hello");
      let len = calculate_length(&s1); // s1 の参照 (&s1) を渡す (所有権は移動しない)
      println!("The length of '{}' is {}.", s1, len); // s1 はまだ有効
      
      fn calculate_length(s: &String) -> usize { // s は String への参照
          s.len() // 参照はスコープを抜けても、指している値は drop されない
      }
      
    • 関数に値を渡すために参照を使うことを借用 (borrowing) と呼びます。
  2. 不変参照 (Immutable References):

    • &T の形式で、値への不変な参照を作成します。
    • 不変参照を通して値を変更することはできません。
    • 同じ値への不変参照は、同時に複数存在できます。
      let s = String::from("hello");
      let r1 = &s;
      let r2 = &s;
      println!("{} and {}", r1, r2); // OK
      // let r3 = &mut s; // エラー! 不変参照がある間は可変参照を作れない
      
  3. 可変参照 (Mutable References):

    • &mut T の形式で、値への可変な参照を作成します。
    • 可変参照を通して値を変更できます。
    • 重要な制約: 特定のスコープ内で、ある値への可変参照は、同時に1つしか存在できません。
    • また、可変参照が存在する間は、その値への不変参照も存在できません。
      let mut s = String::from("hello"); // 可変にするために `mut` が必要
      
      let r1 = &mut s;
      // let r2 = &mut s; // エラー! 2つ目の可変参照は作れない
      // let r3 = &s; // エラー! 可変参照がある間は不変参照も作れない
      r1.push_str(", world!"); // 可変参照を通して変更
      println!("{}", r1); // 出力: hello, world!
      
      // r1のスコープがここで終われば、新しい参照を作成できる
      let r2 = &mut s;
      println!("{}", r2);
      
    • この制約により、コンパイル時にデータ競合 (data races) を防ぎます。データ競合は、複数のポインタが同じデータにアクセスし、少なくとも1つが書き込みを行い、同期機構がない場合に発生します。
  4. 借用ルール (Borrowing Rules):
    コンパイラは以下のルールを強制します。

    1. 任意の時点で、あなたはどちらか一方を持つことができます。
      • 1つの 可変参照 (&mut T)。
      • 任意の数の 不変参照 (&T)。
    2. 参照は常に有効でなければならない。
  5. ダングリング参照 (Dangling References) の防止:

    • Rustコンパイラは、参照が指すデータよりも長く参照が生存しないことを保証します。これにより、無効なメモリを指すポインタ(ダングリングポインタ/参照)を防ぎます。
      // fn dangle() -> &String { // エラー! 戻り値の参照が、関数内で破棄されるデータを指してしまう
      //     let s = String::from("hello");
      //     &s // s への参照を返そうとする
      // } // s はここでスコープを抜け drop されるが、参照は残ろうとする
      
      fn no_dangle() -> String { // 所有権ごと返すのが正しい
          let s = String::from("hello");
          s
      }
      
    • このチェックはライフタイム (Lifetimes) という機能によって行われますが、多くの場合コンパイラが推論してくれるため、明示的に書く必要はありません(複雑なケースでは必要になります)。

TypeScriptとの比較:

  • 参照の概念: TypeScriptのオブジェクト/配列は参照渡しのように振る舞いますが、Rustのような厳密な借用ルール(可変参照は1つだけ、不変参照と共存不可)はありません。
  • 可変性: TypeScriptでは const で再代入を防げますが、オブジェクトの内容の変更は防げません (readonly 修飾子や Readonly<T> 型である程度制御可能)。Rustは mut と借用ルールで可変性をより厳密に制御します。
  • 安全性: Rustの借用チェッカーは、コンパイル時にデータ競合やダングリング参照といった厄介なバグを防ぎます。TypeScript (JavaScript) では、これらの問題は実行時エラーや予期せぬ動作として現れる可能性があります。

借用は、所有権を維持しつつ効率的にデータを扱うための重要なメカニズムです。特に可変参照の制約は、Rustの安全性を支える柱の一つです。


ステップ6: スライス (Slices)

概要:
コレクション(配列、String, Vec<T> など)全体ではなく、その一部の連続したシーケンスへの参照を持つ方法を学びます。所有権は持ちません。

主要な概念:

  1. スライスとは:

    • コレクション内の要素の連続したシーケンスへのビュー(参照)です。
    • 所有権を持たないので、サイズはコンパイル時にはわかりません(動的サイズ型, DST)。そのため、常に & を付けて参照(&[T]&str)の形で使われます。
    • 開始位置と長さの情報を持っています。
  2. 文字列スライス (&str):

    • String の一部、または全体への参照です。
    • 文字列リテラル ("hello") の型は、実は &'static str です。これはプログラムのバイナリに直接埋め込まれ、プログラム実行中ずっと有効な文字列スライスです。
      let s = String::from("hello world");
      
      // [start..end] の形式でスライスを作成 (startは含む, endは含まない)
      let hello = &s[0..5]; // &s[..5] とも書ける
      let world = &s[6..11]; // &s[6..] とも書ける
      let whole = &s[..]; // 文字列全体のスライス
      
      println!("hello: {}", hello); // 出力: hello
      println!("world: {}", world); // 出力: world
      println!("whole: {}", whole); // 出力: hello world
      
      // 文字列リテラルは既にスライス
      let literal = "hello literal"; // 型は &'static str
      
    • 注意: Rustの String はUTF-8エンコーディングです。スライスはバイト境界で行う必要があり、文字境界の途中でスライスしようとすると実行時パニックを引き起こします。
      let s = String::from("नमस्ते"); // UTF-8 文字列 (各文字が複数バイト)
      // let slice = &s[0..1]; // パニック! バイト境界だが文字境界ではない
      
  3. 配列スライス (&[T]):

    • 配列や Vec<T> (後述する可変長配列) の一部または全体への参照です。
      let a = [1, 2, 3, 4, 5]; // 配列 [i32; 5]
      
      let slice: &[i32] = &a[1..3]; // インデックス1, 2の要素へのスライス ([2, 3])
      assert_eq!(slice, &[2, 3]);
      
      let whole_slice = &a[..]; // 配列全体のスライス
      println!("Whole slice length: {}", whole_slice.len()); // 出力: 5
      
  4. スライスの利点:

    • 所有権を移動せずにコレクションの一部を扱えるため、効率的です。
    • 関数が具体的なコレクション型 (String, Vec<T>) ではなく、スライス (&str, &[T]) を受け取るように設計すると、より汎用性が高くなります。例えば、&str を受け取る関数は String と文字列リテラルの両方を扱えます。
      // String を受け取る関数 (効率が悪い場合がある)
      fn first_word_string(s: String) -> usize { /* ... */ }
      
      // 文字列スライスを受け取る関数 (より汎用的で効率的)
      fn first_word_slice(s: &str) -> &str {
          let bytes = s.as_bytes();
          for (i, &item) in bytes.iter().enumerate() {
              if item == b' ' {
                  return &s[0..i]; // 単語のスライスを返す
              }
          }
          &s[..] // スペースがなければ文字列全体のスライスを返す
      }
      
      fn main() {
          let my_string = String::from("hello world");
          let word = first_word_slice(&my_string[..]); // Stringから作ったスライスを渡す
          println!("First word: {}", word);
      
          let my_literal = "hello literal";
          let word = first_word_slice(my_literal); // 文字列リテラル (既に&str) を渡す
          println!("First word: {}", word);
      }
      

TypeScriptとの比較:

  • 配列のスライス: TypeScriptの Array.prototype.slice(start, end) は、元の配列の一部を含む新しい配列を作成します。メモリコピーが発生します。Rustのスライス (&[T]) は元のデータへの**参照(ビュー)**であり、コピーは発生しません(より軽量)。
  • 文字列のスライス: TypeScriptの String.prototype.slice(start, end)substring(start, end)新しい文字列を作成します。Rustの文字列スライス (&str) は元の String や文字列リテラルへの**参照(ビュー)**です。

スライスは、Rustにおける効率的で安全なデータアクセス方法の重要な一部です。特に &str は頻繁に使われます。


ステップ7: 構造体 (Structs)

概要:
関連するデータをまとめて、意味のある独自の型を作成する方法を学びます。TypeScriptの interfaceclass のデータ部分に似ています。

主要な概念:

  1. 構造体の定義 (struct):

    • struct キーワードを使って定義します。フィールド(データメンバー)とその型を指定します。
    • 構造体名はキャメルケース (CamelCase) が慣習です。
      struct User {
          active: bool,
          username: String, // String (所有権を持つ)
          email: String,
          sign_in_count: u64,
      }
      
  2. 構造体のインスタンス化:

    • StructName { field1: value1, field2: value2, ... } のようにしてインスタンスを作成します。フィールドの順序は定義時と同じである必要はありません。
      let mut user1 = User {
          email: String::from("someone@example.com"),
          username: String::from("someusername123"),
          active: true,
          sign_in_count: 1,
      };
      // フィールドへのアクセスと変更 (インスタンスが `mut` である必要あり)
      user1.email = String::from("anotheremail@example.com");
      println!("Username: {}", user1.username);
      
  3. フィールド初期化省略記法:

    • インスタンス化する際に、変数名とフィールド名が同じ場合、field: variable の代わりに variable とだけ書けます。
      fn build_user(email: String, username: String) -> User {
          User {
              email, // email: email と同じ
              username, // username: username と同じ
              active: true,
              sign_in_count: 1,
          }
      }
      let user2 = build_user(String::from("user2@example.com"), String::from("user2"));
      
  4. 構造体更新記法 (..):

    • 既存のインスタンスの値を一部使って新しいインスタンスを作成する場合、.. 構文が便利です。指定しなかったフィールドは、指定した他のインスタンスから値がコピー(またはムーブ)されます。
      let user3 = User {
          email: String::from("user3@example.com"),
          ..user2 // user2の残りのフィールド (username, active, sign_in_count) を使う
      };
      // 注意: user2 の username (String型) は user3 にムーブされるため、
      // この後 user2.username は使えなくなる。active, sign_in_count は Copy なので問題ない。
      // println!("{}", user2.username); // エラー!
      
  5. タプル構造体 (Tuple Structs):

    • フィールド名を持たず、タプルのように型だけを持つ構造体です。タプルと似ていますが、異なる型として区別したい場合に有用です。
      struct Color(i32, i32, i32); // RGB
      struct Point(i32, i32, i32); // XYZ
      
      let black = Color(0, 0, 0);
      let origin = Point(0, 0, 0);
      
      println!("First color value: {}", black.0); // インデックスでアクセス
      // let mixed = black + origin; // エラー! 型が違うため演算できない
      
  6. ユニット様構造体 (Unit-Like Structs):

    • フィールドを全く持たない構造体です。特定のトレイト(後述するインターフェースのようなもの)を実装する必要があるが、状態を持つ必要がない場合などに使われます。
      struct AlwaysEqual; // フィールドなし
      
      let subject = AlwaysEqual;
      // 用途の例: ジェネリクスやトレイトでマーカーとして使う
      
  7. メソッド (impl):

    • 構造体に関連付けられた関数をメソッド (methods) と呼びます。メソッドは impl (implementation) ブロック内に定義します。
    • 最初の引数は常に self で、これは構造体のインスタンス自身を表します。self には3つの形式があります。
      • &self: インスタンスへの不変参照を借用します(読み取り専用)。最も一般的。
      • &mut self: インスタンスへの可変参照を借用します(読み書き可能)。
      • self: インスタンスの所有権を完全に取得します(インスタンスを消費する)。あまり使われません。
      struct Rectangle {
          width: u32,
          height: u32,
      }
      
      // Rectangle 構造体のための impl ブロック
      impl Rectangle {
          // &self を取るメソッド (インスタンスを不変借用)
          fn area(&self) -> u32 {
              self.width * self.height
          }
      
          // 別の Rectangle を不変借用するメソッド
          fn can_hold(&self, other: &Rectangle) -> bool {
              self.width > other.width && self.height > other.height
          }
      
          // &mut self を取るメソッド (インスタンスを可変借用)
          fn set_width(&mut self, width: u32) {
              self.width = width;
          }
      
          // self を取るメソッド (インスタンスの所有権を奪う)
          // このメソッド呼び出し後、元のインスタンスは使えなくなる
          fn destroy(self) {
              println!("Destroying rectangle with width {}", self.width);
          }
      }
      
      fn main() {
          let mut rect1 = Rectangle { width: 30, height: 50 };
          let rect2 = Rectangle { width: 10, height: 40 };
          let rect3 = Rectangle { width: 60, height: 45 };
      
          println!("The area of the rectangle is {} square pixels.", rect1.area()); // メソッド呼び出し
          println!("Can rect1 hold rect2? {}", rect1.can_hold(&rect2)); // true
          println!("Can rect1 hold rect3? {}", rect1.can_hold(&rect3)); // false
      
          rect1.set_width(35); // 可変メソッド呼び出し
          println!("New width: {}", rect1.width); // 出力: 35
      
          // rect1.destroy(); // これを呼ぶと rect1 はムーブされる
          // println!("{}", rect1.width); // エラー!
      }
      
  8. 関連関数 (Associated Functions):

    • impl ブロック内で定義される、self を第一引数に取らない関数です。これはメソッドではなく関連関数と呼ばれます。
    • インスタンスではなく、型自身に関連付けられます。多くの場合、コンストラクタ(新しいインスタンスを生成する関数)として使われます。
    • :: (ダブルコロン) 構文を使って呼び出します。
      impl Rectangle {
          // 関連関数 (コンストラクタとしてよく使われる)
          fn square(size: u32) -> Self { // Self は impl している型 (Rectangle) を指す
              Self { width: size, height: size }
          }
      }
      
      fn main() {
          let sq = Rectangle::square(3); // 関連関数の呼び出し
          println!("Square area: {}", sq.area()); // 出力: 9
      }
      

TypeScriptとの比較:

  • データ構造定義:
    • TypeScript: interface (主に形状定義), class (データ+振る舞い)。
    • Rust: struct (データ構造)。振る舞いは impl ブロックで分離して定義。
  • メソッド:
    • TypeScript: class 内にメソッドを定義。this キーワードがインスタンスを指す。this の扱いはやや複雑な場合がある。
    • Rust: impl ブロック内にメソッドを定義。第一引数 (self, &self, &mut self) でインスタンスへのアクセス方法を明示的に示す。これにより、メソッドがインスタンスの状態をどう扱うかが明確になる。
  • 静的メソッド/コンストラクタ:
    • TypeScript: static キーワードで静的メソッド、constructor キーワードでコンストラクタ。
    • Rust: self を取らない関連関数が静的メソッドに相当。コンストラクタは、慣習的に new や他の名前(例: square)を持つ関連関数として実装される。:: でアクセス。
  • アクセス修飾子:
    • TypeScript: public, private, protected
    • Rust: デフォルトでプライベート。pub キーワードで公開(モジュールシステムと関連、後述)。

構造体と impl は、Rustで独自の型とその振る舞いを定義するための基本的なツールです。


ステップ8: 列挙型 (Enums) と パターンマッチ (match)

概要:
ある値が、複数の可能性のある「バリアント(変種)」のうちの1つであることを表現する型、列挙型 (Enum) を学びます。そして、Enumなどの値に対して、構造に基づいた強力な分岐処理を行うパターンマッチ (match) を学びます。RustのEnumはTypeScriptのEnumよりもはるかに強力です。

主要な概念:

  1. 列挙型 (Enum) の定義:

    • enum キーワードを使って定義します。複数の可能な値(バリアント)をリストします。
    • 各バリアントは、関連するデータを持つことができます(構造体やタプルのように)。これにより、単なる識別子以上の表現力豊かな型を作成できます(これは「代数的データ型」や「Tagged Union」と呼ばれる概念です)。
      // シンプルなEnum (C言語風)
      enum IpAddrKind {
          V4,
          V6,
      }
      
      // データを持つEnum
      enum IpAddr {
          V4(u8, u8, u8, u8), // V4バリアントは4つのu8値を持つ
          V6(String),        // V6バリアントは1つのString値を持つ
      }
      
      enum Message {
          Quit, // データなし
          Move { x: i32, y: i32 }, // 匿名構造体のようなデータ
          Write(String), // タプルのようなデータ (1要素)
          ChangeColor(i32, i32, i32), // タプルのようなデータ (3要素)
      }
      
      // Enumのインスタンス化
      let four = IpAddrKind::V4;
      let six = IpAddrKind::V6;
      
      let home = IpAddr::V4(127, 0, 0, 1);
      let loopback = IpAddr::V6(String::from("::1"));
      
      let m = Message::Write(String::from("hello"));
      
    • Enumにも impl ブロックを使ってメソッドを定義できます。
      impl Message {
          fn call(&self) {
              // メソッド本体は後でmatchを使って実装することが多い
              println!("Calling message...");
          }
      }
      m.call();
      
  2. match 式:

    • ある値が、一連のパターン (pattern) のどれに一致するかを調べ、一致したパターに対応するコードを実行する制御フロー演算子です。
    • 非常に強力で、if/else チェーンよりも複雑な条件分岐を簡潔かつ安全に記述できます。
    • 網羅的 (Exhaustive): match 式は、対象となる値のすべての可能性をパターンで網羅する必要があります。これにより、考慮漏れによるバグを防ぎます。コンパイラがチェックしてくれます。
    • _ (アンダースコア) はワイルドカードパターンで、どの値にもマッチしますが、値は束縛されません。「その他すべて」の場合に使います。
      fn value_in_cents(coin: Coin) -> u8 {
          match coin {
              Coin::Penny => { // `{}` を使って複数行のコードを書ける
                  println!("Lucky penny!");
                  1
              }
              Coin::Nickel => 5, // 単一の式なら `{}` は不要
              Coin::Dime => 10,
              Coin::Quarter => 25,
              // ここで全てのCoinバリアントを網羅しないとコンパイルエラー
          }
      }
      
      enum Coin { Penny, Nickel, Dime, Quarter }
      
      // Enumのデータを取り出す
      let msg = Message::ChangeColor(0, 160, 255);
      match msg {
          Message::Quit => println!("Quit"),
          Message::Move { x, y } => { // 構造体のようにデータを取り出す
              println!("Move to x: {}, y: {}", x, y);
          }
          Message::Write(text) => { // タプルのようにデータを取り出す
              println!("Text message: {}", text);
          }
          Message::ChangeColor(r, g, b) => { // 複数の値を取り出す
              println!("Change color to red {}, green {}, blue {}", r, g, b);
          }
      }
      
  3. Option<T> Enum:

    • Rustには他の多くの言語にある nullundefined がありません。代わりに、値が存在しない可能性を表現するために、標準ライブラリで定義された Option<T> Enum を使います。
    • Option<T> の定義:
      enum Option<T> {
          None,    // 値が存在しないことを示す
          Some(T), // 値 T が存在することを示す
      }
      
    • これにより、値が存在しないかもしれないケースを型システムレベルで明示的に扱う必要があり、null ポインタ参照のような実行時エラーを防ぎます。
      let some_number = Some(5);
      let some_string = Some("a string");
      let absent_number: Option<i32> = None; // 型を指定する必要がある場合
      
      // Option<T> を扱うには match を使うのが基本
      fn plus_one(x: Option<i32>) -> Option<i32> {
          match x {
              None => None,
              Some(i) => Some(i + 1),
          }
      }
      let five = Some(5);
      let six = plus_one(five); // Some(6)
      let none = plus_one(None); // None
      
  4. Result<T, E> Enum:

    • エラーハンドリングに使われる標準ライブラリのEnumです。処理が成功したか失敗したかを表します。
    • Result<T, E> の定義:
      enum Result<T, E> {
          Ok(T),  // 処理が成功し、値 T を含む
          Err(E), // 処理が失敗し、エラー値 E を含む
      }
      
    • 例外 (exceptions) を投げる代わりに Result を返すことで、エラーが発生する可能性のある処理を明示的にし、呼び出し元にエラー処理を強制します。
      use std::fs::File;
      use std::io::Read;
      
      fn read_username_from_file() -> Result<String, std::io::Error> {
          let f_result = File::open("hello.txt"); // File::open は Result<File, io::Error> を返す
      
          let mut f = match f_result {
              Ok(file) => file,
              Err(e) => return Err(e), // エラーの場合は早期リターン
          };
      
          let mut s = String::new();
          match f.read_to_string(&mut s) { // read_to_string も Result を返す
              Ok(_) => Ok(s), // 成功したら Ok(String) を返す
              Err(e) => Err(e), // 失敗したら Err(io::Error) を返す
          }
          // (この処理は `?` 演算子を使うともっと簡潔に書けます - 後述)
      }
      
  5. if letwhile let:

    • match は強力ですが、特定の1つのパターンにのみ関心があり、他のケースは無視したい場合、if let 構文を使うとより簡潔に書けます。match の糖衣構文 (syntactic sugar) の一種です。
      let favorite_color: Option<&str> = None;
      let is_tuesday = false;
      let age: Result<u8, _> = "34".parse();
      
      if let Some(color) = favorite_color { // favorite_color が Some の場合のみ実行
          println!("Using your favorite color, {}, as the background", color);
      } else if is_tuesday {
          println!("Tuesday is green day!");
      } else if let Ok(age) = age { // age が Ok の場合のみ実行
          if age > 30 {
              println!("Using purple as the background color");
          } else {
              println!("Using orange as the background color");
          }
      } else { // 上記のどれでもない場合
          println!("Using blue as the background color");
      }
      
      // while let は、ループがパターンにマッチする限り続く
      let mut stack = Vec::new();
      stack.push(1);
      stack.push(2);
      stack.push(3);
      
      while let Some(top) = stack.pop() { // stack.pop() が Some(value) を返す間ループ
          println!("{}", top); // 出力: 3, 2, 1
      }
      

TypeScriptとの比較:

  • Enum:
    • TypeScript: Enumは基本的に数値または文字列への名前付きエイリアス。リバースマッピングなどの機能はあるが、バリアントごとに異なるデータを持つことはできない。
    • Rust: Enumは代数的データ型(Tagged Union)。各バリアントが異なる型のデータを持つことができるため、非常に表現力が高い。Option<T>Result<T, E> はその代表例。
  • パターンマッチ:
    • TypeScript: switch 文が最も近いが、マッチできるのはプリミティブ値が主。構造に基づいた分解や網羅性チェックはない。if/else チェーンやオブジェクトのプロパティチェックで代用することが多い。
    • Rust: match は値の構造に基づいた強力なパターンマッチングを提供。網羅性チェックにより、コンパイル時にバグを防ぐ。Enumとの組み合わせで特に強力。
  • Null/Undefined Handling:
    • TypeScript: nullundefined が存在する。strictNullChecks オプションで型安全性を高められるが、言語レベルでの強制ではない部分もある。T | null | undefined のようなユニオン型を使う。
    • Rust: null/undefined は存在しない。Option<T> Enumで値の欠如を明示的に表現し、matchif let で安全に処理することを強制する。
  • エラーハンドリング:
    • TypeScript: try...catch による例外処理、Promiseの .catch()、コールバックでのエラー渡しなどが一般的。
    • Rust: Result<T, E> Enumが標準的なエラー処理メカニズム。エラーが発生する可能性のある関数は Result を返し、呼び出し元に match? 演算子(後述)での処理を促す。

Enumと match は、Rustの型システムと安全性の中核をなす機能です。これらを使いこなすことで、堅牢で表現力豊かなコードを書くことができます。


これで、Rustの基本的なステップ1から8までの詳細な解説は完了です。各ステップには多くの情報が含まれていますので、実際にコードを書きながら、一つ一つ試していくことをお勧めします。特に所有権、借用、ライフタイム(ステップ5で少し触れました)はRustのユニークな部分なので、時間をかけて理解を深めてください。

次にどのステップについて更に掘り下げたい、あるいは具体的なコード例で試したいなど、ご希望があれば遠慮なくお申し付けください。

Discussion