Rustにおける関数の基本 引数、mut修飾子とは:基本からしっかり学ぶRust入門(4)
Rustについて基本からしっかり学んでいく本連載。第4回は関数や引数の基本について。
Rustについて基本からしっかり学んでいく本連載。連載第4回は、関数についてRustでどう書くかを紹介します。
引数も戻り値もない関数の定義
Rustにおける関数は、C/C++のようなプログラミング言語における関数と変わりません。ある特定の機能を実現するものとして、引数を受け取り、処理の結果としての戻り値を返します。引数がない関数、戻り値のない関数がある点も同様です。
引数も戻り値もない関数は以下のように定義します。関数はネストできないので、main関数の外などグローバルに定義する必要があります。また、関数定義の場所はコンパイラが見つけることができる限り、どこでも構いません。main関数の後でも構いません。
fn function_name() { 関数の処理… }
関数定義は「fn」(fnはfunctionの意)と関数名を記述します。変数名やパッケージ名もそうであったように、関数名はスネークケース(全て小文字の英単語をアンダースコア〈_〉で区切った形式)を用いるのがRustの流儀です。引数も戻り値もない関数simple_functionを定義し、呼び出す例を示します。
fn main() { println!("シンプルな関数を呼び出します。"); simple_function(); // simple_function関数を呼び出す println!("関数呼び出しが終了しました。"); } // simple_functionの定義 fn simple_function() { // simple_functionの処理内容 println!("simple_functionは何も受け取らないし何も返しません。"); }
% cargo run --bin func1 シンプルな関数を呼び出します。 simple_functionは何も受け取らないし何も返しません。 関数呼び出しが終了しました。
main関数はバイナリが実行されるときに最初に呼び出される関数でもあります(「エントリーポイント」と呼ばれます)。main関数に引数はなく、戻り値もありません。C/C++のようにmain関数の引数にコマンドライン引数が渡されることはありません。Rustでは、コマンドライン引数の取得には専用のモジュールを使用しますが、これは入出力の回で取り上げる予定です。
simple_function関数も引数はありません。戻り値もないため、単純に何かをするだけという関数になっています。
【補足】Cとプロトタイプ宣言
Cでは、関数呼び出しより後に関数の定義がある場合には、あらかじめプロトタイプ宣言を関数呼び出しの前に記述する必要がありましたが、Rustではこのような二重定義は不要となっています。
引数のある関数の定義
引数のある関数は以下のように定義します。
fn function_name(arg: type[, ...]) { 関数の処理… }
引数(parameter:仮引数)は関数名に続く小かっこの中に型修飾とともに列挙します。引数の型修飾は、変数の宣言と異なり省略できません(型推論は利用できません)。引数が2個以上になる場合には、カンマで区切ります。i32型の引数が2個ある関数を定義し、それを呼び出す例を示します。
fn main() { display_sum(10, 5); // display_sum関数を実引数10と5で呼び出す } // display_sumの定義 fn display_sum(a: i32, b: i32) { // 仮引数はi32型のaとb println!("2つの引数の和は{}です。", a + b); // display_sumの処理内容 // 「2つの引数の和は15です。」 }
display_sumにはi32型のaとbという名前の引数が渡されます。aとbは仮引数としてdisplay_sumの中で利用され、処理の結果としてaとbの和の値が表示されます。main関数では、display_sum関数に引数(argument:実引数)として「10」と「5」を与えて呼び出します。
戻り値のある関数の定義
戻り値のある関数は以下のように定義します。
fn function_name(arg: type[, ...]) -> type { 関数の処理… 戻り値 }
関数に戻り値がある場合、アロー演算子「->」を利用して戻り値の型を指定します。また、戻り値はreturn式で返すか、最後に評価された式の値で返されます。関数の途中で明示的に抜けたいということでない限りは、後者の方法がRustらしいといえるでしょう。
fn main() { let a = 5; let b = 10; println!("{}の二乗は{}、{}の二乗は{}です。", a, square1(a), b, square2(b)); // 「5の二乗は25、10の二乗は100です。」 } // square1関数の定義 fn square1(x: i32) -> i64 { // 仮引数はi32型のx、戻り値はi64 return x as i64 * x as i64; // return式で返す } // square2関数の定義 fn square2(x: i32) -> i64 { // 仮引数はi32型のx、戻り値はi64 x as i64 * x as i64 // 式で返す }
square1関数は、return式で関数の処理結果を返す、C/C++でもおなじみの形式です。square2関数は、計算式がぽつんとあるだけで違和感があるかもしれませんが、x*xの評価結果が関数の戻り値になります。
引数はi32型ですが、その2乗を返すのでオーバフローを考慮して戻り値はi64型としています。計算式を見ると「as」というキーワードが出てきますが、これは「型キャスト」のための演算子です。なぜここで型キャストが必要なのかというと、戻り値がi64型なので、あらかじめi64型として乗算する必要があるからです。
ちなみに、オーバフローを考慮せず戻り値の型をi64型としたい場合には、以下のように書きます。intoは「トレイト」と呼ばれる関数の一つで、コンテキストに合った型(この場合はi64型)に自動的に変換されます。トレイトは、構造体の回で取り上げる予定です。
(x * x).into()
なお、戻り値がない関数は、空のタプルである「()」を返す関数となっています。すなわち、以下のように書くのと同義です。「()」を返すからといって、関数を呼び出した側でそれを受け取る必要はありません。
fn square1() -> () …
引数を書き換える関数の定義
引数のある関数の定義では、関数が引数を受け取って計算式に利用できることを示しました。では次のコードを見てください。このコードは、コンパイルするとエラーになります。
// main()関数は省略 // xのy倍を繰り返しで求める関数 fn multiplier(x: i32, y: i32) -> i32 { let mut r = 0; // 戻り値の変数 while y > 0 { r += x; y -= 1; } r // 戻り値 }
% cargo run --bin func4 Compiling functions v0.1.0 (/Users/nao/Documents/atmarkit_rust/functions) error[E0384]: cannot assign to immutable argument `y` --> src/bin/func4.rs:10:9 | 6 | fn multiplier(x: i32, y: i32) -> i32 { | - help: consider making this binding mutable: `mut y` ... 10 | y -= 1; | ^^^^^^ cannot assign to immutable argument
不変変数yへの代入はできないというエラーです。というのも、関数定義の引数においてmut修飾子が記述されていないので、この引数も不変変数になるのです。関数定義の引数にもmut修飾子を付けることで解決できます。
// main()関数は省略 fn multiplier(x: i32, mut y: i32) -> i32 { // yにmutを付加 let mut r = 0; while y > 0 { r += x; y -= 1; } r }
関数から抜ける方法
関数から戻り値を返すreturn式は、その場所で関数から抜ける動作になります。return式が置かれた場所より下にあるコードは一切実行されません。
条件分岐から抜ける
条件分岐を持つ関数が戻り値を返す場合、どのような実行経路になっても関数が値を返すようにreturn式か通常の式がなければなりません。次のようなソースコードはコンパイルエラーになります。
fn main() { let a = divider(5, 2); // 関数呼び出し } fn divider(x: i32, y: i32) -> i32 { if y != 0 { // yが0でない場合にx/yを返す return x / y; } }
% cargo run --bin func_if1 Compiling functions v0.1.0 (/Users/nao/Documents/atmarkit_rust/functions) error[E0317]: `if` may be missing an `else` clause --> src/bin/func6.rs:6:5 | 5 | fn divider(x: i32, y: i32) -> i32 { | --- expected `i32` because of this return type 6 | / if y != 0 { 7 | | return x / y; 8 | | } | |_____^ expected `i32`, found `()` | = note: `if` expressions without `else` evaluate to `()` = help: consider adding an `else` block that evaluates to the expected type
最初のエラーメッセージで「`if` may be missing an `else` clause」、すなわち「ifにはelse節がないのでは?」と表示されます。関数の戻り値がi32型なのに、その値を返さない経路があることを示しており、else節を記述すべきと示唆しているのです。また、8行目に対するエラーの内容が「expected `i32`, found `()`」となっているのは、if式が値を持たない、すなわち空のタプル()となるためです。いずれにしろ、if式の中で必ずreturn式を記述するか、何かしらの値に評価される必要があります。
先ほどのソースコードを次のように修正すると、コンパイルエラーにはならなくなります。
fn main() { let a = divider(5, 2); } fn divider(x: i32, y: i32) -> i32 { if y != 0 { // yが0でない場合にx/yを返す return x / y; } else { // yが0なら戻り値も0にする return 0; } }
% cargo run --bin func_if2 Compiling functions v0.1.0 (/Users/nao/Documents/atmarkit_rust/functions) warning: unused variable: `a` --> src/bin/func7.rs:2:9 | 2 | let a = divider(5, 2); | ^ help: if this is intentional, prefix it with an underscore: `_a` | = note: `#[warn(unused_variables)]` on by default
上記のソースコードではコンパイルエラーになりませんが、警告が出ます。main関数の中で変数aを関数の戻り値で初期化していますが、変数aがその後使われていないためです。アドバイスにあるように、aを_aという具合にアンダースコア(_)を前に置けば、それは故意(intentional)によるものと見なして警告は出なくなります。デバッグで一時的な変数を使いたい場合で、警告を出したくないときに利用するとよいでしょう。
このように、return式はif式の中で利用できますし、以下のように書くこともできます。
if y != 0 { x / y // yが0でない場合にif式の値をx/yとする } else { 0 // yが0である場合にif式の値を0にする } } // ここで関数を抜ける
if y != 0 { return x / y; // yが0でない場合にx/yを返す } 0 // 最終的に戻り値を0にする } // ここで関数を抜ける
return if y != 0 { x / y // yが0でない場合にx/yを返す } else { 0 // 最終的に戻り値は0にする }; // ここで関数を抜ける }
繰り返しから抜ける
繰り返しの構文において、繰り返しを止めたい場合にはbreak式を利用することを紹介しました。return式を置いてもその時点で繰り返しを中断し、関数から抜けることができます。以下のソースコードは例ですが、これはコンパイルエラーになります。
// main()関数は省略 fn calc_sum(mut x: i32) -> i32 { let mut r = 0; while x > 0 { r += x; if r > 10_000 { return r; } x -= 1; } }
% cargo run --bin func_while1 Compiling functions v0.1.0 (/Users/nao/Documents/atmarkit_rust/functions) error[E0308]: mismatched types --> src/bin/func_while1.rs:8:5 | 5 | fn calc_sum(mut x: i32) -> i32 { | --- expected `i32` because of return type ... 8 | / while x > 0 { 9 | | r += x; 10 | | if r > 10 { 11 | | return r; 12 | | } 13 | | x -= 1; 14 | | } | |_____^ expected `i32`, found `()`
エラーの内容は、型の不一致です。関数calc_sum()はi32型の値を返すことになっていますが、while式の値が空のタプルなので一致しないという意味です。先ほどのif式もそうですが、while式が値を持つために出るRust特有のエラーです。
【補足】Cのreturn文
Cでは警告こそ出るものの、このような書き方をしてもバイナリを実行できてしまうため「return文を経由しないときの関数の戻り値は不定になる」というバグの原因になっていました。このエラーを解決するには、if式の例と同様に、while式がreturn式により終了しなくても、何らかのi32型の値を返すように修正します。
// main()関数は省略 fn calc_sum(mut x: i32) -> i32 { let mut r: i32 = 0; while x > 0 { r += x; if r > 10_000 { return r; } x -= 1; } r }
まとめ
今回は、Rustの関数の振る舞いを紹介しました。次回はいよいよRustの核心的な機能である「所有権」を紹介します。
筆者紹介
WINGSプロジェクト
有限会社 WINGSプロジェクトが運営する、テクニカル執筆コミュニティー(代表山田祥寛)。主にWeb開発分野の書籍/記事執筆、翻訳、講演等を幅広く手掛ける。2021年10月時点での登録メンバーは55人で、現在も執筆メンバーを募集中。興味のある方は、どしどし応募頂きたい。著書、記事多数。
・サーバーサイド技術の学び舎 - WINGS(https://wings.msn.to/)
・RSS(https://wings.msn.to/contents/rss.php)
・Twitter: @yyamada(https://twitter.com/yyamada)
・Facebook(https://www.facebook.com/WINGSProject)
Copyright © ITmedia, Inc. All Rights Reserved.