今回は、「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
以下のプログラムを作成してみよう。
- 区切り線(
====================)を出力するprint_line関数を作る main関数で、print_lineを呼び出す- その後に「Rustの関数を学ぼう!」と出力する
- もう一度
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 integer4.0、2.0と、小数点を付けて渡せば解決する。
Rustは、整数から浮動小数点数への変換を自動ではしてくれない。
練習問題 2
以下のプログラムを作成してみよう。
- 名前(
&str)と年齢(i32)を受け取るintroduce関数を作る - 「太郎さんは25歳です」のように出力する
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の絶対値: 3ifは「条件によって処理を分ける」ための構文である。次回、詳しく学ぶので、今は「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
以下のプログラムを作成してみよう。
- 整数を受け取り、その2乗を返す
square関数 - 整数を受け取り、偶数なら
true、奇数ならfalseを返すis_even関数 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
ブロックを使って、以下のプログラムを作成してみよう。
totalという変数に、ブロックの値を代入する- ブロックの中で、10、20、30を別々の変数に入れて、合計を最後の式にする
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解説
- 最初の
xは5 - シャドーイングで、
xは6(5 + 1)になる - 内側のブロックでは、さらにシャドーイングして
xは12(6 * 2)になる。これが「A」として出力される - ブロックを抜けると、内側の
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_basictest ... 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を、じっくり学んでいこう。


コメント