Linq 新功能 (4) 自訂預設值, Zip 與 Index struct, Range struct

.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) 效果一樣,我感覺差異是在應用場景。

  1. 如果需求敘述是以跳過幾筆再取幾筆用 Skip(int).Take(int) 比較適用。
  2. 如果需求敘述是以取第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 或是其他方式,端看整體情境的需求。