是否有一种方法可以使用LINQ根据封闭条件(即不是简单的WHERE子句)选择一系列项目

本文关键字:简单 条件 子句 项目 一系列 选择 WHERE 一种 方法 可以使 是否 | 更新日期: 2023-09-27 18:11:55

假设你有一个对象的List<Foo>Foo有一个IsSelected属性,像这样…

public class Foo
{
    public string Name{ get; set; }
    public bool IsSelected{ get; set; }
}
List<Foo> sourceItems = new List<Foo>
{
    new Foo(){ Name="First",   IsSelected=false},
    new Foo(){ Name="Second",  IsSelected=true },
    new Foo(){ Name="Third",   IsSelected=false},
    new Foo(){ Name="Fourth",  IsSelected=true },
    new Foo(){ Name="Fifth",   IsSelected=false},
    new Foo(){ Name="Sixth",   IsSelected=true },
    new Foo(){ Name="Seventh", IsSelected=true },
    new Foo(){ Name="Eighth",  IsSelected=false},
    new Foo(){ Name="Ninth",   IsSelected=false},
    new Foo(){ Name="Tenth",   IsSelected=false}
};

使用Where子句,我当然可以得到选中的项目,像这样…

var results = sourceItems.Where(item => item.IsSelected);

…但如果我想要所有在第一项和最后一项之间且IsSelected为true的项目呢?(即第二至第七)

我知道我可以使用SkipWhile,因为它跳过直到第一个true语句,然后返回之后的所有内容…

// Returns from Second on
var results = sourceItems.SkipWhile(item => !item.IsSelected);

…我知道我可以倒回去再做一遍,但这样我就必须在最后重新倒回去,而且两次倒回去感觉会不必要地昂贵。

我的另一个想法是使用Select索引并存储IsSelected为true的最后一个索引,然后在末尾使用where子句,检查索引是否低于最后选择的索引,但这似乎很昂贵且模糊。

int lastSelectedIndex = -1;
var results = sourceItems
    .SkipWhile(item => !item.IsSelected)
    .Select( (item, itemIndex) => 
    {
        if(item.IsSelected)
            lastSelectedIndex = index;
        return new {item, index};
    })
    .Where(anonObj => anonObj.index <= lastSelectedIndex)
    .Select(anonObj => anonObj.Item);

另外,我认为我可以用Take子句替换最后的Where,只是采取正确的项目数量,这样我就不必迭代整个列表,但我不确定lastSelectedIndex将具有正确的值,因为我不认为选择返回整个列表,只有下一个枚举,但我可能是错误的。

.Take(lastSelectedIndex + 1);

那么有没有别的方法来做我想做的?

是否有一种方法可以使用LINQ根据封闭条件(即不是简单的WHERE子句)选择一系列项目

那么,让我用不同的方法来处理这个问题,通过使用索引并将项目加到一个新的集合中。

List<Foo> sourceItems = new List<Foo>
{
    new Foo(){ Name="First",   IsSelected=false},
    new Foo(){ Name="Second",  IsSelected=true },
    new Foo(){ Name="Third",   IsSelected=false},
    new Foo(){ Name="Fourth",  IsSelected=true },
    new Foo(){ Name="Fifth",   IsSelected=false},
    new Foo(){ Name="Sixth",   IsSelected=true },
    new Foo(){ Name="Seventh", IsSelected=true },
    new Foo(){ Name="Eighth",  IsSelected=false},
    new Foo(){ Name="Ninth",   IsSelected=false},
    new Foo(){ Name="Tenth",   IsSelected=false}
};
int startIndex = sourceItems.FindIndex(x => x.IsSelected);
int endIndex   = sourceItems.FindLastIndex(x => x.IsSelected);
var items = new List<Foo>();
for (int i = startIndex; i <= endIndex; i++)
    items.Add(sourceItems[i]);    

对于1 000 000个条目,只需要13毫秒就可以得到结果。

你可以使用.Aggregte只迭代一次,但这有点混乱:

