【筆記】使用 Enumerable.​Aggregate 方法彙總委派/運算式樹狀架構。

  • 434
  • 0
  • 2018-12-15

  那一天,自己終於想起了泛型 + 委派 + LINQ 的恐怖……。

  小時候在看 91 哥的《[.NET]快快樂樂學LINQ系列-Aggregate() 簡介[1]》文章時,還是懵懵懂懂不太知道如何運用,直到那一天,自己終於想起了泛型 + 委派 + LINQ 的恐怖……。

  有時候我們會需要組合布林值條件判斷式運用在集合的查詢條件上,例如:Enumerable.Where(this IEnumerable source, Func predicate)[2] 的方法,須要傳入一個委派來篩選集合;自己知道有幾個方法可以達成,像是透過 if-else 的條件判斷式來一步一步的篩選集合,或是使用巢狀的 if-else 的條件判斷式來篩選最後要的集合,但總覺得這樣做會有壞味道……。

  下列透過變形的責任鏈來降低條件判斷式的循環複雜度,並使用 Enumerable.​Aggregate(this IEnumerable source, Func func)[3] 方法彙總委派/運算式樹狀架構,最後再將組合好的布林值條件判斷委派傳入 Enumerable.Where 方法中進行集合的篩選。

  範例情境是:使用者會傳入一個帶有 FlagsAttribute[4] 的整數列舉要在由 1 至 20 連續整數的集合中進行複合條件的篩選,例如:使用者要篩選出集合中大於 10 且為奇數的整數。如下所示:


var filterType = FilterType.Odd | FilterType.GreaterThanTen;

  範例程式裡也將條件判式收進一個內含委派鍵/值組的集合來做到變形的責任鏈以降低循環複雜度,而運算式樹狀架構也是相同的做法,差別的就是鍵/值組的值是運算式樹狀架構,如下所示:


/// <summary>
/// 委派篩選條件。
/// </summary>
private static IEnumerable<KeyValuePair<Func<FilterType, bool>, Func<int, bool>>>
    _funcConditions =
    new List<KeyValuePair<Func<FilterType, bool>, Func<int, bool>>>(4)
    {
        new KeyValuePair<Func<FilterType, bool>, Func<int, bool>>
        (
            filter => (filter & FilterType.Odd) == FilterType.Odd,
            number => number % 2 != 0
        ),
 
        new KeyValuePair<Func<FilterType, bool>, Func<int, bool>>
        (
            filter => (filter & FilterType.Even) == FilterType.Even,
            number => number % 2 == 0
        ),
 
        new KeyValuePair<Func<FilterType, bool>, Func<int, bool>>
        (
            filter => (filter & FilterType.GreaterThanTen) == FilterType.GreaterThanTen,
            number => number > 10
        ),
 
        new KeyValuePair<Func<FilterType, bool>, Func<int, bool>>
        (
            filter => (filter & FilterType.LessThanTen) == FilterType.LessThanTen,
            number => number < 10
        )
    };
 
/// <summary>
/// 運算式樹狀架構篩選條件。
/// </summary>
private static IEnumerable<KeyValuePair<Func<FilterType, bool>, Expression<Func<int, bool>>>>
    _expressionConditions =
    new List<KeyValuePair<Func<FilterType, bool>, Expression<Func<int, bool>>>>(4)
    {
        new KeyValuePair<Func<FilterType, bool>, Expression<Func<int, bool>>>
        (
            filter => (filter & FilterType.Odd) == FilterType.Odd,
            number => number % 2 != 0
        ),
 
        new KeyValuePair<Func<FilterType, bool>, Expression<Func<int, bool>>>
        (
            filter => (filter & FilterType.Even) == FilterType.Even,
            number => number % 2 == 0
        ),
 
        new KeyValuePair<Func<FilterType, bool>, Expression<Func<int, bool>>>
        (
            filter => (filter & FilterType.GreaterThanTen) == FilterType.GreaterThanTen,
            number => number > 10
        ),
 
        new KeyValuePair<Func<FilterType, bool>, Expression<Func<int, bool>>>
        (
            filter => (filter & FilterType.LessThanTen) == FilterType.LessThanTen,
            number => number < 10
        )
    };

  在範例程式裡有建立 IAggregateDelegateExampleService 定義彙總委派範例服務方法的介面,其中定義 GetFilterDelegatePredicate 取得委派篩選判斷條件與 GetFilterExpressionPredicate 取得運算式樹狀架構篩選判斷條件的方法,並透過 ExampleServiceByCollection 依照集合實作範例服務類別與 ExampleServiceByDelegate 依照委派實作範例服務類別以不同的方法來實作介面定義的方法。

  首先說明的是 ExampleServiceByCollection 類別使用集合實作的方法,一開始會在方法中產生一個空集合的 predicates 區域變數,再逐一列舉傳進方法的委派篩選條件,當鍵/值組的鍵回傳 true 時就將值加入 predicates 中,再針對 predicates 集合使用 Aggregate 方法將目前從集合中取出的元素與下一個元素做 AND(&&)運算進行彙總,最後再將彙總後的委派回傳,而在這個範例情境中是使用 AND(&&)運算子來彙總,可依照不同的情境來彙總。在對運算式樹狀架構彙總布林值條件判斷式前須要先執行 Expression.Compile[5] 編譯方法,編譯成 TDelegate 委派型別可執行的 Lambda 運算式再進行彙總,如下所示:


