使用Linq查找列表子集中的下一个元素

4
我有以下的集合
// The collection to query
    var stageTable = new List<Stage>
    {
    new Stage {StageId = 1, LifecycleId = 1, StageSeqNo = 1},
    new Stage {StageId = 2, LifecycleId = 1, StageSeqNo = 2},
    new Stage {StageId = 3, LifecycleId = 1, StageSeqNo = 3},
    new Stage {StageId = 4, LifecycleId = 1, StageSeqNo = 4},
    new Stage {StageId = 5, LifecycleId = 2, StageSeqNo = 1},
    new Stage {StageId = 6, LifecycleId = 2, StageSeqNo = 2},
    new Stage {StageId = 7, LifecycleId = 2, StageSeqNo = 3},
    new Stage {StageId = 8, LifecycleId = 2, StageSeqNo = 4},
    };

我正在尝试构建一个查询,该查询将返回下一个阶段(Stage),给定一个当前阶段(currentStage),但包含在由LifecycleId定义的同一子集中,例如,给定currentStage = 2,我希望能得到stageId = 3阶段(Stage),但是如果currentStage = 4,由于LifecycleId切换到值2,我希望得到null

这是我的代码:

 var lifecycleId = stageTable
                .Where(x => x.StageId == currentStageId)
                .Select(x => x.LifecycleId);

 var nextStage = stageTable
                .Where(s => s.LifecycleId == lifecycleId.First())
                .SkipWhile(s => s.StageId != currentStageId)
                .Skip(1).FirstOrDefault();

看起来它可以工作,但是否有一种方法可以在单个查询中执行此操作?


这里的任何答案是否符合您的期望,@JamesB? - mjwills
@mjwills 决定坚持原来问题中的两行代码。所有解决方案都很有趣,但不确定它们是否提供更多的好处,在某些情况下会失去可读性。 - James B
主要的好处将是性能 @JamesB。我的或stop-cran将在单次遍历中完成。最终,您的解决方案也很好。您可能希望浏览答案并决定哪个是最好的,以便将其标记为答案。 - mjwills
4个回答

1
如果您安装了MoreLINQ NuGet包,那么您可以使用以下代码:
var currentStageId = 4;

var nextStage = stageTable.SkipWhile(z => z.StageId < currentStageId)
    .Lead(1, (x, y) => new { existing = x, next = y })
    .Take(1)
    .FirstOrDefault(z => z.next?.LifecycleId == z.existing.LifecycleId)?.next;

Console.WriteLine(nextStage?.StageId);

SkipWhile会跳过当前行之前的数据。

Lead将合并相邻的行(即将当前行和下一行放在一起)。

Take 1将确保我们只获取单个行(表示当前行和下一行在一起)。

FirstOrDefault将确保如果第二行没有与第一行相同的LifeCycleId,则返回null。


1
这里使用了带有谓词的FirstOrDefault重载方法:
{
    var currentStageId = 3;

    // The collection to query
    var stageTable = new List<Stage> {
        new Stage {StageId = 1, LifecycleId = 1, StageSeqNo = 1},
        new Stage {StageId = 2, LifecycleId = 1, StageSeqNo = 2},
        new Stage {StageId = 3, LifecycleId = 1, StageSeqNo = 3},
        new Stage {StageId = 4, LifecycleId = 1, StageSeqNo = 4},
        new Stage {StageId = 5, LifecycleId = 2, StageSeqNo = 1},
        new Stage {StageId = 6, LifecycleId = 2, StageSeqNo = 2},
        new Stage {StageId = 7, LifecycleId = 2, StageSeqNo = 3},
        new Stage {StageId = 8, LifecycleId = 2, StageSeqNo = 4},
    };

    var nextStage = stageTable.FirstOrDefault(s => s.StageId > currentStageId && s.LifecycleId == stageTable.FirstOrDefault(s2=>s2.StageId==currentStageId)?.LifecycleId);
}

你似乎只是通过声明一个currentStage对象来替换了问题中的第一个Linq查询,而没有使用stageTable.Where(x => x.StageId == currentStageId)。如果OP有一个阶段对象,那么第一个查询就不需要了,因此你的答案并没有真正解决OP的问题。 - Chris
@Chris 我做了一个改变,考虑到未知的LifecycleID。我想问一下 OP,这似乎是你在循环中做的事情,逐渐找到下一个阶段。既然你从中得到了一个Stage对象,为什么下次你不知道LifecycleId呢? 为什么你没有整个Stage而只有currentStageId?这迫使你必须有两个循环,第一个是查找LifecycleId,然后用它查找Stage。如果列表更大,那将是一件非常昂贵的事情。 - Ashley Pillay
这确实是你能够使用的最接近单个查询的方式。我不明白为什么它会被踩。 - charliefox2
你可能会发现 stageTable.FirstOrDefault(s2=>s2.StageId==currentStageId) 会为每个源行执行,直到找到匹配项(因此,如果它要查找的行是第一百万行,则该解决方案可能不会很好地扩展)。 - mjwills
1
仔细查看这些解决方案,尽管它们使用类似LINQ的语义,但它们依赖于额外的代码来实现该行为。本质上编写新的LINQ运算符。OPs的请求是在单个表达式中完成此操作,这意味着他想要减少代码。除了代码重用的可能好处外,OPs最初的解决方案在2行标准LINQ中完成此操作通常比在1行中编写其他支持函数的最终表达式更可取。 - Ashley Pillay
显示剩余5条评论

0
一种方法是先获取当前阶段和下一个阶段(无论循环如何),然后检查以下阶段是否属于同一循环:
var nextStage = stageTable.Where(s => s.StageId >= currentStageId)   //filter out all lower values
                .OrderBy(s=> s.StageId)                              //if the list is always ordered, this could be omitted
                .Take(2)                                             //enumeration now contains the currentid and the next id
                .GroupBy(s=>s.LifecycleId)                           //only from those 2 objects -> group by the lifecycle
                .First().Skip(1).FirstOrDefault();                   //the (first) group will only contain 2 items, if the lifecycle is the same

0
我将介绍一种方法,可以迭代基础可枚举项以及(可能为空的)前一个项:
public static class EnumerableExtras
{
    public static IEnumerable<Pair<T>> WithPrevious<T>(this IEnumerable<T> source)
        where T : class
    {
        T previous = null;

        foreach (var item in source)
        {
            yield return new Pair<T>(item, previous);
            previous = item;
        }
    }
}

public class Pair<T> where T : class
{
    public Pair(T current, T previous)
    {
        Current = current;
        Previous = previous;
    }

    public T Current { get; }
    public T Previous { get; }
}

可以使用这个扩展提出以下解决方案:
var nextStage = stageTable
    .WithPrevious()
    .FirstOrDefault(pair => pair.Previous?.StageId == currentStageId &&
                            pair.Current.LifecycleId == pair.Previous?.LifecycleId)
    ?.Current;

对于值类型,可以定义类似于具有可空 Previous 属性的 Pair<> 类。


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