← 開發日常

改善迴圈的可讀性(二)

在一些迴圈使用情境,我們比較難使用Aggregation方法來簡化。有一種使用情境是需要透過暫存變數來保存每一輪的狀態,並帶到下一輪計算,此時就會讓代碼變得複雜。

舉個例子

在下面這段Josephus Survivor的Kata例子中,迴圈中會計算每一輪要移除受害者的位置,並把倖存者帶進下一輪迴圈中計算並找出下一輪的受害者。每一輪的迴圈中都需要把上一個受害者的位址紀錄在start裡。

c#
public int Play(int count, int step)
{
    var candidates = Enumerable.Range(1, count).ToList();

    int start = 0;
    while (!HasSurvivor(candidates))
    {
        var victim = FindVictim(step, start, candidates);
        candidates = NextCandidates(candidates, victim);
        start = victim;
    }

    return candidates.First();
}

此時可以發現迴圈中的代碼雖然不多,卻也不好理解。在這個例子中,start不只是最一開始的初始位置,而是迴圈中的每一輪的初始位置,這個資訊必須要來回反覆的閱讀才能夠理解。

在累加數字的例子中,sum也是相似的情境,sum表示在每一輪迴圈中的暫時合計值。但是我們可以透過Aggregation方法讓sum這個暫存變數消失。與sum不同的是,start無法使用Aggregation方法簡化。

雖然無法使用Aggregation方法,但是我們可以使用遞迴來隱藏start。

c#
public int Play(int count, int step)
{
    var candidates = Enumerable.Range(1, count).ToList();

    return FindSurvivor(candidates, 0, step);
}

private int FindSurvivor(List<int> candidates, int start, int step)
{
    if (HasSurvivor(candidates))
    {
        return candidates.First();
    }

    var victim = FindVictim(start, step, candidates.Count());
		var nextCandiates = NextCandidates(candidates, victim);
    return FindSurvivor(nextCandidates, victim, step);
}

當透過遞迴來重構這個例子中,我們可消去迴圈並把start隱藏在參數中,並且只表示初始位址這個意義,讓代碼更容易閱讀。

實際情況...

在實際情況中,這種使用情境並不常見,多數是在演算法的情境下會比較常使用到,例如:在Quick Sort演算法中紀錄pivot、在多個商品與多種打折策略中計算最優惠的折扣組合。


迴圈最常使用到的語法之一,用起來也相當的容易,但是要如何寫得讓人容易理解,就不是一件容易的事情。所幸的是很多語言都支援好用的Aggregation方法,讓我們更容易從把一些常見的迴圈形式簡化,使代碼更專注在其職責,而不是迴圈本身。