/// <summary>
/// 取得委派的篩選判斷條件。
/// </summary>
/// <param name="conditions">條件集合。</param>
/// <param name="filterType">篩選類型。</param>
/// <returns>
/// 委派的篩選判斷條件。
/// </returns>
public Func<int, bool> GetFilterDelegatePredicate(
    IEnumerable<KeyValuePair<Func<FilterType, bool>, Func<int, bool>>> conditions,
    FilterType filterType)
{
    var predicates = new List<Func<int, bool>>();
 
    foreach (var condition in conditions)
    {
        if (condition.Key(filterType))
        {
            predicates.Add(condition.Value);
        }
    }
 
    if (predicates.Any() == false)
    {
        return number => true;
    }
 
    var predicate = predicates.Aggregate((current, next) =>
                        number => current(number) && next(number));
 
    return predicate;
}
 
/// <summary>
/// 取得運算式樹狀架構的篩選判斷條件。
/// </summary>
/// <param name="conditions">條件集合。</param>
/// <param name="filterType">篩選類型。</param>
/// <returns>
/// 運算式樹狀架構的篩選判斷條件。
/// </returns>
public Expression<Func<int, bool>> GetFilterExpressionPredicate(
    IEnumerable<KeyValuePair<Func<FilterType, bool>, Expression<Func<int, bool>>>> conditions,
    FilterType filterType)
{
    var predicates = new List<Expression<Func<int, bool>>>();
 
    foreach (var condition in conditions)
    {
        if (condition.Key(filterType))
        {
            predicates.Add(condition.Value);
        }
    }
 
    if (predicates.Any() == false)
    {
        return number => true;
    }
 
    var predicate = predicates.Aggregate((current, next) =>
                        number => current.Compile()(number) && next.Compile()(number));
 
    return predicate;
}

  另一 ExampleServiceByDelegate 類別的實作則是透過多點傳送委派的引動過程清單來取得委派的集合,其中將符合篩選條件的委派加入至多點傳送委派引動過程清單是使用 += 加法指派運算子來操作,而在進行彙總前先由委派取得引動過程清單再轉型回原本的 Func 委派型別後其餘的部分就都相同了,如下所示。附帶一提組合委派(多點傳送委派)[6]可以使用 + 加法與 - 減法運算子進行操作。


/// <summary>
/// 取得委派的篩選判斷條件。
/// </summary>
/// <param name="conditions">條件集合。</param>
/// <param name="filterType">篩選類型。</param>
/// <returns>
/// 委派的篩選判斷條件。
/// </returns>
public Func<int, bool> GetFilterDelegatePredicate(
    IEnumerable<KeyValuePair<Func<FilterType, bool>, Func<int, bool>>> conditions,
    FilterType filterType)
{
    Func<int, bool> func = null;
 
    foreach (var condition in conditions)
    {
        if (condition.Key(filterType))
        {
            func += condition.Value;
        }
    }
 
    if (func == null)
    {
        return number => true;
    }
 
    var predicate = func.GetInvocationList().Cast<Func<int, bool>>()
        .Aggregate((current, next) =>
            number => current(number) && next(number));
 
    return predicate;
}
 
