How to combine `Select` and `Where`

How to combine `Select` and `Where`

7

I am doing something very simple and straightforward, and I cannot help but suspect / wish that it could be made a bit shorter.

For the sake of the example, let us suppose that I have an IEnumerable<int?> and all I want is an IEnumerable<int> yielding only the non-null values. Here is some code that works, but can it be made more terse?

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();
}

Note that if it could be made more terse, then it would also be made more neat, because that ugly i! could be avoided.

But also note that this is just an example, so please do not focus on the fact that these are nullable ints. They could just as easily be some complex objects which might be derived, and we are trying to obtain an enumeration of only those that are more derived, converted to the more derived type.

So, this question is how to do a Where() and Select() in one statement, so as to achieve conversion and filtering in one step, thus taking advantage of work done during filtering, to lessen the amount of work necessary during conversion.

Share
Improve this question

2 Answers
2

Reset to default

Highest score (default)

Trending (recent votes count more)

Date modified (newest first)

Date created (oldest first)

6

If you have an IEnumerable<int?>, then .OfType<int>() returns IEnumerable<int> and excludes elements that are null.

Similarly, if you have an IEnumerable<Base>, then .OfType<Derived>() returns IEnumerable<Derived> and excludes objects of other types.

If you want to do something more complicated than filtering based on type, then you could write your own extension method:

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;
}

As an example, if .OfType<int>() didn’t exist, you could use the following instead:

IEnumerable<int> ints = a.SelectWhere(n => n is int i ? (true, i) : (false, default));

Share
Improve this answer

1

It can be done in one step using LINQ’s SelectMany method, e.g.:

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

This works because SelectMany allows us to create a new sequence of elements (possible of a different type) for every element of the input sequence (which then are simply concatenated by SelectMany to produce the final result). Here we return a single-element sequence for every element we want to include and empty sequence for every element we want to discard.

For fun, if we go a bit deeper into theory, LINQ is an example of monad with SelectMany serving the role of monad’s bind operation (>>= operator in Haskell). Which means that in fact we should be able to implement all LINQ’s methods using only SelectMany method.

NB: The above implementation might not be the most efficient way to do it since we allocate a new single-element array for every element we want to include in the resulting sequence. And in fact that new[] { i } expression if monad’s return operator for which there is no corresponding method in LINQ. We can implement it ourselves easily though (just for fun):

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

This Return might even improve the efficiency of our implementation:

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

Or without extension method:

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

Share
Improve this answer

Your Answer

Draft saved
Draft discarded

Post as a guest

Required, but never shown


By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged

or ask your own question.

Leave a Reply

Your email address will not be published. Required fields are marked *