有没有办法使用LINQ根据封闭条件选择一系列项目(即不是简单的WHERE子句)?

时间:2014-07-07 04:24:40

标签: c# linq lambda

考虑您有List<Foo>个对象,FooIsSelected属性,如此...

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的第一个和最后一个项目之间的所有项目是真的怎么办? (即第二至第七)

我知道我可以使用SkipWhile,因为它会跳过第一个真正的语句,然后返回所有内容......

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

......而且我知道我可以扭转并再次做同样的事情,但是我必须在最后重新扭转并且双重反转只是感觉它会不必要地昂贵。

我的另一个想法是使用Select with index并存储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);

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

.Take(lastSelectedIndex + 1);

那么还有另一种方法可以做我想要的吗?

6 个答案:

答案 0 :(得分:3)

好吧,让我通过使用索引并将项目添加到新集合中来区别对待。

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毫秒才能得到结果。

答案 1 :(得分:2)

你可以使用.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#s语法对于这类事情来说不是很好,所以我确实坚持使用旧的 - 学校命令式编程在这里)

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

最后一行是因为该算法错过了最后一个selected

如果涉及表现

一直走

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替换为List.Add

答案 2 :(得分:1)

你可以在Where中传递索引并在条件中使用它。

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

编辑,您可以将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());

答案 3 :(得分:1)

你的初衷是正确的。跟着这个:

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

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

我刚刚在列表中尝试了这个包含1,000,000个元素的代码,并且在我三岁的笔记本电脑上完成了163毫秒。 10,000,000只花了1.949秒。

答案 4 :(得分:1)

除了Dimi Toulakis Answer

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

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

答案 5 :(得分:1)

合并Dimi Toulakis's AnswerAdil's Answer

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的步骤。