var lists = sourceItems.Aggregate(Tuple.Create(new List<Foo>(), new List<Foo>()), (acc, foo) =>
{
    if (foo.IsSelected)
    {
        acc.Item1.AddRange(acc.Item2);
        acc.Item2.Clear();
        acc.Item2.Add(foo);
    }
    else if (acc.Item2.Any())
    {
        acc.Item2.Add(foo);
    }
    return acc;
});
if (lists.Item2.Any()) lists.Item1.Add(lists.Item2.First());

正如你所看到的,这个解决方案不是很纯粹的但是你可以在里面复制列表(c#的语法不太适合这种事情,所以我在这里坚持使用老式的命令式编程)

这个想法很简单:你有两个列表-第一个将是结果,在第二个中你累积所有对象直到你到达另一个selected -如果你把累加器附加到第一个列表,并开始一个新的累加器。

最后一行在那里是因为算法漏掉了最后一个selected

如果涉及性能

go all way

static IEnumerable<Foo> BetweenSelected(List<Foo> foos)
{
    var lastSelected = foos.Count;
    for (var i = 0; i < foos.Count; i++)
    {
        var foo = foos[i];
        if (foo.IsSelected)
        {
            for (var j = lastSelected; j < i; j++)
                yield return foos[j];
            lastSelected = i+1;
            yield return foo;
        }
    }
}

您甚至可以将其更改为返回另一个列表,并将yield s替换为List.Add

你最初的直觉是对的。用这个:

var results =
    sourceItems
        .SkipWhile(x => x.IsSelected == false)
        .Reverse()
        .SkipWhile(x => x.IsSelected == false)
        .Reverse();

除非你的列表包含数百万个项目,否则它不会有太大的性能问题。

我刚刚用列表中的1,000,000个元素尝试了这段代码,在我3年旧的笔记本电脑上,它在163毫秒内完成。1000万只花了1.949秒

可以在Where中传递index,并在condition中使用。

var result = sourceItems.Where((ele, index) => 
             index > 0 && index < 7 && ele.IsSelected);

Edit根据注释,可以得到IsSelected设置为true的索引,并使用MinMax来获得Where条件下可以使用的起始和结束索引,以获得该范围内的记录。

var indexes = Enumerable.Range(0, sourceItems.Count)
                        .Where(i => sourceItems[i].IsSelected);
var result1 = sourceItems.Where((el, idx)=>idx >= indexes.Min() &&  idx <= indexes.Max());

除了Dimi Toulakis的回答

如果有人在他的代码中经常需要这个,这里是一个扩展

public static class ListExtension
{
    public static List<T> FindGroup<T>(this List<T> mylist, Predicate<T> pred)
    {
        var first = mylist.FindIndex(pred);
        var last = mylist.FindLastIndex(pred);
        last += 1; // to get the Last Element
        return mylist.GetRange(first, last - first);
    }
}

结合Dimi Toulakis的答案和Adil的答案:

List<Foo> sourceItems = new List<Foo>{
    new Foo(){ Name="First",   IsSelected=false},
    new Foo(){ Name="Second",  IsSelected=true },
    new Foo(){ Name="Third",   IsSelected=false},
    new Foo(){ Name="Fourth",  IsSelected=true },
    new Foo(){ Name="Fifth",   IsSelected=false},
    new Foo(){ Name="Sixth",   IsSelected=true },
    new Foo(){ Name="Seventh", IsSelected=true },
    new Foo(){ Name="Eighth",  IsSelected=false},
    new Foo(){ Name="Ninth",   IsSelected=false},
    new Foo(){ Name="Tenth",   IsSelected=false}
};
int startIndex = sourceItems.FindIndex(item => item.IsSelected);
int endIndex   = sourceItems.FindLastIndex(item => item.IsSelected);
var result = sourceItems.Where((item, itemIndex) => itemIndex >= startIndex && itemIndex <= endIndex);

我们也可以使用GetRange(),但它不是LINQ。

另一种似乎更有效的方法是使用TakeWhile()SkipWhile():
int endIndex = sourceItems.FindLastIndex(item => item.IsSelected);
var result = sourceItems
    .TakeWhile((item, itemIndex) => itemIndex <= endIndex) // Take all before (and including) index of last match (must be before SkipWhile as that would change the index)
    .SkipWhile(item => !item.IsSelected); // Skip up until the first item where IsSelected is true

这也省去了查找startIndex的步骤。