linq 如何合并`Select`和`Where`

q9rjltbz  于 2023-03-27  发布在  其他
关注(0)|答案(2)|浏览(141)

我正在做一些非常简单和直接的事情,我不禁怀疑/希望它能短一点。
为了便于举例,我们假设我有一个IEnumerable<int?>,我想要的是一个只产生非空值的IEnumerable<int>。下面是一些可以工作的代码,但是可以更简洁吗?

public static void Main( string[] args )
{
    int?[] a = { null, 42, null, 5 };
    IEnumerable<int> ints = a //
            .Where( i => i.HasValue ) //
            .Select( i => i!.Value );
    foreach( var i in ints )
        Console.WriteLine( i );
    Console.ReadLine();
}

请注意,如果可以使它更简洁,那么它也会变得更整洁,因为可以避免丑陋的i!
但也要注意,这只是一个例子,所以请不要关注这些是可空的int,它们可以很容易地是一些复杂的对象,可能是派生的,我们试图获得一个枚举,只枚举那些派生程度更高的对象,转换为派生程度更高的类型。
那么,这个问题就是如何在一条语句中做一个Where()Select(),从而在一个步骤中实现转换和过滤,从而利用过滤时所做的工作,减少转换时所需的工作量。

bvuwiixz

bvuwiixz1#

考虑使用LINQ OfType方法。如果你有一个IEnumerable<int?>,那么.OfType<int>()返回IEnumerable<int>并排除null元素。同样,如果你有一个IEnumerable<Base>,那么.OfType<Derived>()返回IEnumerable<Derived>并排除其他类型的对象。
对于比基于类型过滤更复杂的情况,你可以编写自己的扩展方法:

public static IEnumerable<TResult> SelectWhere<TSource, TResult>(
    this IEnumerable<TSource> source, Func<TSource, (bool, TResult)> selector) 
{
    foreach (TSource item in source)
        if (selector(item) is (true, var result))
            yield return result;
}

对于每个输入值,如果应该包含该值,则selector应该返回(true, transformedValue),如果应该排除该值,则(false, default)应该返回。
例如,给定一个数组int?[] a,语句

IEnumerable<int> negatedInts = a
    .Where(i => i.HasValue)
    .Select(i => -i!.Value);

可以这样重写:

IEnumerable<int> negatedInts = a
    .SelectWhere(i => i is int value ? (true, -value) : (false, default));
oaxa6hgo

oaxa6hgo2#

它可以使用LINQ的SelectMany方法一步完成,例如:

IEnumerable<int> ints = a
    .SelectMany(e => e switch
    {
        int i => new[] { i },
        _ => Enumerable.Empty<int>()
    });

这是可行的,因为SelectMany允许我们为输入序列的每个元素创建一个新的元素序列(可能是不同的类型)(然后简单地通过SelectMany连接以产生最终结果)。这里,我们为我们想要包含的每个元素返回一个单元素序列,为我们想要丢弃的每个元素返回空序列。
有趣的是,如果我们深入一点理论,LINQ是monad的一个例子,SelectMany充当monad的bind操作(Haskell中的>>=操作符)。这意味着实际上我们应该能够使用SelectMany方法实现所有LINQ的方法。
注意:上面的实现可能不是最有效的方法,因为我们为每个我们想要包含在结果序列中的元素分配了一个新的单元素数组。事实上,new[] { i }表达式是monad的return运算符,在LINQ中没有对应的方法。我们可以自己轻松实现它(只是为了好玩):

public static class EnumerableExtension
{
    public static IEnumerable<T> Return<T>(this T elem)
    {
        yield return elem;
    }
}

这个Return甚至可以提高我们实现的效率:

IEnumerable<int> ints = a
    .SelectMany(e => e switch
    {
        int i => i.Return(),
        _ => Enumerable.Empty<int>()
    });

或无扩展方法:

IEnumerable<int> ints = a
    .SelectMany(e => e switch
    {
        int i => Enumerable.Empty<int>().Prepend(i),
        _ => Enumerable.Empty<int>()
    });

更新:

在@MichaelLiu评论之后,我运行了一个quick benchmark,它证明了与我的预期相反,new[] { i }在内存和性能方面确实比Return更有效,而原始方法和SelectWhere比我尝试的任何其他方法都更有效(特别是在内存分配方面):
| 方法|平均值|错误|标准差|Gen0|已分配|
| --------------|--------------|--------------|--------------|--------------|--------------|
| 原创|9.462微秒|0.0094 μs|0.0078 μs|0.0153|104 B|
| 选择地点|9.963 μs|0.0486 μs|0.0379 μs|0.0153|104 B|
| 数组|17.547 μs|0.0465微秒|0.0412 μs|五点零九六四|小行星32096 B|
| 返回|31.306 μs|0.0816 μs|0.0681 μs|6.3477|小B|
| 前置|26.489 μs|0.1392 μs|0.1162 μs|8.9417|小行星56096 B|
| 附加|26.555 μs|0.0728 μs|0.0681 μs|8.9417|小行星56096 B|

相关问题