Rust プログラミング 入門

Rust完全ガイド 第2回 制御フローと関数

制御フロー

プログラムを実行する際、常に上から順番にコードが処理されるわけではありません。
実際のアプリケーションでは、条件に応じて処理を分岐したり、繰り返し実行したりすることが必要になります。

Rustでは、これらの制御フロー(Control Flow)if 文や loopwhilefor などの構文を使って実現します。
この章では、Rustの制御フローを理解し、柔軟なプログラムを作成できるようになりましょう。

条件分岐 (if, else if, else)

条件に応じて異なる処理を実行するには、ifを使います。

fn main() {
    let number = 10;

    if number > 0 {
        println!("number は正の数です。");
    }
}

このコードでは、number0 より大きい場合に "number は正の数です。" を出力します。
もし number の値が -5 だった場合、何も出力されません。

else で別の処理を追加

else を使うことで、条件に合致しなかった場合の処理を記述できます。

fn main() {
    let number = -3;

    if number > 0 {
        println!("number は正の数です。");
    } else {
        println!("number は 0 以下の値です。");
    }
}

else if で複数の条件を設定

else if を使えば、複数の条件を順番にチェックできます。

fn main() {
    let number = 0;

    if number > 0 {
        println!("number は正の数です。");
    } else if number < 0 {
        println!("number は負の数です。");
    } else {
        println!("number は 0 です。");
    }
}

if 文の評価は上から順番に行われるため、最初に true になった条件の処理だけが実行されます。
例えば number = -5 の場合、number < 0 に一致するため、"number は負の数です。" のみが出力されます。

ループ (loop, while, for)

プログラムを実行する際、同じ処理を何度も繰り返したい場面があります。
Rustでは、loopwhilefor の3種類のループを使うことができます。

無限ループ (loop)

Rustでは、loop を使うことで無限ループを作成できます。

fn main() {
    let mut count = 0;

    loop {
        println!("カウント: {}", count);
        count += 1;

        if count == 5 {
            break;
        }
    }
}
  • loop無条件に繰り返し処理を実行します。
  • break を使うことでループを抜けることができます。
  • count5 になると break; が実行され、ループが終了します。

無限ループはゲームループサーバープログラムなど、継続的に動作する処理でよく使われます。

条件付きループ (while)

while を使うと、条件が満たされている間だけループを実行できます。

fn main() {
    let mut number = 3;

    while number > 0 {
        println!("{}...", number);
        number -= 1;
    }

    println!("発射!");
}

出力:

3...
2...
1...
発射!
  • while条件が true の間だけ実行されます。
  • number0 になると条件が false になり、ループが終了します。

反復処理 (for)

for は、コレクション(配列や範囲)を順番に処理するループです。

fn main() {
    let numbers = [10, 20, 30, 40, 50];

    for num in numbers.iter() {
        println!("数値: {}", num);
    }
}

出力:

数値: 10
数値: 20
数値: 30
数値: 40
数値: 50
  • for num in numbers.iter() は、配列の各要素を順番に処理します。
  • iter() を使うことで、配列の要素を借用しながら処理できます。

数値の範囲を指定してループ (for in)

for は、数値の範囲を指定して繰り返し処理を行うこともできます。

fn main() {
    for i in 1..=5 {
        println!("{}回目の処理", i);
    }
}

出力:

1回目の処理
2回目の処理
3回目の処理
4回目の処理
5回目の処理
  • 1..=5 は、1から5までの範囲を意味します。
  • ..= を使うと終端値を含む1, 2, 3, 4, 5)。
  • .. を使うと終端値を含まない1, 2, 3, 4 になる)。

for を使うと、配列の要素や数値範囲を扱う際にシンプルで分かりやすいコードを書くことができます。

まとめ

Rustの制御フローは、プログラムの流れを柔軟に制御するために不可欠な要素です。
if 文を使えば条件分岐を、loopwhilefor を使えば繰り返し処理を実装できます。

特に for配列や範囲を扱うのに便利であり、多くのRustプログラムで使われます。
また、無限ループ (loop) はサーバープログラムなど継続的に動作する処理で利用されます。

次は関数の基本について学びます。
関数は、コードを整理し、再利用しやすくするために重要な要素です。
引数や戻り値の扱い、関数のスコープについて理解を深めていきましょう。

関数

プログラムをより整理された構造にするために、関数(Function)は欠かせません。
Rustでは、関数を使うことでコードを分割し、再利用しやすくする
ことができます。

関数は、特定の処理をまとめることで、可読性を向上させ、バグの少ないプログラムを作るのに役立ちます。
また、Rustの関数は型安全であり、明確なスコープ管理が可能なため、バグを防ぐ設計になっています。

この章では、Rustにおける関数の定義方法、引数と戻り値、スコープとライフタイムについて学びます。

Rustの関数定義 (fn)

Rustの関数は、fn キーワードを使って定義します。

fn say_hello() {
    println!("Hello, Rust!");
}

