.NET 6 在 Linq 上的新增功能真的很多,這一篇聊一些原有方法的多載新增。
本集提要
- 框架 : .NET 6
- 功能 : FirstOrDefault, LastOrDefault, SingleOrDefault
- 功能 : Zip
- 功能 :ElementAt, ElementAtOrDefault
- 功能 :Take
自訂預設值
以前 FirstOrDefault, LastOrDefault, SingleOrDefault 如果沒找到符合條件的結果就只能回傳該型別的預設值,就拿 FirstOrDefault 來說吧,以前只有兩個多載 ( LastOrDefault, SingleOrDefault 請以此類推):
FirstOrDefault<TSource>(IEnumerable<TSource>, Func<TSource,Boolean>)
FirstOrDefault<TSource>(IEnumerable<TSource>)
.NET 6 之後則多出這兩個,讓使用者可以自己設定想要的預設值:
FirstOrDefault<TSource>(IEnumerable<TSource>, Func<TSource,Boolean>, TSource)
FirstOrDefault<TSource>(IEnumerable<TSource>, TSource)
好像在哪看過類似的玩意對吧?很久以前 Nullable<T> 有個方法 GetValueOrDefault(T) 就是可以設定自己想要的預設值,我猜大概也是從這來的靈感。不過有另外一個事情讓我很疑惑, FirstOrDefault, LastOrDefault, SingleOrDefault 都多加了這類可以自訂回傳預設值的多載,但 ElementAtOrDefault 卻沒有!?
以後要設定回傳的預設值就可以這樣寫:
var source = Enumerable.Range(1, 10);
var first = source.FirstOrDefault(x => x > 10, int.MinValue);
var last = source.LastOrDefault(x => x > 10, int.MinValue);
var single = source.SingleOrDefault(x => x == 11, int.MinValue);
Console.WriteLine($"First: {first}, Last: {last}, Single: {single}");
Zip
過去 Zip 只能傳入兩個序列合併,它在 .NET Framework 4.0 初登場的時候只有一種:
IEnumerable<TResult> Zip<TFirst,TSecond,TResult> (this IEnumerable<TFirst> first, IEnumerable<TSecond> second, Func<TFirst,TSecond,TResult> resultSelector);
這個的回傳方式是靠傳入Func<TFirst,TSecond,TResult> 來組合回傳值,例如
var s1 = new string[] { "A", "B", "C", "D" };
var numbers = new int[] { 1, 2, 3, 4 };
var zip1 = s1.Zip(numbers, (a, b) => $"{a}-{b}");
後來大概是覺得用 ValueTuple 很爽,於是到了 .NET Core 3.0 的時候,出了一個回傳 ValueTuple 的多載,但此時還是合併兩個序列:
IEnumerable<(TFirst First, TSecond Second)> Zip<TFirst,TSecond> (this IEnumerable<TFirst> first, IEnumerable<TSecond> second);
var s1 = new string[] { "A", "B", "C", "D" };
var numbers = new int[] { 1, 2, 3, 4 };
var zip2 = s1.Zip(numbers);
推移到 .NET 6 的時代,又多出一個可以直接組合三個序列的多載:
Enumerable<(TFirst First, TSecond Second, TThird Third)> Zip<TFirst,TSecond,TThird> (this IEnumerable<TFirst> first, IEnumerable<TSecond> second, IEnumerable<TThird> third);
於是我們可以這麼用:
var s1 = new string[] { "A", "B", "C", "D" };
var s2 = new string[] { "甲", "乙", "丙", "丁" };
var numbers = new int[] { 1, 2, 3, 4 };
var zip3 = s1.Zip(numbers, s2);
Index struct in ElementAt
ElementAt 與 ElementAtOrDefault 各別新增一個參數是 Index struct 的多載:
TSource ElementAt<TSource> (this IEnumerable<TSource> source, Index index);
TSource ElementAtOrDefault<TSource> (this IEnumerable<TSource> source, Index index);
這個好處是甚麼呢?就是要從尾端往前數的時候可以比較好寫。來比較一下 before / after
// before .NET 6
var element1b = source.ElementAt(source.Count() - 1);
var element2b = source.ElementAtOrDefault(source.Count() - 2);
// after .NET 6
var element1a = source.ElementAt(^1);
var element2a = source.ElementAtOrDefault(^2);
Range struct in Take
在 .NET 6,Take 引進了一個使用 Range struct 作為參數的多載:
Enumerable.Take<TSource>(IEnumerable<TSource>, Range)
這用途和 Skip(int).Take(int) 效果一樣,我感覺差異是在應用場景。
- 如果需求敘述是以跳過幾筆再取幾筆用 Skip(int).Take(int) 比較適用。
- 如果需求敘述是以取第x筆到第y筆用 Take(Range) 較能彰顯意義。【註1】
列出兩種方式寫法:
// before .NET 6
var take1 = source.Skip(2).Take(3);
// after .NET 6
var take2 = source.Take(2..5);
這篇文章所有的範例請參考這裡。
註1:這樣的需求有許多種解法,比方說有可能會先轉 ReadOnlySpan<T> 呼叫 Slice,也有可能會用 Array.Copy 或是其他方式,端看整體情境的需求。