.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 或是其他方式,端看整体情境的需求。