是否有一种方法可以使用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);
那么有没有别的方法来做我想做的?
那么,让我用不同的方法来处理这个问题,通过使用索引并将项目加到一个新的集合中。
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的索引,并使用Min
和Max
来获得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的步骤。