Chapter 4 - Item 30 : Prefer Query Syntax Loops

Effective C# (Covers C# 6.0), (includes Content Update Program): 50 Specific Ways to Improve Your C#, 3rd Edition By Bill Wagner 讀後心得

Linq 加入 C# 特性後,繞行查詢有以下三種方式。

1. 傳統的 for, while, do/while, foreach 表達式。

2. query 語法。

3. Method Call 擴充方法。

Note:實務上應盡量採用 query 方式,讓意圖更加明確;而 Method Call 的方式只有在 query 無法滿足需求時採用(Take, TakeWhile, Skip, SkipWhile, Min, Max, etc.)。

範例:

var foo = new int[ 100 ];

for ( int num = 0; num < foo.Length; num++ )
    foo[ num ] = num * num;

foreach ( int i in foo )
    Console.WriteLine( i.ToString( ) );

這是一個很簡單的例子,試著將其用 query 的方式改寫。

var foo = ( from n in Enumerable.Range( 0, 100 )
            select n * n ).ToArray( );

foo.forAll( n => Console.WriteLine( n.ToString( ) ) );

public static void forAll<T>( this IEnumerable<T> sequence,
    Action<T> action )
{
    foreach ( T item in sequence )
        action( item );
}

如此寫法是否有比較容易閱讀呢?

接著考慮比較複雜的情境。

private static IEnumerable<ValueTuple<int, int>> produceIndices( )
{
    for ( int x = 0; x < 100; x++ )
        for ( int y = 0; y < 100; y++ )
            yield return (x, y);
}

用巢狀的迴圈開始讓方法變得複雜且漸漸難以閱讀。

同樣的,用 query 的方式改寫。

private static IEnumerable<ValueTuple<int, int>> queryIndices( )
{
    return from x in Enumerable.Range( 0, 100 )
            from y in Enumerable.Range( 0, 100 )
            select (x, y);
}

這樣可能還沒甚麼感覺,把範例再變的複雜一點。

private static IEnumerable<ValueTuple<int, int>> produceIndices2( )
{
    for ( int x = 0; x < 100; x++ )
        for ( int y = 0; y < 100; y++ )
            if ( x + y < 100 )
                yield return (x, y);
}

兩層的巢狀迴圈加上最內層的 if 判斷式,難以一眼看出意圖。
接著用 query 的方式改寫。

private static IEnumerable<ValueTuple<int, int>> queryIndices2( )
{
    return from x in Enumerable.Range( 0, 100 )
            from y in Enumerable.Range( 0, 100 )
            where x + y < 100
            select (x, y);
}

加入了 where 語法,一眼即可看出意圖且較為簡潔。

目前為止,使用巢狀迴圈或 query 語法皆沒有太大的差異,即便巢狀迴圈較難以一眼看出意圖,但尚可接受。接下來的範例會拉開兩者可讀性的差異。

private static IEnumerable<ValueTuple<int, int>> produceIndices3( )
{
    var storage = new List<ValueTuple<int, int>>( );

    for ( int x = 0; x < 100; x++ )
        for ( int y = 0; y < 100; y++ )
            if ( x + y < 100 )
                storage.Add( (x, y) );

    storage.Sort( ( point1, point2 ) =>
            ( point2.Item1 * point2.Item1 + point2.Item2 * point2.Item2 ).CompareTo(
                point1.Item1 * point1.Item1 + point1.Item2 * point1.Item2 ) );

    return storage;
}

首先方法內宣告了一個 storage 儲存結果,接著呼叫排序方法;如果沒特別注意,我們很難發現 Sort 方法內的 CompareTo 是反向的。

用 query 的方式改寫,觀察會不會更容易理解。

private static IEnumerable<ValueTuple<int, int>> queryIndices3( )
{
    return from x in Enumerable.Range( 0, 100 )
            from y in Enumerable.Range( 0, 100 )
            where x + y < 100
            orderby ( x * x + y * y ) descending
            select (x, y);
}

query 寫法的優勢已完全展現,語意清楚明白且沒有多餘的程式碼。

最後,query 寫法也可以寫成 Method Call 擴充方法。

private static IEnumerable<ValueTuple<int, int>> methodIndices3( )
{
    return Enumerable.Range( 0, 100 ).
        SelectMany( x => Enumerable.Range( 0, 100 ),
        ( x, y ) => (x, y) ).
        Where( pt => pt.Item1 + pt.Item2 < 100 ).
        OrderByDescending( pt =>
            pt.Item1 * pt.Item1 + pt.Item2 * pt.Item2 );
}

Method Call 在語意上並沒有比 query 簡潔好懂,除了特殊情況應用(如 Note 所提及),應盡量使用 query。

結論:
1. 使用 query 方式寫出語意清晰的程式碼。

2. 只有在 query 無法滿足需求時,才考慮使用 Method Call 擴充。