はじめに
実装していると、こんな議論にぶつかることがあります。
- returnが多いと読みにくいのでは?
- ifをネストするより早期リターンの方が良い?
- コーディング規約的にどうなの?
この記事では早期リターンについて整理します。
早期リターン(Early Return)
早期リターンとは、条件を満たさない場合に処理を続けず、早めに return する書き方です。
public int Calc(int x)
{
if (x < 0)
{
return 0;
}
return x * 2;
}
特徴は以下です。
- 異常系・前提条件を先に処理
- 正常系を一直線で書ける
- ネストを減らせる
早期リターンのメリット
1. 可読性が大きく向上する
ネストが深い例
if (user != null)
{
if (user.IsEnabled)
{
if (user.Age >= 20)
{
Register(user);
}
}
}
早期リターンを使った例
if (user == null) return;
if (!user.IsEnabled) return;
if (user.Age < 20) return;
Register(user);
- 「この処理が実行される条件」が一瞬で分かる
2. ガード節(Guard Clause)
public void Register(User user)
{
if (user == null)
throw new ArgumentNullException(nameof(user));
if (!user.IsActive)
return;
// 正常系
}
- 前提条件が明確になる
- 仕様がコードとして残る
- テスト観点も作りやすい
3. 修正時の事故を減らせる
追加仕様:20歳未満は処理をしない(20歳未満の場合はログを出力する)
変更前
public void Process(User user)
{
if (user != null)
{
if (user.IsActive)
{
Execute(user);
}
}
}
変更後(if / else の対応ミス)
public void Process(User user)
{
if (user != null)
{
if (user.IsActive)
{
if (user.Age >= 20)
{
Execute(user);
}
}
else
{
Log("Too young");
}
}
}
早期リターンを使っていると…
変更前(早期リターン)
if (user == null)
{
return;
}
if (!user.IsActive)
{
return;
}
Execute(user);
変更後(早期リターン)
if (user == null)
{
return;
}
if (!user.IsActive)
{
return;
}
if (user.Age < 20)
{
Log("Too young");
return;
}
Execute(user);
ネストが深いコードは、
- if / else の対応ミス
- 条件追加時の影響範囲拡大
が起きがちです。
早期リターンは「条件ごとに独立した行」になるため、変更時の影響範囲が小さくなります。
早期リターンの注意点
1. 後処理・リソース解放に注意
Disposeされない
public void ProcessFile(string path)
{
var reader = new StreamReader(path);
if (reader.BaseStream.Length == 0) return;
var text = reader.ReadToEnd();
Console.WriteLine(text);
reader.Dispose();
}
- return に到達すると Dispose() が呼ばれない
対策
対策①:using を使う
public void ProcessFile(string path)
{
using (var reader = new StreamReader(path))
{
if (reader.BaseStream.Length == 0) return;
var text = reader.ReadToEnd();
Console.WriteLine(text);
}
}
対策②:try / finally を使う
public void ProcessFile(string path)
{
StreamReader reader = null;
try
{
reader = new StreamReader(path);
if (reader.BaseStream.Length == 0) return;
var text = reader.ReadToEnd();
Console.WriteLine(text);
}
finally
{
reader?.Dispose();
}
}
2. ログや共通処理が抜けやすい
終了ログが残らない
public IActionResult UpdateUser(User user)
{
_logger.LogInformation("User update requested");
if (user == null) return BadRequest();
if (!ModelState.IsValid) return BadRequest(ModelState);
// 更新処理
Update(user);
_logger.LogInformation("User update completed");
return Ok();
}
- 早期リターンの時、終了ログが残らない
対策
対策:終了ログを finally に集約
public IActionResult UpdateUser(User user)
{
_logger.LogInformation("User update requested");
try
{
if (user == null) return BadRequest();
if (!ModelState.IsValid) return BadRequest(ModelState);
Update(user);
return Ok();
}
finally
{
_logger.LogInformation("User update completed");
}
}
まとめ
早期リターンが向いているケース
- 引数チェック
- 権限チェック
- nullチェック
- 異常系処理
- 正常系が一本
早期リターンが向いていないケース
- 複雑な状態遷移 ⇒ 流れが追いづらい
- 後処理が多い ⇒ return 抜け事故
- ビジネスルール分岐 ⇒ if/else の方が明確
実務でおすすめのルール
- 異常系・前提条件は早期リターン
- 正常系はネストさせない
- 後処理がある場合は try-finally を使う
おわりに
早期リターンは可読性を高め、異常系や前提条件を明確にする強力なテクニックです。
その意味では、間違いなく「正義」です。
一方で、後処理や共通処理を考慮せずに多用すると、リソースリークやログ欠落といった事故を招きます。
その瞬間、早期リターンは「悪」になります。
チーム開発において重要なのは早期リターンを「禁止すること」でも「無条件に推奨すること」でもなく、どこで使うかをルールとして共有することです。
早期リターンは万能ではない。
だが、使わない理由を探す時間ほど無駄なものはない。
正しく使えば早期リターンはコードを守る「正義の味方」になります。