這只是一個純粹為了有趣的練習題,不需要太嚴肅看待。
假設我們有一個資料結構是這樣 (github link)
public enum Gender
{
None, Male, Female
}
/// <summary>
/// immmutable
/// </summary>
public class Person
{
public string Name { get; }
public int Age { get; }
public Gender Gender { get; }
public Person(string name, int age, Gender gender)
{
Name = name;
Age = age;
Gender = gender;
}
}
依據這個資料形式產出一個 List<Person> people,接著依照以下條件查詢:
(1) 輸入:string name, int age, Gender gender,依照前置條件決定後設條件是否為查詢條件
(2) 如果 !string.IsNullOrWhiteSpace (name) 則 people.Where((y) => y.Name.Contains (name))
(3) 如果 age > 0 則 people.Where((y) => y.Age < age)
(4) 如果 gender != Gender.None 則 people.Where((y) => y.Gender == gender)
(1) 如果輸入 ("B", 24 , Gender.Male) 則會找出 people 中 Name 包含 "B" 、Age 小於 24 以及 Gender 為 Gender.Male 者
(2) 如果輸入 (default(string), 0, Gender.Female) 則會找中 people 中所有 Gender 為 Gender.Female 者,不論 Name 與 Age 為何
(3) 如果輸入 (default(string), 0, Gender.None) 則忽略所有條件回傳整個 people
看起來好像很複雜,仔細想想其實整個邏輯滿簡單的。我們以 A 代表前置條件、B 代表後設條件,就能把公式轉換為:( !A1 || B1 ) && ( !A2 || B2 ) && ( !A3 || B3 )
根據這條公式,我們得到第一個解法 (github link),為了方便閱讀,我把所有的 Not 條件全倒過來寫。
例如 ! age > 0 改成 age < 1。
public static class DirectSolution
{
public static IEnumerable<Person> Execute(this IEnumerable<Person> source, string name, int age, Gender gender)
{
return source.Where((x) =>
(string.IsNullOrWhiteSpace(name) || x.Name.Contains(name))
&& (age < 1 || x.Age < age)
&& ((gender == Gender.None) || (x.Gender == gender)));
}
}
這樣解題太無聊了,來做點有趣的事情。假設這樣的模式會一再出現,而且可能會應用在不同型別,上面那種直接的解題方式就會寫得讓人覺得很煩。依照我喜歡搞怪的個性,就來發展一下各式各樣的解題方法。
(1) 聯合條件直覺法:簡單說就是把前面的公式化成一條可以解決同樣形式問題的程式碼,也就是分組為 ( A1,B1 )、 ( A2,B2 ) 與 ( A3,B3 ),若三組條件都是 true,則表示符合條件需回傳。事實上這是我最喜歡的作法,簡單易懂、反璞歸真。(github link)
public static class UnionCondition
{
public static bool NeedExecute<TCondition, TSource>(this IEnumerable<ConditionExpression<TCondition, TSource>> expressions, TCondition condition, TSource source)
{
return expressions.All((y) => !y.Precondition(condition) || y.Postcondition(source));
}
}
(2) 聯合委派法:有點類似 (1),但聯合的是委派本身,而不是執行後的 bool 結果。(github link)
public static class UnionDelegateExtension
{
public static Func<T, bool> CombineExpression<T>(this Func<T, bool> precondition, Func<T, bool> postcondition)
{
if (precondition == null && postcondition == null )
{
throw new ArgumentNullException();
}
if (precondition == null && postcondition != null)
{
return postcondition;
}
if (precondition != null && postcondition == null)
{
return precondition;
}
return (x) => precondition(x) && postcondition(x);
}
}
(3) 聯合委派法 by Aggregate :基本上這是 (2) 的變形,只是改用 Enumerable.Aggregate 替代使用 foreach 語法的結合方式 (github link)
public static class AggregateExtension
{
public static Func<T2, bool> AggregateExpression<T1, T2>(this IEnumerable<ConditionExpression<T1, T2>> expressions, T1 condition)
{
return expressions.Where((x) => x.Precondition(condition)).Select((y) => y.Postcondition).Aggregate((x, y) => (z => x(z) && y(z)));
}
}
(4) 遞迴結合法:利用遞迴運算的方式,一直回傳 Where 查詢後的結果再套入為下一個的資料來源,這方式我也覺得很有趣。(github link)
public static class Recursive
{
public static IEnumerable<T2> Execute<T1, T2>(this IEnumerable<T2> source, List<ConditionExpression<T1, T2>> expressions, T1 condition)
{
if (expressions.Count > 0)
{
source = !expressions[0].Precondition(condition) ? source : source.Where(expressions[0].Postcondition);
expressions.RemoveAt(0);
return source.Execute(expressions, condition);
}
return source;
}
}
(5) 交集法:先取得各組條件的 IEnumerable<T>,最後使用 Enumerable.Intersect 取得所有 IEnumerable<T> 的交集 (github link)
public static class Intersection
{
public static IEnumerable<T2> Execute<T1, T2>(this IEnumerable<T2> source, IEnumerable<ConditionExpression<T1, T2>> expressions, T1 condition)
{
return source.InnerExecute(expressions, condition).Aggregate((x, y) => x.Intersect(y));
}
private static IEnumerable<IEnumerable<T2>> InnerExecute<T1, T2>(this IEnumerable<T2> source, IEnumerable<ConditionExpression<T1, T2>> expressions, T1 condition)
{
return expressions.Where((x) => x.Precondition(condition)).Select((y) => source.Where (y.Postcondition));
}
}
剩下幾個方式比較無趣一點,就不多作介紹了。
(6) 範本方法模式 (github link)
(7) 策略模式 (github link)
(8) 委派型的策略模式 (github link)
就是個單純寫程式的樂趣,如果對這題目有興趣可以 fork LinqForFun,或許我還會想到奇怪的主意也不一定,有趣就好。