【ゲーム開発のためのC#入門講座・おまけ編】ガード節を使ってネストを減らそう【#1】

9.0_C#おまけ編

ネストの波が押し寄せる!

入れ子にしたif文などで発生するネストは、可読性を向上させるという意味で大変重要です。

/*
    ネストというのはスペースを使ってコードの記述を入れ子にすること。
    例えば下記のコードは二つのif文で成り立っているが、
    ふたつ目のif文は先頭にスペースがあることで、
    どこからどこまでがふたつ目のif文の範囲なのかがわかりやすい!
*/
if (text == "hoge")
{
    if (result == true)
    {
        ……処理……
    }
}
​
// ↓こんな風にネストがないと、条件の範囲が分かり辛いのだ!
if (text == "hoge")
{
if (result == true)
{
……処理……
}
}

一方で、ネストは一定数を超えるとむしろコードが読み辛くなってしまうという問題も抱えています。

少し恥ずかしいですが、実際に過去の自分が実装したコードを例にしてみます。

当時ウディタ(日本語コマンドでプログラミングができるゲーム開発ツール)という開発ツールで、1行30文字などの決められた文字数で自動改行するメッセージ出力機能を作成していた時のこと。

文字の折り返しにちょうど「しょ」などの大文字+小文字の組み合わせが重なってしまうと、「し」が文章の末尾に表示され「ょ」が次の行に折り返されてしまうという問題に気づきました。

例1

他愛もないことでもいいから、とにかくお互いのことを色々と話しまし
ょうと二人は約束しました。

同様に、折り返しにちょうど「、」や「。」が来た時も綺麗な表示にならないことが発覚しました。

例2

そんなこともないと思うのだけれど、と彼は歯切れ悪く言った

そこで、次の文字が「、」「。」や小文字であった場合は、1行の文字数制限を超えたとしてもそのまま出力するという仕様を追加することに。

解決案

↓特定文字の場合は文字数を超えていてもあえて改行せずそのまま出力
そんなこともないと思うのだけれど、と彼は歯切れ悪く言った。

繰り返し言いますが、追加仕様は、

「次の文字が「、」「。」や小文字でなかった場合は改行する」

です。

これをただただ文字通りに実装しようとした結果、C#で再現すると、下記のようになりました。

public void CreateNewLine()
{
    if (character != "、")
    {
        if (character != "。")
        {
            if (character != "ゃ")
            {
                if (character != "ゅ")
                {
                    if (character != "ょ")
                    {
                        if (character != "っ")
                        {
                            if (character != "ぁ")
                            {
                                if (character != "」")
                                {
                                    ...改行処理...
                                }
                            }
                        }
                    }
                }
            }
        }
    }
}

……ネストの波が、押し寄せる……!

これは似たような条件の繰り返しなのでまだ読めますが、内容の異なる条件や比較演算子が混ざってきたらマジでカオスになります。

public void CreateNewLine()
{
    if (character != "、")
    {
        if (character != "。")
        {
            // ↓実はここだけ「==」
            if (character == "ゃ")
            {
                // ↓何か毛色の違う条件
                if (result == true)
                {
                    // ↓何か毛色の違う条件2
                    if (partyName != "data")
                    {
                        if (character != "っ")
                        {
                            if (character != "ぁ")
                            {
                                if (character != "」")
                                {
                                    ...改行処理...
                                }
                            }
                        }
                    }
                    else
                    {
                        // ↓ここだけelse処理
                        ……異なる処理……
                    }
                }
            }
        }
    }
}

このコードを見て「改行処理」が発生する条件、ぱっと説明できますか?

あるいは、「異なる処理」が発生する条件、ぱっと理解できますか?

正直かなり難しいですよね。

そもそも判定と処理を1メソッドで実装しているという点において設計的な問題もありますが、一旦それは置いておくこととして。

この押し寄せるネストの波を防ぐコーディングテクニックがガード節です。

ガード節って?

例えば「条件Aかつ条件Bだったら処理Cを実行する」というメソッドがあったとします。

if (条件A)
{
    if (条件B)
    {
        ……処理C……
    }
}

これは逆説的に言うと、

  1. 条件Aじゃなかったら処理Cは実行しない(対象外)
  2. 条件Bじゃなかったら処理Cを実行しない(対象外)
  3. 1と2以外(=条件AかつB)だったら処理Cを実行する

ということでもあります。

// 1. 条件Aじゃなかったら処理Cは実行しない
if (!条件A)
{
    return;
}
​
// 2. 条件Bじゃなかったら処理Cを実行しない
if (!条件B)
{
    return;
}
​
// 3. 1と2以外(=条件AかつB)だったら処理Cを実行する
……処理C……

このように、絡み合っている前提条件を分解し、個々の条件が処理の対象外だったらその時点で処理を終了してしまおう、という考え方および実装方法のことをガード節といいます。

対象外のデータが流入することをあらかじめ「ガード」する節です。

このガード節を使って先程のメソッド「CreateNewLine」を修正すると下記の通り。

