Using TryGetValue() in LINQ?
Asked Answered
B

3

7

This code works, but is inefficient because it double-lookups the ignored dictionary. How can I use the dictionary TryGetValue() method in the LINQ statement to make it more efficient?

IDictionary<int, DateTime> records = ...

IDictionary<int, ISet<DateTime>> ignored = ...

var result = from r in records
             where !ignored.ContainsKey(r.Key) ||
             !ignored[r.Key].Contains(r.Value)
             select r;

The problem is I'm not sure how to declare a variable within the LINQ statement to use for the out parameter.

Birdhouse answered 19/7, 2010 at 11:23 Comment(0)
B
4

You need to declare the out variable before the query :

ISet<DateTime> s = null;
var result = from r in records
             where !ignored.TryGetValue(r.Key, out s)
                || !s.Contains(r.Value)
             select r;

Be careful of side effects if the query isn't evaluated until later, though...

Braud answered 19/7, 2010 at 12:6 Comment(4)
This works. I'm guessing make sure you evaluate it while the variable is still in scope, and don't try to use it with parallel LINQ.Birdhouse
@Joe, precisely... that would have unpredictable effectsBraud
Note that in C# 7.3 it's now available to initialize the variable inline, such that where !ignored.TryGetValue(r.Key, out var s) || !s.Contains(r.Value)Entelechy
@Entelechy Can't see how that helps.Polariscope
F
18

(My answer concerns the general case of using TrySomething( TInput input, out TOutput value ) methods (like IDictionary.TryGetValue( TKey, out TValue ) and Int32.TryParse( String, out Int32 ) and so it does not directly answer the OP's question with the OP's own exmaple code. I'm posting this answer here because this QA is currently the top Google result for "linq trygetvalue" as of March 2019).

When using the extension method syntax there are at least these two approaches.

1. Using C# value-tuples, System.Tuple, or anonymous-types:

Call the TrySomething method first in a Select call, and store the outcome in a value-tuple in C# 7.0 (or anonymous-type in older versions of C#, note that value-tuples should be preferred due to their lower overhead):

Using C# 7.0 value-tuples (recommended):

// Task: Find and parse only the integers in this input:
IEnumerable<String> input = new[] { "a", "123", "b", "456", ... };

List<Int32> integersInInput = input
    .Select( text => Int32.TryParse( text, out Int32 value ) ? ( ok: true, value ) : ( ok: false, default(Int32) ) )
    .Where( t => t.ok )
    .Select( t => t.value )
    .ToList();

This can actually be simplified by taking advantage of another neat trick where the value variable is in-scope for the entire .Select lambda, so the ternary expression becomes unnecessary, like so:

// Task: Find and parse only the integers in this input:
IEnumerable<String> input = new[] { "a", "123", "b", "456", ... };

List<Int32> integersInInput = input
    .Select( text => ( ok: Int32.TryParse( text, out Int32 value ), value ) ) // much simpler!
    .Where( t => t.ok )
    .Select( t => t.value )
    .ToList();

Using C# 3.0 anonymous types:

// Task: Find and parse only the integers in this input:
IEnumerable<String> input = new[] { "a", "123", "b", "456", ... };

List<Int32> integersInInput = input
    .Select( text => Int32.TryParse( text, out Int32 value ) ? new { ok = true, value } : new { ok = false, default(Int32) } )
    .Where( t => t.ok )
    .Select( t => t.value )
    .ToList();

Using .NET Framework 4.0 Tuple<T1,T2>:

// Task: Find and parse only the integers in this input:
IEnumerable<String> input = new[] { "a", "123", "b", "456", ... };

List<Int32> integersInInput = input
    .Select( text => Int32.TryParse( text, out Int32 value ) ? Tuple.Create( true, value ) : Tuple.Create( false, default(Int32) ) )
    .Where( t => t.Item1 )
    .Select( t => t.Item2 )
    .ToList();

2. Use an extension method

I wrote my own extension method: SelectWhere which reduces this to a single call. It should be faster at runtime though it shouldn't matter.

It works by declaring its own delegate type for methods that have a second out parameter. Linq doesn't support these by default because System.Func does not accept out parameters. However due to how delegates work in C#, you can use TryFunc with any method that matches it, including Int32.TryParse, Double.TryParse, Dictionary.TryGetValue, and so on...

To support other Try... methods with more arguments, just define a new delegate type and provide a way for the caller to specify more values.

public delegate Boolean TryFunc<T,TOut>( T input, out TOut value );

public static IEnumerable<TOut> SelectWhere<T,TOut>( this IEnumerable<T> source, TryFunc<T,TOut> tryFunc )
{
    foreach( T item in source )
    {
        if( tryFunc( item, out TOut value ) )
        {
            yield return value;
        }
    }
}

Usage:

// Task: Find and parse only the integers in this input:
IEnumerable<String> input = new[] { "a", "123", "b", "456", ... };

List<Int32> integersInInput = input
    .SelectWhere( Int32.TryParse ) // The parse method is passed by-name instead of in a lambda
    .ToList();

If you still want to use a lambda, an alternative definition uses a value-tuple as the return type (requires C# 7.0 or later):

public static IEnumerable<TOut> SelectWhere<T,TOut>( this IEnumerable<T> source, Func<T,(Boolean,TOut)> func )
{
    foreach( T item in source )
    {
        (Boolean ok, TOut output) = func( item );

        if( ok ) yield return output;
    }
}

Usage:

// Task: Find and parse only the integers in this input:
IEnumerable<String> input = new[] { "a", "123", "b", "456", ... };

List<Int32> integersInInput = input
    .SelectWhere( text => ( Int32.TryParse( text, out Int32 value ), value ) )
    .ToList();

This works because C# 7.0 allows variables declared in an out Type name expression to be used in other tuple values.

Functional answered 26/3, 2019 at 8:28 Comment(0)
B
4

You need to declare the out variable before the query :

ISet<DateTime> s = null;
var result = from r in records
             where !ignored.TryGetValue(r.Key, out s)
                || !s.Contains(r.Value)
             select r;

Be careful of side effects if the query isn't evaluated until later, though...

Braud answered 19/7, 2010 at 12:6 Comment(4)
This works. I'm guessing make sure you evaluate it while the variable is still in scope, and don't try to use it with parallel LINQ.Birdhouse
@Joe, precisely... that would have unpredictable effectsBraud
Note that in C# 7.3 it's now available to initialize the variable inline, such that where !ignored.TryGetValue(r.Key, out var s) || !s.Contains(r.Value)Entelechy
@Entelechy Can't see how that helps.Polariscope
P
1

Using an external variable, you don't need to worry about it going out of scope because the LINQ expression is a closure that will keep it alive. However to avoid any conflicts, you could put the variable and expression in a function:

public IEnumerable GetRecordQuery() {
    ISet<DateTime> s = null;
    return from r in records
           ... 
}

...

var results = GetRecordQuery();

That way, only the query has access to the s variable, and any other queries (returned from separate calls to GetRecordQuery) will each have their own instance of the variable.

Peisch answered 8/1, 2014 at 15:3 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.