Rustの式(Expression)と文(Statement)の違いを徹底解説!初心者が迷うセミコロンの秘密
生徒
「Rustのコードを書いていると、セミコロンをつける時とつけない時があって混乱します。これって何か決まりがあるんですか?」
先生
「それはRustの核心部分ですね!Rustでは、式(Expression)と文(Statement)を明確に区別しています。この違いを理解することが、Rustの構文をマスターする第一歩ですよ。」
生徒
「式と文……。他の言語だとあまり意識しなかった気がしますが、Rustではどうして重要なんですか?」
先生
「Rustは、関数や制御フローが値を返す『式ベース』の言語だからです。セミコロン一つで、その行が『値を返すもの』になるか『単なる命令』になるかが変わるんですよ。具体的な違いを見ていきましょう!」
1. Rustにおける式と文の根本的な違いとは?
Rustプログラミングにおいて、最も基本的でありながら非常に重要な概念が式(Expression)と文(Statement)の使い分けです。多くのプログラミング言語ではこれらを曖昧に扱えることもありますが、Rustは非常に厳格です。
結論から言うと、式は「値を返すもの」であり、文は「値を返さず、何らかの動作を行うもの」です。この定義を頭に叩き込むだけで、Rustのコンパイルエラーの多くを回避できるようになります。Rustの公式ドキュメントでも強調されている通り、Rustは「式ベース(expression-based)」の言語です。これは、プログラムの大部分が値を計算し、それを次の処理に渡すという流れで構成されていることを意味します。
例えば、数学の計算式 1 + 2 は「3」という値を生成するため「式」です。一方で、「変数に値を代入する」という行為そのものは、特定の値をどこかへ返すわけではなく「代入した」という事実で完結するため、Rustでは「文」として扱われます。この細かな違いが、Rust特有の柔軟なコーディングスタイルを支えているのです。
2. 値を生成する式の特徴と具体例
Rustにおける式(Expression)は、評価されると結果として値を生成します。初心者の方が驚くのは、Rustではほとんどのものが式として扱える点です。数値の加算や比較だけでなく、後述する if ブロックや match 式、さらには {} で囲まれたスコープ自体も式になります。
式にはセミコロンを付けないのが基本です。もし式の末尾にセミコロンを付けてしまうと、それは値を返さない「文」へと変化してしまいます。この性質を利用して、Rustでは関数の最後に書かれた式の値を、そのまま関数の戻り値として扱うことができます。これは return キーワードを省略できる理由でもあります。
以下のコードを見て、どのようなものが式に該当するか確認してみましょう。
fn main() {
// 5 + 10 は式であり、15という値を生成します
let sum = 5 + 10;
// {} ブロック自体も式になります
let y = {
let x = 3;
x + 1 // ここにセミコロンがないので、x + 1 の結果が y に代入される
};
println!("sumの値は: {}", sum);
println!("yの値は: {}", y);
}
sumの値は: 15
yの値は: 4
この例では、変数 y の代入時にブロック {} を使っています。ブロック内の最後の行 x + 1 にセミコロンがないため、この計算結果がブロック全体の評価値となり、変数 y に格納されるのです。もしここで x + 1; と書いてしまうと、何も返さない状態になり、エラーの原因となります。
3. 動作を完結させる文の役割
一方で、文(Statement)は命令を実行するだけで、値を返しません。Rustで主に使われる文は二種類あります。一つは「宣言文(Declaration statements)」で、もう一つは「式文(Expression statements)」です。
宣言文の代表例は let キーワードを使った変数の定義です。let x = 5; という記述は、変数 x を作成して値を束縛するという「手続き」を行っていますが、この一行自体が何か値をどこかへ返しているわけではありません。そのため、let a = (let b = 5); のような記述はRustでは許可されません。他の言語(C言語など)では代入式が値を返すことがありますが、Rustでは安全性の観点からこのような書き方を禁止しています。
式文は、式の末尾にセミコロン ; を付けることで、その式の戻り値を破棄し、文へと変換したものです。例えば、関数の途中で計算を行っても、その結果を次に使わない場合はセミコロンを付けて文にします。これにより、メモリ管理やプログラムの意図が明確になります。
4. セミコロンの有無がプログラムに与える影響
Rust初心者が最も躓きやすいのが、関数の戻り値におけるセミコロンの有無です。Rustでは、関数の最後の行にセミコロンがない場合、その行の式の値が自動的に関数の戻り値となります。しかし、誤ってセミコロンを付けてしまうと、その式は「文」になり、戻り値は空のタプル ()(ユニット型と呼ばれます)になってしまいます。
この挙動を正しく理解していないと、「型が一致しません」というコンパイルエラーに悩まされることになります。例えば、整数 i32 型を返すはずの関数で、最後の行にセミコロンを付けてしまうと、コンパイラは「あなたは () を返そうとしていますが、期待されているのは i32 です」と警告を出します。これは、セミコロンが単なる区切り記号ではなく、意味を持つ「演算子」のような役割を果たしているからです。
fn add_numbers(a: i32, b: i32) -> i32 {
a + b // セミコロンがないので、計算結果が返される
}
fn main() {
let result = add_numbers(10, 20);
println!("計算結果: {}", result);
}
計算結果: 30
上記のプログラムは正しく動作します。もし a + b; と記述した場合、関数 add_numbers は何も返さないとみなされ、コンパイルエラーが発生します。この厳密さが、Rustの堅牢なソフトウェア開発を支える柱となっているのです。
5. 制御フローも式!ifやmatchの活用術
Rustの非常に強力な特徴の一つは、if や match といった制御フロー構造も「式」であるという点です。つまり、条件分岐の結果を直接変数に代入したり、関数の戻り値として利用したりできます。これにより、他の言語で使われる「三項演算子(? :)」のような構文を、より読みやすい形で実現できます。
if を式として使う場合、if ブロックと else ブロックが返す値の型は必ず一致していなければなりません。一方は数値を返し、もう一方は文字列を返すといったことはできません。これはRustが静的型付け言語であり、コンパイル時であらゆる変数の型が確定していなければならないからです。
fn main() {
let condition = true;
// if文を式として使い、結果を変数に代入する
let number = if condition {
5
} else {
6
// ここで 6; と書くとエラーになる
};
println!("numberの値は: {}", number);
}
numberの値は: 5
この書き方を活用することで、変数を一度 mut(可変)で宣言してから if 文の中で値を書き換える、といった冗長な記述を避けることができます。不変な変数を推奨するRustの哲学に非常にマッチした機能と言えるでしょう。コードがシンプルになり、バグの混入を防ぐ効果もあります。
6. スコープブロックを利用した値の計算
Rustでは {} で囲まれた任意のスコープ(ブロック)を式として扱うことができます。これは複雑な初期化処理を行いたい時に非常に便利です。特定の変数を計算するために一時的な変数が必要な場合、それらをスコープ内に閉じ込めることで、メインのスコープを汚さずに済みます。
ブロック式の最後には、そのブロックが評価される値を記述します。ここでもルールは同じで、値を返したい場合はセミコロンを付けません。この仕組みを理解すると、Rustのコードが非常に構造化され、各変数の生存期間(ライフタイム)も意識しやすくなります。
fn main() {
let outer_value = 100;
let computed_value = {
let temporary_multiplier = 2;
// outer_value を利用して計算し、結果を返す
outer_value * temporary_multiplier
};
// temporary_multiplier はここでは使えない(スコープ外のため)
println!("計算後の値: {}", computed_value);
}
計算後の値: 200
このように、ブロックを使うことで「この変数はこの計算のためだけに使う」という意図を明確にできます。可読性が向上するだけでなく、誤って一時的な変数を別の場所で使ってしまうミスも防げます。Rustが「安全でクリーンなコード」を書くための仕組みをいかに重視しているかが分かりますね。
7. 式ベースの言語がもたらすメリット
Rustが式ベースの設計を採用している最大の理由は、コードの意図を明確にし、副作用を最小限に抑えるためです。多くの命令型言語では、文を組み合わせて状態を変化させていくことでプログラムを進めます。しかし、これではどこで何が変わったのかを追うのが大変になります。対して「式」を主体にすると、データがどのように変換されていくのかという「流れ」を追いやすくなります。
また、式を多用することで、変数を immutable(不変)のまま保ちやすくなります。初期化の段階で if 式やブロック式を使って値を決定してしまえば、後からその変数を書き換える必要がなくなるからです。これは、並行処理やメモリ安全性を重視するRustにとって、極めて重要なメリットとなります。データ競合のリスクを減らし、予測可能なプログラムを作成するための工夫が、この「式と文」の区別には込められているのです。
演算子一つをとっても、それが式の一部としてどのように評価されるかを考える癖をつけると、Rustの習得スピードは格段に上がります。算術演算子 +, -, *, / や論理演算子 &&, || もすべて式を構成する要素です。これらが組み合わさって一つの値を形作り、最終的にプログラムのロジックを構成していく様は、非常に美しいシステムと言えるでしょう。
8. ユニット型と空の戻り値について
最後に、値を返さない式や文が何を返しているのかについて触れておきます。Rustでは、一見何も返していないように見える関数や、セミコロンで終わる文も、実は ()(ユニット型)という値を返しています。これは「値がないことを表す値」のようなものです。
例えば、println! マクロ自体も式ですが、その戻り値は () です。そのため、関数の最後に println!("Hello"); と書くと、その関数はユニット型を返していることになります。戻り値の型を指定していない関数は、デフォルトで -> () が指定されているとみなされます。この一貫性こそがRustの魅力です。すべてを型システムの中に組み込むことで、例外的な挙動を排除し、コンパイラが完璧にチェックを行えるようにしているのです。
初心者のうちは、「セミコロンを忘れてエラーが出た」あるいは「セミコロンを付けてしまってエラーが出た」という経験を何度もするでしょう。その時は、エラーメッセージをじっくり読んでみてください。Rustのコンパイラは非常に親切で、「ここにセミコロンを付けるか、あるいは外すべきです」と具体的に教えてくれることが多いです。それは、コンパイラがあなたのコードの「式」と「文」の整合性を一生懸命チェックしてくれている証拠なのです。