public void CreateNewLine()
{
    if (character == "、")
    {
        return;
    }
    
    if (character == "。")
    {
        return;        
    }
    
    if (character == "ゃ")
    {
        return;            
    }
    
    if (character == "ゅ")
    {
        return;               
    }
    
    if (character == "ょ")
    {
        return;                 
    }
    
    if (character == "っ")
    {
        return;             
    }
    
    if (character == "ぁ")
    {
        return;                            
    }
    
    if (character == "」")
    {
        return;
    }
    
    ...改行処理...
}

どうでしょうか。あれだけ大量にあったネストは綺麗さっぱりなくなっていますね。

また、元のコードだと「AかつBかつCかつ……だったら」と、すべての条件を繋げて考える必要があるため、頭の中でコード内容を整理するのが難しかったかと思います。

// 次の文字が「、」じゃなくて、
if (character != "、")
{
    // 次の文字が「。」でもなくて、
    if (character != "。")
    {
        // 次の文字が「ゃ」でもなくて……
        if (character != "ゃ")
        {

一方、ガード節を使うと「まずAじゃなかったらだめ」「次にBじゃなかったらだめ」「さらにCじゃなかったらだめ」と個々の条件毎に考えを区切ることができるので、内容を整理しやすいというメリットもあります。

// 次の文字が「、」だったら対象外。次!
if (character == "、")
{
    return;
}
    
// 次の文字が「。」だったら対象外。次!
if (character == "。")
{
    return;        
}
    
// 次の文字が「ゃ」だったら対象外。次!
if (character == "ゃ")
{
    return;            
}
​
// と、条件毎に考えを区切ることができるので、情報の整理が楽!

このように、ガード節を使うと視覚的にすっきり見やすく、頭の中も整理しやすいコードにすることができます

もちろん何でもかんでもガード節にすればよいという訳ではありません。

例えばそのメソッドでやりたいこと自体が何らかの条件判定であった場合は、条件を反転させて除外するよりも素直に書いた方がわかりやすいことも多いです。

// ↓これも悪いコードではないけれど……
if (!条件A)
{
    return false;
}
​
if (!条件B)
{
    return false;
}
​
return true;
​
// ↓素直にこう書いた方がわかりやすいことも!
if (条件A && 条件B)
{
    return true;
}
{
    return false;
}

どちらがよいかはそのコード内容によります。

あくまでネストの深さやコードの可読性を改善しうるテクニックとして覚えていただき、皆さんが使いたいと思う場面があれば適用してみるとよいでしょう。

ループ処理にも適用できる

returnで処理を抜けるパターンをご紹介しましたが、ループ処理にも適用できます。

例えば3と10の倍数だけコンソール画面に出力するプログラムを作るとしましょう。

for (int i = 1; i < 100; i++)
{
    if (i % 3 != 0) continue;
    if (i % 10 != 0) continue;
    Console.WriteLine(i);
}

このように条件を満たさない場合はcontinueするなりbreakするなりすることで、事前に対象外となる条件を弾くことができます。

こちらも可読性の向上に繋がるため、必要に応じて利用していくとよいでしょう。

実はif文は1行でも書ける

実はif文は1行で書くことができます

そのため、先程のコードはもっとすっきり書くことができます。

public void CreateNewLine()
{
    if (character == "、") return;
    if (character == "。") return; 
    if (character == "ゃ") return; 
    if (character == "ゅ") return; 
    if (character == "ょ") return; 
    if (character == "っ") return; 
    if (character == "ぁ") return; 
    if (character == "」") return; 
    
    ...改行処理...
}

めちゃくちゃすっきりしましたね!

構文は下記の通り。

if (条件) 条件を満たす場合に実行する1行のみの処理;

複数行に渡る処理が必要な場合はいつも通り「{}」をつける必要があるので、あくまで簡略的記述方法です。

ガード節で使うと非常にすっきり書けて便利なのでオススメです。

一方で、通常の処理でこの1行記述方法と通常記述方法が混在すると、ぱっと見どれが条件式のコードなのか分かり辛くなるというデメリットがあります。

public void DammyMethod()
{
    // ↓混在していると読み辛い……
    if (character == "、") return;
    Console.WriteLine("hoge");
    if (isCompleted) Console.WriteLine("fuga");
    Console.WriteLine("hogehoge");
    if (isCanceled)
    {
        Console.WriteLine("Cancelされました");
        return;
    }
    ...改行処理...
}

便利な機能ではあるので、どのように利用していくか、自分なりのルールを決めていくとよいでしょう。

まとめ

  • ガード節とは、絡み合っている前提条件を分解し、個々の条件が処理の対象外だったらその時点で処理を終了してしまおう、という考え方および実装方法
  • 視覚的にすっきり見やすく、頭の中も整理しやすいコードにすることができる(使わない方が理解しやすいこともあるので、そこはコード内容次第)
  • if文の1行記述方法と合わせて使うとよりすっきり見やすいのでオススメ

おまけ編はこんな感じで、本編では解説できなかったがゲーム開発でも使えそうな小ネタや設計技法をご紹介していこうかなと思っています。

それでは、今回もお疲れ様でした!

また次の記事でお会いしましょう!

Posted by yuumekou