如何结合“选择”和“位置”

Mik*_*kis 18 c# linq

假设我有一个IEnumerable<X>,其中 的 一些实例X可以转换为Y,而有些则不能,并且想要一个IEnumerable<Y>, 只包含那些X可以转换为 的 es Y

例如,如果我有一个IEnumerable<int?>同时包含空值和非空值的 ,我可能需要一个IEnumerable<int>仅包含非空值的。

注意:请不要int?过于字面地理解这个例子;该解决方案应该适用于任何Xand ,其中和Y之间的差异不仅仅是类型差异或可空性差异。XY

我可以做到的一种方法如下:

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();
}
Run Code Online (Sandbox Code Playgroud)

虽然上面的代码有效,但我想将Select()and合并Where()到一个语句中。这样做可以避免丑陋的i!. 它还可以一步完成转换和过滤,从而利用过滤期间完成的工作来减少转换期间所需的工作量。

有办法实现吗?

Mic*_*Liu 17

一般案例

您可以编写自己的扩展方法来组合SelectWhere

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;
}
Run Code Online (Sandbox Code Playgroud)

对于每个输入值,如果应包含该值或应排除该值,selector则应返回。(true, transformedValue)(false, default)

举个例子,给定一个数组int?[] a,语句

IEnumerable<int> negatedInts = a
    .Where(i => i.HasValue)
    .Select(i => -i!.Value);
Run Code Online (Sandbox Code Playgroud)

可以这样重写:

IEnumerable<int> negatedInts = a
    .SelectWhere(i => i is int value ? (true, -value) : (false, default));
Run Code Online (Sandbox Code Playgroud)

特殊情况:按类型过滤

如果您只想按类型过滤值而不以其他方式转换值,则可以使用 LINQ OfType方法:

  • 如果有IEnumerable<int?>, 则.OfType<int>()返回IEnumerable<int>并排除空值。
  • 如果您有IEnumerable<Base>, 则.OfType<Derived>()返回IEnumerable<Derived>并排除空引用和其他类型的对象。


Ed'*_*'ka 7

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

\n
IEnumerable<int> ints = a\n    .SelectMany(e => e switch\n    {\n        int i => new[] { i },\n        _ => Enumerable.Empty<int>()\n    });\n
Run Code Online (Sandbox Code Playgroud)\n

这是有效的,因为允许我们为输入序列的每个元素SelectMany创建一个新的元素序列(可能是不同类型) (然后将其简单地连接起来以产生最终结果)。在这里,我们为要包含的每个元素返回一个单元素序列,为要丢弃的每个元素返回空序列。SelectMany

\n

为了好玩,如果我们更深入地研究一下理论,LINQ 是monad的一个例子,它SelectMany服务于 monad 的操作bind>>=Haskell 中的运算符)。这意味着实际上我们应该能够仅使用方法来实现 LINQ 的所有SelectMany方法。

\n

注意:上述实现可能不是最有效的方法,因为我们为要包含在结果序列中的每个元素分配一个新的单元素数组。事实上,该new[] { i }表达式 if monad 的return运算符在 LINQ 中没有相应的方法。不过,我们可以轻松地自己实现它(只是为了好玩):

\n
public static class EnumerableExtension\n{\n    public static IEnumerable<T> Return<T>(this T elem)\n    {\n        yield return elem;\n    }\n}\n
Run Code Online (Sandbox Code Playgroud)\n

Return甚至可能提高我们的实施效率:

\n
IEnumerable<int> ints = a\n    .SelectMany(e => e switch\n    {\n        int i => i.Return(),\n        _ => Enumerable.Empty<int>()\n    });\n
Run Code Online (Sandbox Code Playgroud)\n

或者没有扩展方法:

\n
IEnumerable<int> ints = a\n    .SelectMany(e => e switch\n    {\n        int i => Enumerable.Empty<int>().Prepend(i),\n        _ => Enumerable.Empty<int>()\n    });\n
Run Code Online (Sandbox Code Playgroud)\n

更新:

\n

在 @MichaelLiu评论之后,我运行了一个快速基准测试,结果表明,与我的预期相反,在内存和性能方面new[] { i }确实比我尝试的任何其他方法都更有效(特别是在内存分配方面),而原始方法和方法都更有效:ReturnSelectWhere

\n
\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n \n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n \n\n\n\n\n\n\n\n\n\n\n\n
方法意思是错误标准差0代已分配
原来的9.462\xce\xbcs0.0094 \xce\xbcs0.0078 \xce\xbcs0.0153104乙
选择地点9.963\xce\xbcs0.0486 \xce\xbcs0.0379 \xce\xbcs0.0153104乙
大批17.547\xce\xbcs0.0465 \xce\xbcs0.0412 \xce\xbcs5.096432096乙
返回31.306 \xce\xbcs0.0816 \xce\xbcs0.0681 \xce\xbcs6.347740096乙
前置26.489 \xce\xbcs0.1392 \xce\xbcs0.1162 \xce\xbcs8.941756096乙
附加26.555 \xce\xbcs0.0728 \xce\xbcs0.0681 \xce\xbcs8.941756096乙
\n

  • 请注意,在 C# 编译器的当前实现中,Return 扩展方法在每次调用时分配一个枚举器对象,因此它不一定比简单地编写“new[] { i }”更有效。 (2认同)