/// <summary>
/// 取得運算式樹狀架構的篩選判斷條件。
/// </summary>
/// <param name="conditions">條件集合。</param>
/// <param name="filterType">篩選類型。</param>
/// <returns>
/// 運算式樹狀架構的篩選判斷條件。
/// </returns>
public Expression<Func<int, bool>> GetFilterExpressionPredicate(
    IEnumerable<KeyValuePair<Func<FilterType, bool>, Expression<Func<int, bool>>>> conditions,
    FilterType filterType)
{
    Func<int, bool> func = null;
 
    foreach (var condition in conditions)
    {
        if (condition.Key(filterType))
        {
            func += condition.Value.Compile();
        }
    }
 
    if (func == null)
    {
        return number => true;
    }
 
    var predicate = func.GetInvocationList().Cast<Func<int, bool>>()
        .Aggregate((current, next) =>
            number => current(number) && next(number));
 
    Expression<Func<int, bool>> result = number => predicate(number);
    return result;
}

  最後,簡單示範如何使用這個些方法。先定義好要篩選的類型再分別取得四個不同方式實作的布林值條件判斷式,再對目標集合進行篩選即可,當然,若是運算式樹狀架構型別則須要先呼叫編譯的方法再傳入 Enumerable.Where 方法中,如下所示:


/// <summary>
/// 主程式。
/// </summary>
/// <param name="args">參數集合。</param>
private static void Main(string[] args)
{
    var filterType = FilterType.Odd | FilterType.GreaterThanTen;
 
    var exampleServiceByCollection = new ExampleServiceByCollection();
    var funcPredicateByCollection =
        exampleServiceByCollection.GetFilterDelegatePredicate(_funcConditions, filterType);
    var expressionPredicateByCollection =
        exampleServiceByCollection.GetFilterExpressionPredicate(_expressionConditions, filterType);
 
    var exampleServiceByDelegate = new ExampleServiceByDelegate();
    var funcPredicateByDelegate =
        exampleServiceByDelegate.GetFilterDelegatePredicate(_funcConditions, filterType);
    var expressionPredicateByDelegate =
        exampleServiceByDelegate.GetFilterExpressionPredicate(_expressionConditions, filterType);
 
    var source = Enumerable.Range(1, 20);
 
    Console.WriteLine("Func Filter Predicate By Collection:");
    var funcResultByCollection = source.Where(funcPredicateByCollection);
    PrintOut(funcResultByCollection);
 
    Console.WriteLine("Func Filter Predicate By Delegate:");
    var funcResultByDelegate = source.Where(funcPredicateByDelegate);
    PrintOut(funcResultByDelegate);
 
    Console.WriteLine("====================");
 
    Console.WriteLine("Expression Filter Predicate By Collection:");
    var expressionResultByCollection = source.Where(expressionPredicateByCollection.Compile());
    PrintOut(expressionResultByCollection);
 
    Console.WriteLine("Expression Filter Predicate By Delegate:");
    var expressionResultByDelegate = source.Where(expressionPredicateByDelegate.Compile());
    PrintOut(expressionResultByDelegate);
}

  輸出的結果如下所示:


Func Filter Predicate By Collection:
11
13
15
17
19
Func Filter Predicate By Delegate:
11
13
15
17
19
====================
Expression Filter Predicate By Collection:
11
13
15
17
19
Expression Filter Predicate By Delegate:
11
13
15
17
19

  完整的範例程式碼皆推送至個人的 GitHub 中 AggregateDelegateExample 的儲存庫[7],其中也包含範例服務類別方法的單元測試,歡迎有興趣的朋友參考與指教,非常感謝!

參考資料:

  1. [.NET]快快樂樂學LINQ系列-Aggregate() 簡介 | In 91 - 點部落
  2. Enumerable.Where<​TSource>(​IEnumerable<​TSource>, ​Func<​TSource,​Int32,​Boolean>) | Microsoft Docs
  3. Enumerable.​Aggregate(this IEnumerable source, Func func) | Microsoft Docs
  4. FlagsAttribute Class | Microsoft Docs
  5. Expression.Compile | Microsoft Docs
  6. 如何:組合委派 (多點傳送委派) (C# 程式設計手冊) | Microsoft Docs
  7. GitHub - Zhi-Wei/AggregateDelegateExample: 使用 Enumerable.​Aggregate 方法彙總委派/運算式樹狀架構的範例程式碼。

創用 CC 授權條款 本著作由Zhi-Wei製作,以創用CC 姓名標示-非商業性-相同方式分享 4.0 國際 授權條款釋出。