在LINQ中如何使用TryGetValue()?

7

这段代码是可行的,但效率不高,因为它会对字典 ignored 进行双重查找。如何在 LINQ 语句中使用字典的 TryGetValue() 方法来提高效率呢?

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;

问题在于我不确定如何在LINQ语句中声明一个变量以供输出参数使用。
3个回答

17
我的回答涉及使用TrySomething(TInput input, out TOutput value)方法的一般情况(例如IDictionary.TryGetValue(TKey, out TValue)Int32.TryParse(String, out Int32)),因此不直接回答OP在其示例代码中的问题。 我在这里发布这个答案,因为这个QA目前是2019年3月“linq trygetvalue”的谷歌搜索结果中排名最高的。

在使用扩展方法语法时,至少有以下两种方法。

1. 使用C#值元组、System.Tuple或匿名类型:

首先在Select调用中调用TrySomething方法,并在C# 7.0中将结果存储在值元组中(或在旧版本的C#中使用匿名类型,请注意由于其较低的开销,应优先使用值元组):

使用C# 7.0值元组(推荐):

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

这可以通过利用另一个巧妙的技巧来简化,其中value变量在整个.Select lambda中都处于作用域内,因此三元表达式变得不必要,如下所示:

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

使用 C# 3.0 匿名类型:

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

使用 .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. 使用扩展方法

我编写了自己的扩展方法:SelectWhere,这将减少到一个单独的调用。虽然在运行时它应该更快,但这并不重要。

它的工作原理是声明自己的delegate类型,用于具有第二个out参数的方法。Linq默认不支持这些,因为System.Func不接受out参数。但是由于委托在C#中的工作方式,您可以使用TryFunc与匹配它的任何方法,包括Int32.TryParseDouble.TryParseDictionary.TryGetValue等。

要支持其他带有更多参数的Try...方法,只需定义一个新的委托类型,并提供一种让调用者指定更多值的方式即可。

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

使用方法:

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

如果您仍想使用lambda表达式,可以使用值元组作为返回类型的替代定义(需要C#7.0或更高版本):
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;
    }
}

使用方法:

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

这是因为C# 7.0允许在out Type name表达式中声明的变量可以在其他元组值中使用。

4
你需要在查询之前声明out变量:
ISet<DateTime> s = null;
var result = from r in records
             where !ignored.TryGetValue(r.Key, out s)
                || !s.Contains(r.Value)
             select r;

如果查询要在后面才被评估,请注意副作用...


这个可以工作。我猜确保在变量仍然在作用域内时对其进行评估,并且不要尝试在并行LINQ中使用它。 - Joe Daley
1
@Joe,确切地说...那将会产生不可预测的影响。 - Thomas Levesque
请注意,在C#7.3中,现在可以内联初始化变量,例如where !ignored.TryGetValue(r.Key, out var s) || !s.Contains(r.Value) - Caramiriel
@Caramiriel 看不出那有什么帮助。 - HappyNomad

1
使用外部变量,您无需担心它会超出范围,因为LINQ表达式是一个保持其活性的闭包。但是为了避免任何冲突,您可以将变量和表达式放在一个函数中:
public IEnumerable GetRecordQuery() {
    ISet<DateTime> s = null;
    return from r in records
           ... 
}

...

var results = GetRecordQuery();

那样,只有查询才能访问 s 变量,而任何其他查询(从单独调用 GetRecordQuery 返回)都将拥有其自己的变量实例。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接