Rust入門(3)「関数とスコープを学ぼう!」

スポンサーリンク
Rust

今回は、「Rust入門(2)「変数と型を学ぼう!」」の続編である。
なお、GitHub Codespacesで環境構築をしていない方は、第1回「GitHub Codespaces で Rust を始めよう!」を先にお読みください。

Codespacesを削除した場合は注意
GitHubにRustのリポジトリが残っていても、Codespacesを削除した場合や、新しいCodespacesを立ち上げた場合は、開発環境が新しくなるため、Rustのインストールや環境構築を改めて行う必要がある。リポジトリに保存されているソースコードや練習問題などはそのまま利用できるので、本記事では、必要に応じてRustの開発環境を準備してから学習を進めよう。

はじめに

前回は、Rustの変数と型について学んだ。

今回は、プログラムを部品に分けて整理するための関数と、変数が使える範囲を決めるスコープについて学んでいこう。

今回学ぶこと

  • 関数の定義と呼び出し
  • パラメータ(引数)
  • 戻り値
  • 複数の値を返す(タプル)
  • 式と文の違い
  • スコープ(変数が有効な範囲)

準備:体験用プロジェクトを開く

作業場所: trials/03_functions/
前回と同じように、このディレクトリで自由にコードを試してみよう。失敗しても問題ない。Codespacesを開いて、ターミナルで以下のコマンドを実行しよう。

# リポジトリを最新の状態にする
cd /workspaces/rust-practice
git pull

# 体験用プロジェクトに移動
cd trials/03_functions

# VSCodeでファイルを開く
code src/main.rs

# 動作確認
cargo run

これで、体験用プロジェクトの準備が整った。

学習の進め方

1. 体験する(trials/)

  • ブログ記事を読みながら trials/03_functions/ で試す
  • 自由にコードを書いて実験
  • エラーを恐れず、どんどん試そう

2. 練習する(exercises/)

  • 体験が終わったら、docs/03_functions.md(プレビュー表示)を参考にして、 exercises/03_functions/ で練習する
  • 構造化された問題を順番に解く
  • 解答例も用意されている

関数の基本

関数を定義して呼び出す

実は、これまで書いてきたfn main()も関数である。main関数はプログラムの入口となる特別な関数だが、自分で好きな名前の関数をつくることもできる。

src/main.rsを開いて、以下のコードを書いてみよう。

fn main() {
    println!("main関数から始まる");
    say_hello();
    say_hello();
}

fn say_hello() {
    println!("こんにちは、Rust!");
}
# 実行
cargo run

# 出力
main関数から始まる
こんにちは、Rust!
こんにちは、Rust!

ポイント

  • 関数はfnキーワードで定義する
  • 関数名はsnake_case(小文字とアンダースコア)で書く
  • 呼び出すときは、関数名()と書く
  • 同じ処理を何度も書かずに、関数にまとめて再利用できる

定義の順番は自由

上のコードでは、main関数の後ろでsay_hello関数を定義している。Rustでは、呼び出す側より後ろに関数を書いても問題ない。C言語のようにプロトタイプ宣言をする必要はない。

補足:println!の「!」は何?

println!は関数ではなくマクロと呼ばれるものである。名前の最後に!が付いているのが目印だ。今のところは「関数によく似た仕組み」と覚えておけばよい。

練習問題 1

以下のプログラムを作成してみよう。

  1. 区切り線(====================)を出力するprint_line関数を作る
  2. main関数で、print_lineを呼び出す
  3. その後に「Rustの関数を学ぼう!」と出力する
  4. もう一度print_lineを呼び出す

解答例を見る

fn main() {
    print_line();
    println!("Rustの関数を学ぼう!");
    print_line();
}

fn print_line() {
    println!("====================");
}

パラメータ(引数)

パラメータを受け取る

関数に値を渡したいときは、パラメータ(引数)を使う。

fn main() {
    greet("太郎");
    greet("花子");
}

fn greet(name: &str) {
    println!("こんにちは、{}さん!", name);
}
# 出力
こんにちは、太郎さん!
こんにちは、花子さん!

ポイント

  • パラメータは名前: 型の形で、関数名の後ろの()の中に書く
  • 関数のパラメータには、型を必ず明示する(変数のletのような型推論は働かない)
  • &strは文字列スライスを表す型である。文字列リテラル("太郎"など)も&strとして扱える。今は「文字列を受け取るときは&strと書く」と覚えておこう(詳しくは後の所有権で学ぶ)

複数のパラメータ

パラメータが複数あるときは、カンマ(,)で区切る。

fn main() {
    print_sum(3, 5);
    print_rectangle_area(4.5, 2.0);
}

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

fn print_rectangle_area(width: f64, height: f64) {
    println!("横{:.1}、縦{:.1}の長方形の面積: {:.1}", width, height, width * height);
}
# 出力
3 + 5 = 8
横4.5、縦2.0の長方形の面積: 9.0

{:.1}は、小数点以下1桁で表示する指定である。

