Rustは、メモリ安全性と高パフォーマンスを両立させるプログラミング言語であり、その設計思想は関数の戻り値の扱いにも色濃く反映されています。
プログラミングにおいて、関数がどのような値を返し、どのようにエラーを伝えるかを正しく理解することは、堅牢なアプリケーションを構築するための第一歩です。
本記事では、Rustにおける関数の戻り値の基本的な書き方から、実践的なエラー処理のパターンまでを具体例を交えて詳しく紹介します。
2026年現在のモダンな開発手法に基づき、初心者から中級者まで役立つ知識を整理していきましょう。
Rustにおける関数の戻り値の基本
Rustの関数において、戻り値の型は関数のシグネチャの一部として明示的に記述する必要があります。
戻り値の型を指定するには、引数リストの後に矢印記号 -> を使用します。
Rustの最大の特徴の一つは、「式(Expression)」が値を評価して戻り値になるという点です。
関数の最後にセミコロンを付けずに記述された式は、その関数の戻り値として扱われます。
以下のコードは、2つの整数を加算して結果を返す最もシンプルな関数の例です。
// 戻り値を i32 型として定義します
fn add(a: i32, b: i32) -> i32 {
// セミコロンがないため、この計算結果が戻り値になります
a + b
}
fn main() {
let sum = add(10, 20);
println!("合計は {} です", sum);
}
合計は 30 です
もし式の末尾にセミコロンを付けてしまうと、それは「文(Statement)」になり、値を返さなくなってしまいます。
この場合、コンパイルエラーが発生するため、Rustに慣れていないうちは特に注意が必要です。
明示的な return 文と早期リターン
関数の途中で処理を終了して値を返したい場合には、return キーワードを使用します。
これは「早期リターン」と呼ばれる手法で、特定の条件を満たした際に関数を即座に終了させるのに便利です。
関数の最後の行であっても return を使うことは可能ですが、Rustの慣習としては省略するのが一般的です。
以下の例では、数値が負の場合に早期リターンを行う関数を示します。
fn check_positive(number: i32) -> bool {
if number < 0 {
// 数値が負の場合は即座に false を返して終了します
return false;
}
// 正常な場合は true を返します(return を省略した式)
true
}
fn main() {
let result = check_positive(-5);
println!("チェック結果: {}", result);
}
チェック結果: false
このように、複雑な条件分岐がある場合は return を使うことで、コードの可読性を向上させることができます。
戻り値がない場合のユニット型 ()
値を返す必要がない関数の場合、Rustでは戻り値の型を省略することができます。
型を省略した場合、その関数は実際には () という「ユニット型」を返しています。
ユニット型は、「特に意味のある値を持たない」ことを表す特別な型です。
明示的に -> () と記述することもできますが、通常は冗長であるため記述しません。
// 戻り値を指定しない関数
fn say_hello() {
println!("こんにちは、Rustの世界へ!");
}
fn main() {
// 戻り値を受け取っても、それはユニット型 () です
let val = say_hello();
println!("戻り値の内容: {:?}", val);
}
こんにちは、Rustの世界へ!
戻り値の内容: ()
ユニット型は、副作用(画面への出力やデータの保存など)を主目的とする関数で頻繁に利用されます。
複数の値を返すタプル型
Rustでは、一つの関数から複数の値を返したい場合に「タプル」を利用するのが非常に便利です。
構造体を定義するほどではない一時的なデータのセットを返すのに適しています。
戻り値を受け取る側では、「パターンの解体」を使って個別の変数に分解して受け取ることができます。
// (i32, i32) というタプル型を戻り値に指定します
fn get_coordinates() -> (i32, i32) {
let x = 100;
let y = 200;
(x, y) // タプルを返します
}
fn main() {
// 戻り値を分解して受け取ります
let (x_pos, y_pos) = get_coordinates();
println!("X座標: {}, Y座標: {}", x_pos, y_pos);
}
X座標: 100, Y座標: 200
タプルを使うことで、データの関連性を保ったまま簡潔に複数の値を伝播させることが可能です。
条件によって値がない可能性を示す Option 型
Rustには他の言語で見られるような「null」という概念が存在しません。
その代わりに、値が存在するか、あるいは存在しないかという状態を表す Option<T> 型を使用します。
関数が検索結果を返す際や、特定の条件下で計算が不可能な場合などに非常によく使われます。
fn find_index(target: i32, list: Vec<i32>) -> Option<usize> {
for (index, &item) in list.iter().enumerate() {
if item == target {
// 値が見つかった場合は Some で包んで返します
return Some(index);
}
}
// 見つからなかった場合は None を返します
None
}
fn main() {
let numbers = vec![10, 20, 30, 40];
match find_index(30, numbers) {
Some(idx) => println!("インデックス {} で見つかりました", idx),
None => println!("見つかりませんでした"),
}
}
インデックス 2 で見つかりました
このように戻り値を Option にすることで、開発者は値がない場合の処理を強制的に考慮させられるため、実行時のエラーを未然に防ぐことができます。
エラー処理を行うための Result 型
関数が失敗する可能性がある場合、Rustでは Result<T, E> 型を戻り値として使用するのが定石です。
Result 型は、成功時の値 Ok(T) と、失敗時のエラー内容 Err(E) のいずれかを持ちます。
例外(Exception)を使わずに戻り値としてエラーを扱うことで、プログラムの流れが予測しやすくなります。
fn divide(numerator: f64, denominator: f64) -> Result<f64, String> {
if denominator == 0.0 {
// 失敗した場合はエラーメッセージを Err で返します
return Err(String::from("ゼロで割ることはできません"));
}
// 成功した場合は結果を Ok で包んで返します
Ok(numerator / denominator)
}
fn main() {
match divide(10.0, 0.0) {
Ok(res) => println!("計算結果: {}", res),
Err(e) => println!("エラーが発生しました: {}", e),
}
}
エラーが発生しました: ゼロで割ることはできません
実務的なアプリケーション開発では、ファイル操作やネットワーク通信など、失敗の可能性がある処理の戻り値はほぼすべて Result 型となります。
所有権の移動と戻り値の関係
Rustの戻り値を理解する上で避けて通れないのが「所有権(Ownership)」の概念です。
関数内で作成された変数(データ)を戻り値として返す際、そのデータの所有権は呼び出し元へと「移動(Move)」します。
これにより、関数が終了してもデータがメモリから解放されず、安全に後続の処理で利用できるようになります。
fn create_string() -> String {
let s = String::from("Hello Rust");
// s の所有権が呼び出し元に移動します
s
}
fn main() {
// create_string から所有権を受け取ります
let my_str = create_string();
println!("受け取った文字列: {}", my_str);
}
大規模なデータを返す場合でも、Rustのコンパイラは効率的な最適化を行うため、パフォーマンス上の懸念はほとんどありません。
ただし、参照(ポインタ)を戻り値として返したい場合は、ライフタイムという高度な概念が必要になることを覚えておきましょう。
抽象的な型を返す impl Trait
具体的な型を明示するのが難しい場合や、型を隠蔽したい場合には impl Trait 構文を使用します。
これは、「特定のトレイトを実装している何らかの型」を返すという意味です。
特にイテレータやクロージャを返す際に非常に強力な威力を発揮します。
// Iterator トレイトを実装した型を返す
fn get_even_numbers(limit: i32) -> impl Iterator<Item = i32> {
(0..limit).filter(|x| x % 2 == 0)
}
fn main() {
let evens = get_even_numbers(10);
for n in evens {
println!("偶数: {}", n);
}
}
偶数: 0
偶数: 2
偶数: 4
偶数: 6
偶数: 8
呼び出し側は具体的な型名を知る必要がなく、指定されたトレイトのメソッドが使えることだけを保証されます。
非同期関数における戻り値の挙動
現代のRust開発において、async fn による非同期処理は欠かせません。
非同期関数の戻り値は、内部的には Future トレイトを実装した型でラップされています。
呼び出し側で .await を使用することで、その Future が完了した際の実際の戻り値を取り出すことができます。
async fn fetch_data() -> String {
// 実際にはここで通信などの待機処理が発生します
String::from("非同期データ")
}
#[tokio::main]
async fn main() {
// await を使うことで戻り値の String を取得します
let data = fetch_data().await;
println!("取得データ: {}", data);
}
非同期関数でも、これまで解説した Result や Option と組み合わせて戻り値を構成するのが一般的です。
まとめ
Rustにおける関数の戻り値は、単に計算結果を返すだけではなく、プログラムの安全性と意図を明確に伝えるための仕組みが詰まっています。
基本的な式の評価による戻り値から始まり、タプルによる複数値の返却、そして Option や Result による厳格なエラーハンドリングまで、そのパターンは多彩です。
これらのパターンを適切に使い分けることで、バグの少ない、メンテナンス性の高いコードを記述できるようになります。
特に Result 型によるエラー処理はRustの真骨頂であり、マスターすることで開発効率が劇的に向上するでしょう。
まずはシンプルな関数から始め、徐々に所有権や impl Trait などの高度な概念を戻り値に取り入れてみてください。
本記事で学んだ内容を土台として、Rustによる安全で高速なシステム開発を楽しんでいきましょう。
