9. switch文を回避する
ある変数の値によって処理を分岐させるのにはif-elseの連鎖ではなくswitch文を使うと綺麗に書けます。状態を表すenumをswitch文に入れて処理を分岐させることはよくやります。しかし、良かれと思って書いたこのswitch文もバグの原因になり得ます。
switch式
条件として使っているenumに新しい値が追加された場合を考えます。当然、その新しい値のときの処理もswitch文に追加する必要があります。しかしここで問題なのは、enumの値が増えてもswitch文は何もエラーを吐かないということです。正しく修正できればいいですが、もし修正すべきswitch文を見逃してしまうとバグを生んでしまい、特定も困難になります。
そこで、switch文の代わりにswitch式を使うと安全になります。switch式はswitch文と違い、全てのケースが網羅されていなければエラーを吐きます。更に、breakのし忘れも防ぐことができてお得です。
public enum WeaponType
{
Sword,
Bow,
Staff,
}// ❌ Bad: switch文 (新しい値が追加されてもエラーにならない)
string GetWeaponDescription(WeaponType type)
{
switch (type)
{
case WeaponType.Sword:
return "近距離攻撃に向いた武器";
case WeaponType.Bow:
return "遠距離攻撃に向いた武器";
case WeaponType.Staff:
return "魔法攻撃に向いた武器";
default:
return "不明"; // ← 新しい武器が追加されてもここに落ちるだけ
}
}
// ✅ Good: switch式 (全ケースを網羅しないとコンパイルエラー)
string GetWeaponDescription(WeaponType type) => type switch
{
WeaponType.Sword => "近距離攻撃に向いた武器",
WeaponType.Bow => "遠距離攻撃に向いた武器",
WeaponType.Staff => "魔法攻撃に向いた武器",
// ↑ 新しいWeaponTypeが追加されたら、ここに追加しないとエラーになる
};Strategyパターン
しかし、ある条件によって処理を切り替えたい場面は一箇所だけで済まないことが多いです。そこでそのままswitch文やswitch式を使っていると、同じ条件式のswitchがあらゆる場所に置かれることになります。こうなると、ケースを追加したいときに様々な場所を周って変更を加えなくてはいけません。単に変更するのが大変ですし、もし抜けがあれば大変です。
// ❌ Bad: switchが多くの場所に散らばる
void Attack(WeaponType type)
{
switch (type)
{
case WeaponType.Sword: /* 剣の攻撃処理 */ break;
case WeaponType.Bow: /* 弓の攻撃処理 */ break;
case WeaponType.Staff: /* 杖の攻撃処理 */ break;
}
}
void PlayAttackSound(WeaponType type)
{
switch (type)
{
case WeaponType.Sword: /* 剣の攻撃音 */ break;
case WeaponType.Bow: /* 弓の攻撃音 */ break;
case WeaponType.Staff: /* 杖の攻撃音 */ break;
}
}
// → WeaponTypeに新しい値が追加されたら、全てのswitchを探して修正しなきゃいけない!そこで、インターフェースを使うことで条件分岐を大幅に減らし、ロジックをシンプルにすることができます。各switchの処理をインターフェースで抽象化し、各ケースを表すクラスに実装させます。こうすることで、呼び出す側は相手が何であるかを知る必要がないので、条件分岐をしなくてよくなります。
// 各switchの処理をインターフェースで抽象化
public interface IWeapon
{
void Attack();
void PlayAttackSound();
}
// 各ケースをクラスとして実装
public class Sword : IWeapon
{
public void Attack() { /* 剣の攻撃処理 */ }
public void PlayAttackSound() { /* 剣の攻撃音 */ }
}
public class Bow : IWeapon
{
public void Attack() { /* 弓の攻撃処理 */ }
public void PlayAttackSound() { /* 弓の攻撃音 */ }
}
public class Staff : IWeapon
{
public void Attack() { /* 杖の攻撃処理 */ }
public void PlayAttackSound() { /* 杖の攻撃音 */ }
}そして、辞書によって条件に応じた適切なクラスのインスタンスを取得できるようにします。こうすることで、switch文を使わずに条件に従って機能を取り替えることができるようになりました。これをStrategyパターンといいます。
// 辞書で条件に応じたインスタンスを取得
Dictionary<WeaponType, IWeapon> weapons = new()
{
{ WeaponType.Sword, new Sword() },
{ WeaponType.Bow, new Bow() },
{ WeaponType.Staff, new Staff() },
};
// 呼び出す側はswitch不要! 相手が何であるかを知る必要がない
IWeapon weapon = weapons[currentWeaponType];
weapon.Attack();
weapon.PlayAttackSound();
// → 新しい武器を追加するときは、クラスを作って辞書に追加するだけ!