型が合わないとエラー

パラメータの型と違う値を渡すと、コンパイルエラーになる。たとえば、f64のパラメータに整数の4を渡してみよう。

fn main() {
    print_rectangle_area(4, 2); // ← エラー!
}

fn print_rectangle_area(width: f64, height: f64) {
    println!("面積: {:.1}", width * height);
}
error[E0308]: mismatched types
(一部抜粋)expected `f64`, found integer

4.0、2.0と、小数点を付けて渡せば解決する。
Rustは、整数から浮動小数点数への変換を自動ではしてくれない。

練習問題 2

以下のプログラムを作成してみよう。

  1. 名前(&str)と年齢(i32)を受け取るintroduce関数を作る
  2. 「太郎さんは25歳です」のように出力する
  3. main関数から、2人分を呼び出す

解答例を見る

fn main() {
    introduce("太郎", 25);
    introduce("花子", 30);
}

fn introduce(name: &str, age: i32) {
    println!("{}さんは{}歳です", name, age);
}

戻り値

値を返す関数

関数から呼び出し元に値を返したいときは、->の後ろに戻り値の型を書く。

fn main() {
    let result = add(3, 5);
    println!("3 + 5 = {}", result);
}

fn add(a: i32, b: i32) -> i32 {
    a + b
}
# 出力
3 + 5 = 8

ポイント

  • -> i32は、「この関数はi32型の値を返す」という意味である
  • 関数の最後の式(セミコロンを付けない行)の値が、そのまま戻り値になる
  • 他の言語のようにreturnを書かなくても値を返せる

戻り値として返す式にセミコロンを付けると?

戻り値として返したい最後の式にセミコロンを付けるとエラーになる。

fn add(a: i32, b: i32) -> i32 {
    a + b; // ← セミコロンを付けるとエラー!
}
error[E0308]: mismatched types
(一部抜粋)expected `i32`, found `()`
(一部抜粋)help: remove this semicolon to return this value

セミコロンを付けると、a + bは「値を返さない文」になってしまう。すると、この関数は何も返さない(()を返す)ことになり、-> i32と矛盾するためエラーになる。

エラーメッセージにも「セミコロンを外せば値を返せる」というヒントが出ている。
Rustのエラーメッセージは親切なので、よく読んでみよう。

returnで途中で抜ける

関数の途中で値を返したいときは、returnキーワードを使う。

fn main() {
    println!("-5の絶対値: {}", absolute(-5));
    println!("3の絶対値: {}", absolute(3));
}

fn absolute(n: i32) -> i32 {
    if n < 0 {
        return -n;
    }
    n
}
# 出力
-5の絶対値: 5
3の絶対値: 3

ifは「条件によって処理を分ける」ための構文である。次回、詳しく学ぶので、今は「nが0より小さいときだけreturn -n;が実行される」と理解しておけばよい。

使い分けの目安

  • 通常は、最後の式で値を返す
  • 条件によって早めに関数を終わらせたいときにreturnを使う

複数の値を返す(タプル)

前回学んだタプルを使うと、複数の値をまとめて返せる。

fn main() {
    let (sum, product) = calc(4, 5);
    println!("和: {}, 積: {}", sum, product);

    let (quotient, remainder) = divide(17, 5);
    println!("17 ÷ 5 = {} あまり {}", quotient, remainder);
}

fn calc(a: i32, b: i32) -> (i32, i32) {
    (a + b, a * b)
}

fn divide(a: i32, b: i32) -> (i32, i32) {
    (a / b, a % b)
}
# 出力
和: 9, 積: 20
17 ÷ 5 = 3 あまり 2

戻り値を受け取るときも、前回学んだ分割代入(let (sum, product) = ...)が使える。

練習問題 3

以下のプログラムを作成してみよう。

  1. 整数を受け取り、その2乗を返すsquare関数
  2. 整数を受け取り、偶数ならtrue、奇数ならfalseを返すis_even関数
  3. main関数で、5の2乗と、4と7が偶数かどうかを出力

ヒント: 偶数かどうかは、2で割ったあまり(n % 2)が0かどうかで判定できる。

解答例を見る

fn main() {
    println!("5の2乗: {}", square(5));
    println!("4は偶数? {}", is_even(4));
    println!("7は偶数? {}", is_even(7));
}

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

fn is_even(n: i32) -> bool {
    n % 2 == 0
}

式と文の違い

式と文とは?

戻り値の仕組みを理解するために、Rustの式(expression)と文(statement)の違いを確認しておこう。

種類特徴例
文何かを実行するが、値を返さないlet x = 5;
式計算して、値になる5、x + 1、add(1, 2)

Rustの関数は、いくつかの文と、最後に置く式で構成される。
「最後の式が戻り値になる」というのは、この仕組みによるものだ。

ブロックも式

{ }で囲んだブロックも、最後に式があれば、その値を持つ式になる。

fn main() {
    let y = {
        let x = 3;
        x + 1 // セミコロンなし → ブロックの値になる
    };

    println!("yの値は: {}", y);
}
# 出力
yの値は: 4

