20

I have this function:

public IEnumerable<string> EnumPrograms() {
    return dev.AudioSessionManager2.Sessions.AsEnumerable()
        .Where(s => s.GetProcessID != 0)
        .Select(s => {
            try {
                return Process.GetProcessById((int)s.GetProcessID).ProcessName;
            }
            catch (ArgumentException) {
                return null;
            }
        });
}

The try..catch is necessary since there may be sessions with a PID that doesn't exist anymore. I'd like to skip them. Is there a way to do this from the Select callback or do I need to add a new Where condition that skips null values?

ThiefMaster
  • 310,957
  • 84
  • 592
  • 636

6 Answers6

30

No, Select always yields one output element for each input element. There's no alternative to that. You could easily write your own FilteredSelect extension method - but it's simpler just to use a Where clause.

Alternatively, use Process.GetProcesses() to get a snapshot of all processes, and then join that to your sessions collection (or use something similar). That would avoid the ugly catch:

var sessionProcessIds = new HashSet<int>(dev.AudioSessionManager2.Sessions
                                            .AsEnumerable()
                                            .Select(x => x.GetProcessId)
                                            .Where(pid => pid != 0));
var processes = Process.GetProcesses();
var sessionProcessNames = processes.Where(p => sessionProcessIds.Contains(p.Id))
                                   .Select(p => p.ProcessName);

Or:

var names = from session in dev.AudioSessionManager2.Sessions.AsEnumerable()
            let pid = session.GetProcessId
            where pid != 0
            join process in Process.GetProcesses() on pid equals process.Id
            select process.ProcessName;
Jon Skeet
  • 1,421,763
  • 867
  • 9,128
  • 9,194
  • That second suggestion sounds interesting. But the processes with audio sessions is just a small subset of the running processes so I guess it would be somewhat less efficient (not that it matters in this case)? – ThiefMaster Dec 08 '13 at 13:14
  • 1
    @ThiefMaster: I don't know how long `Process.GetProcesses()` takes, but at least you'd only be calling it *once* rather than once per session. – Jon Skeet Dec 08 '13 at 13:15
  • 1
    @ThiefMaster I'm pretty sure both `GetProcesses` and `GetProcessByID` are calling `EnumProcesses` under the hood. – Rotem Dec 08 '13 at 14:59
6

Building on John Skeet's post this extension method has saved me countless lines of code. The name fits perfectly SelectWhere. The code listing below is an Extension method you can use.

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

Usage:

entity.SelectWhere(e => e.FirstName, e => e.Age>25);
Rax
  • 665
  • 8
  • 19
5

Select in Linq is the equivalent to Map, while Aggregate is the equivalent to Reduce. Map/Select is 1:1 input to output. You want to use Reduce/Aggregate if there is not a 1:1 relationship.

public IEnumerable<string> EnumPrograms() {
    return dev.AudioSessionManager2.Sessions.AsEnumerable()
        .Where(s => s.GetProcessID != 0)
        .Aggregate(new List<string>(), (acc, s) => {
            try {
                var proc = Process.GetProcessById((int)s.GetProcessID).ProcessName;
                acc.Add(proc);
            } catch (ArgumentException) { }
            return acc;
    });
}
goldenratio
  • 1,026
  • 14
  • 24
4

Select can't do that by itself, you could create a custom extension method for that as @Jon Skeet mentioned.

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

And use as

elements.FilteredSelect(/* where condition */, /* select values */);
BrunoLM
  • 97,872
  • 84
  • 296
  • 452
2

As others said Select always yields one output element for each input element, but if your case is to filter basing on type then you can use Enumerable.OfType<TResult>(IEnumerable) - https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable.oftype?view=net-5.0

For example this:

var agents = data.Where(d => d is Agent).Select(d => d as Agent);

is equivalent to this:

var agents = data.OfType<Agent>();
tometchy
  • 609
  • 5
  • 6
1

And here is a simpler version of the extension methods by using LINQ:

public static IEnumerable<TResult> FilteredSelect<TSource, TResult>(this IEnumerable<TSource> source, Func<TSource, bool> predicate, Func<TSource, TResult> selector)
{
    return source
        .Where(item => predicate(item))
        .Select(item => selector(item));
}
Just Shadow
  • 10,860
  • 6
  • 57
  • 75