【Rust入門】関数定義の基本を分かりやすく解説

Rust における関数定義の方法について初心者にも分かりやすく解説します。
Rust 関数の基本
プログラミングにおいて関数とは、何度も使用するようなコードを1つのブロックにまとめて関数として名前付けし、何度も使うようにできるものです。関数は、プログラミングにおいてどのような言語であっても中核的な役割をします。
この記事では、Rust における関数定義や引数などの基本について分かりやすく解説します。
基本的な関数定義と呼び出し
Rust の関数は、fn
を使用して以下のように定義します。
fn 関数名() { // 関数で実行する処理 }
以下は、入力された整数の2
乗を返す square
関数を定義し、main
関数から呼び出している例です。
// 2乗を返す関数 square を定義 fn square(x: i32) -> i32 { x * x } fn main() { // square関数を呼び出し let result = square(2); println!("2 の 2乗 は {} です。", result); }
【実行結果】 2 の 2乗 は 4 です。
関数 square
が受け取っている x
を引数と言い、「: i32」の部分は引数の型を示す型注釈と言います。また、「-> i32
」の部分は、戻り値の型を示す型注釈です。Rust では関数の最後の式が戻り値となるため、戻り値として値を返す際には「x * x
」のようにセミコロン(;
)をつけずに記載します。引数や戻り値については以降で詳しく説明します。
作成した関数を呼び出すときには、main
関数内で let result = square(2);
としているように「関数名();
」で何度でも呼び出すことができます。また、戻り値を let
を使って変数に束縛することも可能です。
関数定義のルール
関数定義のルールについて、整理しておきましょう。
型注釈は必須
関数では、引数の型注釈(例:x: i32
)や戻り値の型注釈(例:-> i32
)を明示することが必須です。変数定義の時は、型推論により型が推論されますが、関数の引数の場合は異なるので注意しましょう。ただし、戻り値がないような関数の場合は「->
」の型注釈は省略可能です。
()
戻り値がない場合には、実際には「-> ()
」という戻り値が設定されています。この ()
はユニット型と言います。
戻り値にはセミコロン「;
」を付けない
Rust では、関数の最後の式が戻り値となります。セミコロン「;
」を記載すると文となってしまうため、関数内で戻り値を書く場合には「;
」を付けないように注意してください。
fn square(x: i32) -> i32 { x * x // セミコロンを付けない }
関数名はスネークケース(snake_case
)が慣習
関数名は、小文字をアンダースコアでつなげるスネークケース(snake_case
)で書くのがRustの慣習となっています。
戻り値と早期終了 (return
)
Rust では、関数の最後の式が戻り値になることを見てきましたが、return
キーワードを使って明示的に値を返すことも可能です。
以下の例では、絶対値を返却する abs
関数を作成し、x < 0
の場合は、その場で -x
を return
により返して早期終了しています。
fn abs(x: i32) -> i32 { if x < 0 { // 早期終了 (return) return -x; } // 通常の戻り値 x } fn main() { // abs関数を呼び出し let result1 = abs(-5); let result2 = abs(10); println!("結果 result1: {}, result2: {}", result1, result2); }
【実行結果】 結果 result1: 5, result2: 10
関数の最後で return
を使うのは、非常に多くの言語で見られる方法です。もちろん、Rust でも最後の戻り値で return
を使うことができますが、Rust では、最後の戻り値では return
を書かないことが一般的です。
関数のシャドーイング(定義の上書き)
Rust では、変数の定義で同じ変数名で let
における再定義をすることができました。関数でも同じようにシャドーイングにより定義を上書きすることができます。
関数の場合には、外側のスコープで定義した関数を、内側のスコープでシャドーイングすることで定義を上書きすることができます。let
の時には同一スコープで再定義ができましたが、関数は同一スコープでは複数回の定義はできないので注意してください。
以下の例で見てみましょう。
fn message() { println!("メッセージ 1"); } fn show_message() { message(); // 「メッセージ 1」を表示 } fn main() { message(); // 「メッセージ 2」を表示 { message(); // 「メッセージ 3」を表示 fn message() { println!("メッセージ 3"); } } message(); // 「メッセージ 2」を表示 show_message(); // show_message関数経由で「メッセージ 1」を表示 fn message() { println!("メッセージ 2"); } }
【実行結果】 メッセージ 2 メッセージ 3 メッセージ 2 メッセージ 1
上記の例だと、message
という名前の関数を3回定義しています。show_message
関数が使用するのは「メッセージ 1
」と表示する message
関数です。一方で、main
関数の最初のスコープでは「メッセージ 2
」と表示する message
関数を、一段階内側の {}
ブロック内での呼び出しでは「メッセージ 3
」と表示する message
関数を呼び出します。
このように、関数もスコープによってシャドーイングされる対象となります。ただし、このようにスコープ内で入れ子に関数を定義していくようなスタイルは可読性がよくありません。上記の例は、関数もシャドーイングされるという Rust の言語仕様を説明するものだと理解してください。
関数の引数と戻り値の扱い
ここからは、関数の引数と戻り値の扱いについて、もう少し詳しく見ていきましょう。
引数を値で渡す
関数では、引数として値を渡すことができます。引数では、関数定義で使用される引数を「仮引数(parameter)」、呼び出し時に渡される引数を「実引数(argument)」と言います。
仮引数は渡された実引数に対する別名ではなく、オブジェクトのコピーとなっています※。つまり、メモリー上では別で格納されているものです。このコピーは、関数を呼び出すときに作られて、関数が終わって制御が呼び出し元に戻るときに破棄されます。
// x は仮引数 fn square(x: i32) -> i32 { x * x } fn main() { let x = 2; // x は実引数 let result = square(x); println!("{} の 2乗 は {} です。", x, result); }
上記の例では、square
関数の x: i32
が仮引数で、main 関数内の x
が実引数です。main
関数内で square
関数が呼び出されると square
関数の x
としてコピーが作成され、square
関数処理がされて値を返した際に破棄されています。
その後の println!
で x
を使用していますが、この値は main
関数で最初に定義している let x = 2;
です。これは、C言語にもある値渡しの仕組みと同じものです。
関数から値を返す
関数では、引数を受け取るだけではなく、処理結果を呼び出し元に返すことができます。上記の例でも見てきたように関数の戻り値は、最後の式の値になります。
fn square(x: i32) -> i32 { x * x // 戻り値となる式 }
関数の戻り値を表す型注釈は、関数名の後の「->
」に続けて記載をします。例えば、戻り値がないような関数の場合は、空のタプル ()
が戻されているとみなされます。戻り値がない場合には「-> ()
」は省略可能です。
複数の値を返す
関数から複数の値を返却したい場合には、タプルを使用することで値を返すことができます。以下は、入力された2つの引数で min
と max
を計算し、(min, max)
のタプルとして返却している例です。
// 最小(min), 最大(max) を確認し、(min, max) のタプルで返却する fn min_max(a: i32, b: i32) -> (i32, i32) { if a < b { (a, b) } else { (b, a) } } fn main() { let a = 10; let b = 5; let (min, max) = min_max(a, b); println!("min: {}, max: {}", min, max); }
【実行結果】 min: 5, max: 10
呼び出し側の変数の扱い(不変参照と可変参照)
これまでの例では、実引数を関数の仮引数にコピーして処理を返して値を返すというような例を見てきました。実際には、呼び出し元の変数を直接変更したい場合も出てきます。このような場合について、Rustでは所有権という考え方を意識しないといけません。ここでは、基本型と所有権を持つ型(String
や Vec
)に分けて説明をします。
基本型の場合
基本型はメモリのスタック領域に置かれる型で通常や仮引数へ渡す際にはコピーされます。呼び出し元の基本型の値を変更したい場合には、以下のように &mut
として値を変更できる可変参照を渡して使用します。
// 呼び出し元の変数を変更する fn increment(n: &mut i32) { *n += 1; } fn main() { let mut num = 0; // 1回目の呼び出し (可変参照) increment(&mut num); println!("num: {}", num); // 2回目の呼び出し (可変参照) increment(&mut num); println!("num: {}", num); }
【実行結果】 num: 1 num: 2
変更が可能な参照を指定する場合には、呼び出し時に実引数には「&mut 変数
」のように渡します。一方で、受け取る関数の型注釈は「: &mut 型名
」を付けます。Rust において &
は参照を表します。そして、*
は参照から値を取り出すための演算子です。今回は可変参照を使用しているので、値を書き換えることができます。
&mut num
は、変数 num
の可変な参照を渡しているので、関数 increment
で *n
により値を変更しています。main
関数で呼び出しから戻ってきた際に num
の値が変更されていることからも分かると思います。
基本型を関数に渡す際にコピーされるのは、基本型が Copy
トレイトを実装しているためです。一方で後述する所有権を持つ型は Copy
トレイトが実装されていないため基本は所有権が移動(move)します。
所有権を持つ型の場合
基本型について見ましたが、所有権を持つ型(String
や Vec
)といった型では基本型と少し挙動が異なり、所有権の移動や借用(不変参照、可変参照)について意識する必要があります。
所有権が移動する場合
String
型の例で説明をしていきます。まずは、所有権が移動してしまう例を見てみましょう。
// 所有権が移動 (move) する場合 fn print_str_move(s: String) { println!("print_str_move: {}", s); } fn main() { // 所有権の移動の確認 let s1 = String::from("Hello World"); print_str_move(s1); println!("呼び出し元のs1: {}", s1); }
【実行結果】 error[E0382]: borrow of moved value: `s1` --> examples\arg_param_string_move.rs:10:30 | 8 | let s1 = String::from("Hello World"); | -- move occurs because `s1` has type `String`, which does not implement the `Copy` trait 9 | print_str_move(s1); | -- value moved here 10 | println!("呼び出し元のs1: {}", s1); | ^^ value borrowed here after move | note: consider changing this parameter type in function `print_str_move` to borrow instead if owning the value isn't necessary --> examples\arg_param_string_move.rs:2:22 | 2 | fn print_str_move(s: String) { | -------------- ^^^^^^ this parameter takes ownership of the value | | | in this function = note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info) help: consider cloning the value if the performance cost is acceptable | 9 | print_str_move(s1.clone()); | ++++++++ For more information about this error, try `rustc --explain E0382`. error: could not compile `function_basic` (example "arg_param_string_move") due to 1 previous error
この例では、コンパイル時にエラーとなります。これは、関数に s1
を渡す際に所有権を関数側へ移動(move
)しているためです。この時に、"Hello World"
が格納されるオブジェクトは関数へ所有権が移り、関数呼び出し後に解放されてしまいます。そのため、呼び出し元で再度呼び出そうとするとエラーとなってしまいます。
これを回避する1つの方法は、help
に記載のように clone()
を使用してコピーを渡すことです。以下のようにするとエラーは起こりません。
// コピーを受け取っている fn print_str_move(s: String) { println!("print_str_move: {}", s); } fn main() { // 所有権の移動の確認 let s1 = String::from("Hello World"); // 所有権が移動しないようにコピーを渡す print_str_move(s1.clone()); println!("呼び出し元のs1: {}", s1); }
ただし、String
や Vec
のようにヒープ領域を使用する型はサイズが可変であることから、使用している領域が大きい可能性があり、コピーをすることは不必要なメモリ利用や応答性の低下につながる可能性があります。そのため、以降で紹介する借用(不変参照や可変参照)をうまく使用します。
借用(不変参照と可変参照)
所有権を借用するケースについて見ていきましょう。Rust において借用には、読み取り専用の「不変参照」と変更も可能な「可変参照」があります。これらは呼び出し元で定義している変数が指すオブジェクトへの参照です。
// 不変参照 (読み取り専用) の場合 fn print_str_ref(s: &String) { println!("print_str_ref: {}", s); } // 可変参照 (変更可) の場合 fn print_str_change(s: &mut String) { s.push_str("!!!"); println!("print_str_change: {}", s); } fn main() { // 不変参照の場合 let mut s1 = String::from("World World"); print_str_ref(&s1); // 可変参照の場合 print_str_change(&mut s1); println!("呼び出し元のs1: {}", s1); }
【実行結果】 print_str_ref: World World print_str_change: World World!!! 呼び出し元のs1: World World!!!
読み取り専用の不変参照で関数を定義する場合には、型注釈として「: &型名
」として定義します。関数呼び出しの際には「&変数名
」で渡します。これは、呼び出し元のオブジェクトを指していますが、読み取り専用の参照のため誤った値の変更を避けることができます。
次に変更可の可変参照で関数を定義する場合には、型注釈として「: &mut 型名
」として定義します。関数呼び出しの際には「&mut 変数名
」で渡します。これは変更可であるため呼び出し元の変数の値を変更します。そのため、上記例で main
関数に戻ってきた際に s1
の値が変わっていることが分かります。
上記の内容は所有権の考え方をまだ学んでいない方には少し難しいかもしれません。所有権や借用については、別途詳しく説明しようと思います。
*
がいらない理由上記の例で String
の参照から値にアクセスする際に *
を使っていませんでした。これは、String
型が Deref
や DerefMut
トレイトを実装しているためです。
*
により参照から値を取り出すことをデリファレンスと呼びます。Rust では、Deref
や DerefMut
によりデリファレンスの振る舞いが定義されており、String
型や Vec
型のような型では *
を使わなくてもいいように Rust の型システムが自動で変換してくれます。
まとめ
Rust の関数は、明確な型注釈や所有権のルールにも従って、安全で予測可能なコードを書くために必要なものです。この記事では、関数の基本的な定義方法から、引数と戻り値の扱い、所有権と参照の使い分けまでを学びました。
特に、以下のポイントを押さえておきましょう。
- 関数の引数と戻り値には型注釈が必須(ただし、戻り値がない場合は省略可能)
- 関数の最後の式が戻り値(セミコロンをつけない)
- 所有権の移動と参照(不変・可変)の違いを理解する
- 基本型と所有権を持つ型(Stringなど)で挙動が異なる
Rustの関数をしっかりと理解することは、より発展的なジェネリクスやクロージャなどの理解のために必要です。ぜひこの記事を参考に、関数の書き方に慣れていきましょう。