ブロックの中のx + 1にセミコロンがないので、その値(4)がブロック全体の値になり、yに代入される。

なお、letは文なので、値を返さない。そのため、let x = (let y = 6);のような書き方はできない。

練習問題 4

ブロックを使って、以下のプログラムを作成してみよう。

  1. totalという変数に、ブロックの値を代入する
  2. ブロックの中で、10、20、30を別々の変数に入れて、合計を最後の式にする
  3. totalを出力

解答例を見る

fn main() {
    let total = {
        let a = 10;
        let b = 20;
        let c = 30;
        a + b + c
    };

    println!("合計: {}", total);
}

スコープ

スコープとは?

変数が使える範囲のことをスコープという。Rustでは、変数は宣言された位置から、それを含むブロック({ })の終わりまで有効である。

fn main() {
    let x = 10;

    {
        let y = 20;
        println!("内側: x = {}, y = {}", x, y);
    }

    println!("外側: x = {}", x);
}
# 出力
内側: x = 10, y = 20
外側: x = 10
  • 内側のブロックからは、外側のxも使える
  • yは内側のブロックの中だけで有効である

スコープの外では使えない

内側で宣言した変数を、ブロックの外で使おうとするとエラーになる。

fn main() {
    {
        let y = 20;
        println!("内側: y = {}", y);
    }

    println!("外側: y = {}", y); // ← エラー!
}
error[E0425]: cannot find value `y` in this scope

ブロックが終わった時点で、yは使えなくなる。これが「スコープを抜ける」ということだ。

内側でのシャドーイング

前回学んだシャドーイングを、内側のブロックで使うとどうなるだろうか。

fn main() {
    let x = 1;

    {
        let x = 2;
        println!("内側のx: {}", x);
    }

    println!("外側のx: {}", x);
}
# 出力
内側のx: 2
外側のx: 1

内側のxは、ブロックの中だけで外側のxを隠している。ブロックを抜けると、外側のx(1)に戻る。

関数の間で変数は共有されない

関数の中で宣言した変数は、その関数の中だけで有効である。別の関数からは使えない。

fn main() {
    let message = "main関数の変数";
    show();
}

fn show() {
    println!("{}", message); // ← エラー!
}
error[E0425]: cannot find value `message` in this scope

別の関数に値を渡したいときは、パラメータを使う。

fn main() {
    let message = "main関数の変数";
    show(message);
}

fn show(message: &str) {
    println!("{}", message);
}
# 出力
main関数の変数

定数は例外

関数の外側で宣言した定数(const)は、同じモジュール内の複数の関数から利用できる。
前回学んだ定数を、関数と組み合わせてみよう。

const TAX_PERCENT: u32 = 10;

fn main() {
    let price = 1000;
    println!("税込み価格: {}円", price_with_tax(price));
}

fn price_with_tax(price: u32) -> u32 {
    price + price * TAX_PERCENT / 100
}
# 出力
税込み価格: 1100円

練習問題 5

以下のプログラムを実行すると、どのように出力されるだろうか。実行する前に予想してみよう。

fn main() {
    let x = 5;
    let x = x + 1;

    {
        let x = x * 2;
        println!("A: {}", x);
    }

    println!("B: {}", x);
}

解答を見る

A: 12
B: 6

解説

  1. 最初のxは5
  2. シャドーイングで、xは6(5 + 1)になる
  3. 内側のブロックでは、さらにシャドーイングしてxは12(6 * 2)になる。これが「A」として出力される
  4. ブロックを抜けると、内側のx(12)は消えて、外側のx(6)に戻る。これが「B」として出力される

総合練習問題

ここまでの内容を理解できたら、総合練習問題に挑戦しよう!

練習問題の場所

cd /workspaces/rust-practice/exercises/03_functions

# 練習問題一覧を表示
cargo run

# 個別の練習問題を実行
cargo run --bin ex01_basic
cargo run --bin ex02_return
cargo run --bin ex03_scope
cargo run --bin challenge01_bmi
cargo run --bin challenge02_temperature
cargo run --bin challenge03_circle

練習問題のファイルには、todo!()と書かれた部分がある(ex03_scopeは、出力を予想する問題なのでtodo!()はない)。ここを自分のコードに書き換えて完成させよう。
完成したかどうかは、次のコマンドでテストして確認できる。

# 例:ex01_basicのテストを実行
cargo test --bin ex01_basic

test ... okと表示されれば正解である。


GitHubに保存しよう

学習内容をGitHubに保存しよう。

cd /workspaces/rust-practice
git add .
git commit -m "Complete functions and scope practice"
git push

(参考)筆者のGitHub: https://github.com/Rocky-Seven/rust-practice

次回予告

次回は、制御構文について学ぶ予定である。

  • if式
  • loop、while、for
  • match式

今回のabsolute関数で少しだけ登場したifを、じっくり学んでいこう。

コメント

タイトルとURLをコピーしました