fn main() {
    say_hello();
}
  • fn関数を定義するためのキーワードです。
  • say_hello という名前の関数を定義しています。
  • {} の中に関数の処理を書きます。
  • main 関数内で say_hello() を呼び出すことで、関数の内容が実行されます。

関数の命名規則

Rustでは、関数名はスネークケース(snake_caseを使用するのが一般的です。
例えば calculate_sumprint_message のように、単語をアンダースコアで区切ります。

fn calculate_sum() {
    println!("計算中...");
}

関数名を適切につけることで、コードの可読性を向上させることができます。

引数と戻り値

関数には引数(パラメータ)を渡したり、戻り値(リターン値)を返したりすることができます。

引数を持つ関数

関数に引数を渡すことで、異なる値に応じた処理を実行できます。

fn greet(name: &str) {
    println!("こんにちは, {}!", name);
}

fn main() {
    greet("Alice");
    greet("Bob");
}
  • name: &str は、引数の型を指定しています。
  • greet("Alice") のように、異なる引数を渡して関数を呼び出せます。

複数の引数を持つ関数

複数の引数を持たせることも可能です。

fn add(a: i32, b: i32) {
    println!("{} + {} = {}", a, b, a + b);
}

fn main() {
    add(5, 3);
}
  • a: i32, b: i32 で2つの整数を引数として受け取ります。
  • println! を使って計算結果を出力します。

戻り値を持つ関数

Rustの関数は、戻り値(リターン値)を持つことができます。
戻り値の型は ->(アロー演算子)を使って指定します。

fn square(n: i32) -> i32 {
    n * n
}

fn main() {
    let result = square(4);
    println!("4の2乗は {}", result);
}
  • -> i32 は、この関数が i32 型の値を返すことを示しています。
  • return キーワードを使わずに、最後の式の値が自動的に返されるのがRustの特徴です。
  • square(4) の結果は 16 になり、result に代入されます。

return を使う場合

Rustでは return を明示的に書くことも可能です。

fn cube(n: i32) -> i32 {
    return n * n * n;
}

fn main() {
    let result = cube(3);
    println!("3の3乗は {}", result);
}

この場合でも return の後に式を書くことで、関数の戻り値を指定できます。

スコープとライフタイム

Rustでは、変数のスコープ(有効範囲)が明確に定義されています。
スコープを理解することで、メモリの管理がしやすくなり、安全なプログラムを書くことができます。

スコープとは?

スコープとは、変数が参照可能な範囲のことです。

fn main() {
    let x = 10; // `x` のスコープ開始

    {
        let y = 20; // `y` のスコープ開始
        println!("x: {}, y: {}", x, y);
    } // `y` のスコープ終了

    println!("x: {}", x);
    // println!("y: {}", y); // エラー: `y` はこのスコープには存在しない
}
  • xmain 関数内で有効なので、どこでもアクセスできます。
  • y はブロック {} の中だけで有効なので、ブロックを抜けると y は無効になります。

このように、Rustでは変数のスコープを厳密に管理することで、安全なメモリ管理を実現しています。

ライフタイムとは?

Rustのライフタイム(Lifetime)は、変数の生存期間をコンパイラがチェックする仕組みです。
特に、参照(&)を扱うときに重要になります。

例えば、以下のコードはエラーになります。

fn main() {
    let r;

    {
        let x = 5;
        r = &x; // `x` の参照を `r` に代入
    } // `x` はここでスコープを抜けて無効になる

    println!("{}", r); // エラー! `r` は無効な参照を持っている
}
  • rx の参照を持っていますが、x はスコープを抜けた後に消えてしまいます。
  • Rustのコンパイラは無効な参照が発生しないようにライフタイムをチェックし、エラーを防ぎます。

ライフタイムの詳細は、所有権の章で詳しく解説します。

まとめ

Rustの関数は、fn を使って定義し、引数や戻り値を活用することで再利用性を高めることができます。
また、スコープを意識することで、安全なメモリ管理を行うことができます。

次回は、Rustの最も重要な概念の一つである 「所有権」 について学びます。
所有権システムを理解することで、Rustの安全性とパフォーマンスを両立した設計を深く理解できるようになります。
ぜひ、次のステップへ進んでみましょう!

所有権(Ownership)の基礎

Rustの最も特徴的な機能のひとつに「所有権(Ownership)」 があります。
所有権は、メモリ管理を安全かつ効率的に行うためのRust独自の仕組み です。

Rustではガベージコレクション(GC)が存在しないため、プログラムの実行時に不要なメモリを自動で解放する仕組みはありません。
その代わり、所有権のルールをコンパイル時にチェックすることで、安全にメモリを管理 します。

この章では、Rustのメモリ管理の仕組みを理解し、所有権・借用・スライス について学んでいきましょう。

Rustのメモリ管理の仕組み

プログラミング言語におけるメモリ管理には、主に以下の3つの方法があります。

メモリ管理の方法仕組み採用している言語
ガベージコレクション(GC)メモリを自動で管理し、不要になったデータを自動解放Java, Python, Go
手動メモリ管理mallocfree を使って、開発者が手動でメモリを確保・解放C, C++
所有権システムコンパイル時にメモリの所有権をチェックし、安全に管理Rust

Rustは、CやC++のように手動でメモリ管理をする必要がなく、かつ、GCのオーバーヘッドもない ため、高いパフォーマンスと安全性を両立 しています。

この仕組みを支えているのが 「所有権(Ownership)」 というルールです。

所有権(Ownership)とは

Rustでは、すべての値はある1つの変数が所有している というルールがあります。
所有権には3つの基本ルールがあります。

  1. 各値は1つの所有者(変数)しか持てない。
  2. 所有者がスコープを抜けると、その値はメモリから解放される。
  3. 所有権を移動(ムーブ)すると、元の変数は無効になる。

所有権の基本ルール

以下のコードを見てみましょう。

fn main() {
    let s1 = String::from("Hello");
    let s2 = s1;  // 所有権が `s1` から `s2` に移動
    println!("{}", s1);  // エラー! `s1` は無効
}

このコードはコンパイルエラーになります。
なぜなら、s1 の所有権が s2 に移動(ムーブ)したため、s1 は無効 になったからです。

所有権の移動(ムーブ)

所有権が移動すると、元の変数は無効になります。

fn main() {
    let s1 = String::from("Rust");
    let s2 = s1;  // s1 の所有権が s2 に移動
    println!("{}", s2);  // OK
    // println!("{}", s1);  // エラー! `s1` はもう使えない
}

このように、Rustでは変数のコピーではなく、所有権の移動が行われる ことを理解することが重要です。

借用(Borrowing)と参照(References)

所有権のルールをそのまま適用すると、関数に値を渡すたびに所有権が移動してしまうため、不便になります。
そこで登場するのが 「借用(Borrowing)」 です。

参照 (&) を使った借用

Rustでは、変数を「参照(Reference)」 として渡すことで、所有権を移動せずにデータを扱うことができます。

fn print_message(message: &String) {
    println!("{}", message);
}

fn main() {
    let s = String::from("Hello, Rust!");
    print_message(&s);  // 参照を渡す
    println!("{}", s);   // `s` は依然として有効
}
  • &String は、String の参照(借用) を意味します。
  • print_message(&s) のように & をつけることで、所有権を移動せずに関数へ値を渡せます。
  • 借用を使うことで、所有権を保持しながらデータを利用 できるようになります。

可変参照 (&mut)

デフォルトでは、参照は変更不可(イミュータブル) ですが、&mut を使うと変更可能な借用ができます。

fn change_string(s: &mut String) {
    s.push_str("!");  // 文字列に "!" を追加
}

fn main() {
    let mut s = String::from("Hello");
    change_string(&mut s);  // 可変参照を渡す
    println!("{}", s);  // Hello!
}
  • &mut String可変な参照 を意味します。
  • change_string(&mut s) により、関数内で s の内容を変更できます。

可変参照のルール

可変参照には以下のルールがあります。

  1. 同時に複数の可変参照を持つことはできない。
  2. 可変参照と不変参照を同時に持つことはできない。
fn main() {
    let mut s = String::from("Rust");

    let r1 = &mut s;
    let r2 = &mut s;  // エラー! `s` はすでに `r1` に借用されている
}

これは、データ競合を防ぐためのルールです。
Rustは、安全な並行処理を保証するために、同時に複数の可変参照を禁止 しています。

スライス(Slices)

スライスは、データの一部分を参照するための仕組みです。
&[start..end] のように範囲を指定して使用します。

fn main() {
    let s = String::from("Hello, Rust!");
    let hello = &s[0..5];  // "Hello" のスライス
    let rust = &s[7..11];  // "Rust" のスライス
    println!("{}, {}", hello, rust);
}
  • &s[0..5]文字列の一部分(スライス)を参照 します。
  • スライスは参照と同じく所有権を持たず、元のデータを変更しません。

スライスを使うことで、データの一部だけを安全に利用する ことができます。

まとめ

Rustの所有権(Ownership) は、メモリ管理の安全性を高めるために設計された仕組みです。
所有権の移動(ムーブ)により、不要なデータが自動的に解放されます。
また、借用(Borrowing) を利用することで、所有権を移動せずにデータを利用することができます。
さらに、スライス(Slices) を活用すると、データの一部分を安全に参照できます。

これで 第2回「制御フローと関数」 の学習が完了しました。
次回は、「Rustのデータ構造」 について詳しく学び、より高度なプログラムを作成できるようになりましょう!

  • この記事を書いた人

ふくまる

機械設計業をしていたが25歳でエンジニアになると決意して行動開始→ 26歳でエンジニアに転職→ 28歳でフリーランスエンジニアに→ 現在、34歳でフリーランス7年目 Go案件を受注中 Go,GCPが得意分野

-Rust, プログラミング, 入門