Rustの文と式の違いを徹底解説!初心者でも迷わないプログラムの流れと基本構文
生徒
「Rustのコードを書いていると、セミコロンを付ける場所と付けない場所があって混乱します。これって何か決まりがあるんですか?」
先生
「それはRustの核心的な概念である文(Statement)と式(Expression)の違いに関係しています。Rustは『式ベース』の言語なので、この区別がとても重要なんです。」
生徒
「式ベースの言語……?他のプログラミング言語とは何が違うんでしょうか?」
先生
「例えば、多くの言語ではif文やブロックはただの処理の塊ですが、Rustではそれらが値を返す『式』として扱えます。この仕組みを知ると、コードが驚くほどスッキリ書けるようになりますよ。一緒に詳しく見ていきましょう!」
1. Rustにおける文と式の根本的な違い
Rustのプログラムを構成する要素は、大きく分けて「文(Statement)」と「式(Expression)」の2種類に分類されます。プログラミング初心者が最初に突き当たる壁の一つが、この両者の使い分けです。特にC言語やJava、Pythonなどの経験がある方ほど、Rust特有の「値を返す仕組み」に驚くことが多いでしょう。
まず、基本的な定義を確認しましょう。文(Statement)とは、何らかの動作を実行するものの、値を返さない命令のことです。一方で、式(Expression)とは、評価されて値を算出する命令のことです。Rustの関数本体は、一連の文が並び、最後に式(任意)が置かれるという構造を持っています。この「値を返すかどうか」というシンプルな違いが、Rustの強力な型推論や安全性を支える土台となっています。
Rustを学習する上で、自分の書いている一行が「何かを行うための指示」なのか、「新しい値を作り出すための計算」なのかを意識することは、エラーを未然に防ぐための第一歩となります。コンパイラが「型が合いません」と怒るとき、その原因の多くは文と式の取り違えにあるからです。
2. 文の種類と役割をマスターする
Rustにおける文には、主に2つのタイプがあります。一つは「宣言文」であり、もう一つは「式文」です。宣言文の代表例は、変数を定義するletキーワードを使った構文です。例えば、let x = 5;というコードは、変数xに5を束縛するという動作を行いますが、この一行自体は値を持っていません。そのため、他の言語のようにlet y = (let x = 5);といった書き方をすることはできません。これはエラーになります。
もう一つの式文は、式の末尾にセミコロン(;)を付けることで、その式の評価結果を捨てて文に変えてしまうものです。通常、数値の「5」は式ですが、「5;」と書くとそれは値を返さない文として扱われます。初心者のうちは「とりあえず行末にはセミコロンを付ける」と覚えがちですが、Rustにおいてはセミコロンの有無がプログラムの論理構造を劇的に変えてしまうため、注意深い観察が必要です。
3. Rustが式ベースの言語と呼ばれる理由
Rustの最大の特徴は、ほとんどすべての構文が「式」であるという点です。数値や文字列といったリテラルはもちろんのこと、関数呼び出し、マクロ呼び出し、そして制御フローであるifやmatch、さらにはスコープを定義する波括弧{}のブロックまでもが式として扱われます。これは、プログラムのあらゆる場所で値を生成し、それを変数に代入したり関数の戻り値として利用したりできることを意味します。
例えば、他の言語では三項演算子(condition ? a : b)を使って条件分岐の結果を変数に入れますが、Rustには三項演算子がありません。なぜなら、ifブロック自体が式であり、値を返すことができるからです。この設計思想により、一時的な変数を用意して値を書き換えるといった無駄な処理が減り、より宣言的で読みやすいコードを書くことが可能になります。コードが式として評価されることで、開発者はデータの流れを直感的に追跡できるようになります。
4. セミコロンの有無で決まる戻り値のルール
Rustにおいて最も重要なルールは、「ブロックの最後の式にセミコロンを付けなければ、それがそのブロックの戻り値になる」という点です。逆に、セミコロンを付けてしまうと、その式は文となり、戻り値は「空」を意味するユニット型()になってしまいます。この挙動を理解するために、具体的なコードを見てみましょう。
fn main() {
let x = 10;
// ブロック({})は式として評価される
let y = {
let x_squared = x * x;
let x_cube = x_squared * x;
// セミコロンがないので、この値がyに代入される
x_cube + x_squared
};
println!("計算結果のyは: {}", y);
}
計算結果のyは: 1100
上記のコードでは、変数yに代入される値はブロック内の最終行で計算された結果です。もしx_cube + x_squaredの後にセミコロンを付けてしまうと、コンパイラは「値を返さない文(ユニット型)」を返そうとしていると判断し、型エラーが発生します。この厳密さが、Rustのメモリ安全性を支える強力な型システムの要なのです。
5. 他言語(C言語やPython)との比較で見える独自性
C言語やJava、JavaScriptなどの多くの命令型言語では、文と式の境界線が明確に引かれています。例えば、if文やfor文はプログラムの制御を行う「文」であり、それ自体が変数に代入可能な値を生成することはありません。値を生成したい場合は、あらかじめ変数を宣言しておき、その変数に対して文の中で代入を繰り返す必要があります。これは、変数が「可変(Mutable)」であることを前提とした書き方です。
一方、Pythonなどは柔軟ですが、やはりブロック全体が値を返すような構造は一般的ではありません。Rustのアプローチは、関数型プログラミング言語の影響を強く受けており、可能な限りデータを「不変(Immutable)」に保ちながら、変換の結果を次へと繋いでいくスタイルを推奨しています。以下のコードは、Rustのif式がいかに便利かを示しています。
fn main() {
let condition = true;
// ifは文ではなく式なので、直接変数に代入できる
let number = if condition {
5
} else {
6
};
println!("選択された数値: {}", number);
}
選択された数値: 5
他言語であれば「if文の外側で変数を定義し、中身を書き換える」という手順を踏みますが、Rustではこのように直接代入できます。これにより、変数を再代入不可能なlet(不変)として宣言できる範囲が広がり、プログラムの堅牢性が向上します。
6. 関数における戻り値の省略記法
Rustの関数定義において、最後に評価された式は自動的にその関数の戻り値となります。そのため、多くのRustプログラマは関数の最終行にreturnキーワードを書きません。もちろんreturnを使って早期リターン(途中で処理を抜けて値を返すこと)を行うことは可能ですが、関数の最後についてはセミコロンを省いた「式」を置くのが慣習的なスタイルです。
// 2つの数値を足して返すシンプルな関数
fn add_numbers(a: i32, b: i32) -> i32 {
// return a + b; と書いても正解だが、Rustでは下記が一般的
a + b
}
fn main() {
let sum = add_numbers(10, 20);
println!("合計は: {}", sum);
}
合計は: 30
このように、returnを省略することでコードが簡潔になり、関数の「目的(どの値を生み出すか)」がより明確になります。初心者がよくやってしまうミスは、この戻り値となるべき行にうっかりセミコロンを付けてしまうことです。セミコロンを付けると「値を返さない」という意味になり、関数の戻り値の型(この場合はi32)と矛盾するため、コンパイラが親切にエラーメッセージを出してくれます。
7. ユニット型 () という特殊な概念の理解
Rustには、何も値を返さないことを表す特別な型として「ユニット型」と呼ばれる()が存在します。他の言語におけるvoidに近い概念ですが、Rustではこれも一つの値として扱われます。例えば、値を返さない関数や、セミコロンで終わる文は、実際にはこのユニット型を返しています。Rustの型システムは完璧主義なので、「何も返さない」という状態も一つの型として厳密に管理するのです。
この考え方を理解しておくと、複雑なエラーメッセージを読み解く助けになります。例えば「expected i32, found ()」というエラーが出た場合、それは「整数(i32)を期待している場所に、セミコロンのせいで何も返さない文(())が置かれているよ」という意味になります。式と文の区別は、単なる書き方のルールの違いではなく、Rustがすべてのデータの行き先を把握するための重要な手がかりなのです。
8. 制御フローと式の組み合わせによる応用
Rustの式ベースの性質を最大限に活用すると、より高度なプログラム構造を組み立てることができます。例えば、ループ処理を行うloop文も、実は値を返すことができます。これは、無限ループからbreakを使って脱出する際、そのループの成果物として値を外に持ち出すことができる仕組みです。
fn main() {
let mut counter = 0;
let result = loop {
counter += 1;
if counter == 10 {
// breakの後に値を置くことで、loopの結果として返せる
break counter * 2;
}
};
println!("ループの結果得られた値: {}", result);
}
ループの結果得られた値: 20
この機能により、特定の条件を満たすまで探索を続け、見つかった値をそのまま変数に格納するという処理が非常にスムーズに書けます。他の言語のように、ループの外側で可変な変数を定義し、ループ内で代入し、さらにループが終わった後にその値を参照するといった、冗長でミスを誘発しやすい構造を避けることができます。Rustが提供するこの「式」という道具を使いこなすことで、あなたのコードはより安全で、意図が明確なものへと進化していくでしょう。
9. 初心者がハマりやすいエラーと回避策
最後に、文と式の区別で初心者が遭遇しやすいトラブルとその対策をまとめます。最も多いのは、先述した通り「if式の各ブランチで型が異なる」というケースです。if式が値を返す以上、ifブロックとelseブロックが返す値の型は必ず一致していなければなりません。一方が数値を返し、もう一方が文字列を返すような構成は、コンパイル時に拒否されます。これは、Rustが静的型付け言語として、実行前にすべてのデータの型を確定させる必要があるためです。
また、セミコロンの付け忘れだけでなく、逆に「不必要なセミコロン」もエラーの元になります。特にマクロの呼び出し(例:println!)の後にセミコロンを付けるかどうか迷うかもしれませんが、マクロも式であるため、ブロックの最後で値を返したい場合にはセミコロンを付けず、単に表示だけを行いたい場合にはセミコロンを付けるという基本に忠実になれば大丈夫です。Rustの強力なコンパイラは、こうしたミスに対しても非常に具体的な修正案を提示してくれるので、エラーメッセージを「対話」として楽しみながら学んでいくのが上達の近道です。