Linq:前瞻性条件

9

我有一个列表(简化版)

[Kind]      [Name]
null        E
null        W
4           T
5           G
6           Q
null        L
null        V
7           K
2           Z
0           F

我需要{E,L} -> 某些项,它们的Kind==null并且下一个Kind也==null

假设有一个递增且按顺序的ID。

在Linq中是否可能实现这种前瞻性?

2个回答

9
像这样吗?
void Main()
{
    List<SomeClass> list = new List<SomeClass>() {
        new SomeClass() { Kind = null, Name = "E" },
        new SomeClass() { Kind = null, Name = "W" },
        new SomeClass() { Kind = 4, Name = "T" },
        new SomeClass() { Kind = 5, Name = "G" },
        ...
    };

    var query = list.Where ((s, i) =>
        !s.Kind.HasValue &&
        list.ElementAtOrDefault(i + 1) != null &&
        !list.ElementAt(i + 1).Kind.HasValue);
}

public class SomeClass
{
    public int? Kind { get; set; }
    public string Name { get; set; }
}

编辑:借鉴@Jeff Marcado的解决方案,实现类似于以上用法但更简洁的扩展方法,不需要处理索引:

public static IEnumerable<TSource> WhereWithLookahead<TSource>(this IEnumerable<TSource> source, Func<TSource, TSource, bool> predicate) where TSource : class
{
    using(var enumerator = source.GetEnumerator())
    {
        if (!enumerator.MoveNext())
        {
            //empty
            yield break;
        }

        var current = enumerator.Current;
        while (enumerator.MoveNext())
        {
            var next = enumerator.Current;

            if(predicate(current, next))
            {
                yield return current;
            }

            current = next;
        }

        if (predicate(current, null))
        {
            yield return current;
        }

    }
}

// Use:
var query2 = list.WhereWithLookahead((current, next) =>
    !current.Kind.HasValue &&
    (next != null) &&
    next.Kind.HasValue);

2
我在你的解决方案中看到了一个索引超出范围的异常:如果最后一项 Kindnull,那么 list[i + 1] 将会超出列表的索引范围。 - nemesv
仍然不完美:在 ElementAtOrDefaultElementAt 中用 i+1 替换 i 以使其正确。 - nemesv
虽然这个解决方案可行,但我不确定它的效率。无论是 ElementAtOrDefault 还是 ElementAt 调用都会再次遍历列表(每个都是 O(n) 而不是 O(1)),使得这个过程比应该花费更多时间。如果性能是一个问题,我会避免在这种情况下使用 LINQ 并选择一个更标准的方法。 - Adi Lester
@Lester:ElementAtElementAtOrDefault方法仅在不实现IList<T>时迭代所有元素。如果它们实现了IList<T>接口,则该项将在常数时间(O(1))内返回。 - Alex Essilfie
@Lester:任何其他方法都会使用此处提供的相同索引,因此用任何其他您喜欢的方法替换ElementAtOrDefault,解决方案仍然适用。我确实同意,仅出于兴趣而使用LINQ可能不是最灵活/可读的方法,但它确实可以做到。 - Ocelot20

5

如果采用功能性方法,您可以这样实现一个前瞻枚举器:

IEnumerable<Item> collection = ...;
var lookahead = collection.Zip(collection.Skip(1), Tuple.Create);

枚举器将遍历每个项目及其后续项目的元组。这不包括集合中的最后一个项目。然后只需要执行查询即可。
var query = collection.Zip(collection.Skip(1), Tuple.Create)
    .Where(tuple => tuple.Item1.Kind == null && tuple.Item2.Kind == null)
    .Select(tuple => tuple.Item1);

很遗憾,这种方式非常低效。您正在两次枚举集合的长度,这可能非常昂贵。

最好编写自己的枚举器,这样您只需一次通过集合即可:

public static IEnumerable<TResult> LookAhead<TSource, TResult>(
    this IEnumerable<TSource> source,
    Func<TSource, TSource, TResult> selector)
{
    if (source == null) throw new ArugmentNullException("source");
    if (selector == null) throw new ArugmentNullException("selector");

    using (var enumerator = source.GetEnumerator())
    {
        if (!enumerator.MoveNext())
        {
            //empty
            yield break;
        }
        var current = enumerator.Current;
        while (enumerator.MoveNext())
        {
            var next = enumerator.Current;
            yield return selector(current, next);
            current = next;
        }
    }
}

然后查询变成了:
var query = collection.LookAhead(Tuple.Create)
    .Where(tuple => tuple.Item1.Kind == null && tuple.Item2.Kind == null)
    .Select(tuple => tuple.Item1);

好的。希望你不介意我借鉴了一些这里的通用思路来更新我的答案 :) - Ocelot20

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