使用列表<t>中的take方法后的removerange方法具有意外行为

时间:2018-10-10 08:02:00

标签: c#

有人知道为什么在以下代码中离开while循环主体后的长度为零吗?请不要向我提供分块算法,我已经知道几种这样的算法,并且我知道如何解决此问题。我的问题只是关于RemoverRange或Take的有线行为。

static void Main(string[] args)
{

    var list = new List<int>();
    for (int i = 0; i < 1000; i++)
    {
        list.Add(i);
    }

    var chunks = new List<IEnumerable<int>>();

    while (list.Count > 0)
    {
        chunks.Add(list.Take(10));
        list.RemoveRange(0, 10);
    }

    int length = chunks.ToList()[0].Count();
}

4 个答案:

答案 0 :(得分:3)

在以下行:

var chunks = new List<IEnumerable<int>>();

您创建一个Count为0的列表,因为列表中没有任何项目。然后在每个步骤的foreach语句中添加list.Take(10),然后从list中删除前10个项目。 重要在这里是要认识到list.Take(10)懒惰求值的(您经常会听到术语延迟执行)。第一次评估的时间在以下行:

int length = chunks.ToList()[0].Count();

将在此行list.Take(10)进行评估,并且由于您已删除了list中的所有项目,因此list中没有任何元素。因此,list.Take(10)返回一个空序列,因此chunks.ToList()[0]将是一个空列表。

更新 延迟执行说明

让我们列出以下列表:

var list = new List<int> {1, 2, 3, 4, 5 };
var firstThree = list.Take(3);

变量firstThree保存着对枚举数的引用-特别是IEnumerable<int>。它不包含数组或1,2,3的列表。首次使用此迭代器时,您将开始从list中“获取”数据。

例如,您可以调用ToListToArray方法:

var firstThreeList = firstThree.ToList();
var firstThreeArray = firstThree.ToArray();

以上两个调用都将使迭代器起作用-一般而言,这将强制在LINQ中执行查询-。此时,您将遍历list并获取前三个项目。

话虽如此,很显然,如果您同时通过删除list中的所有数字来修改iist,则list中将没有任何元素你什么也得不到。

作为测试,我建议您一次运行上述代码,然后再次运行,在进行该调用的ToListToArray之前:

list.RemoveAll(x => true);

您现在会注意到firstThreeArrayfirstThreeList都是空的。

答案 1 :(得分:1)

您遇到的问题是LINQ有点像视图。实际上,只有当您调用要求其产生特定值(First()Last()Count()等)的方法时,它才会对集合进行迭代。因此,仅在调用这些方法之一时评估列表。

chunks.Add(list.Take(10));

此代码有效地表示“引用list,并且当有人迭代您时,只能进行前10个项目”。要解决此问题,您可以将该小部分转换为列表(评估这10个项目,然后从中创建一个新列表):

chunks.Add(list.Take(10).ToList());

考虑以下代码:

List<string> names = new List<string>() { "a", "b", "c" };
IEnumerable<string> skip2 = names.Skip(2);
Console.WriteLine(string.Join(", ", skip2)); // "c"
names.Add("d");
names.Add("e");
Console.WriteLine(string.Join(", ", skip2)); // "c, d, e"

因为每次调用IEnumerable<string> skip2时都使用迭代器(string.Join(", ", skip2)),所以即使列表已更改,每次也会遍历列表。

这样,您将在第一次运行中获得"c",在第二次运行中获得"c, d, e"

实际上,这将是完全有效的(尽管很难阅读):

List<int> list = new List<int>();
IEnumerable<int> values = list.DefaultIfEmpty(0);
list.Add(5);
list.Add(10);
list.Add(15);
Console.WriteLine(values.Average()); // 10

答案 2 :(得分:1)

chunks.Add(list.Take(10));

实际上并没有从list中提取10个项目,它只是告诉第一次引用chunks[i]时要提取10个项目(例如,通过Count())。由于您要更改列表,因此参考指向空列表。您可以使用

强制评估列表
chunks.Add(list.Take(10).ToList());

答案 3 :(得分:0)

根本没有怪异的行为。这正是IEnumerable的预期行为。您应该记住的是IEnumerable是惰性计算的,这意味着它是在枚举时(即您实际访问所说的IEnumerable时)进行评估的。你在做什么基本上是→

①获取对list对象的引用
②准备采用上述list对象的前10个元素,还没有评估!
③将not yet evaluated对象,在这种情况下→(LINQ.Take(10))添加到chunks列表中。
④删除list的前10个元素
⑤漂洗并重复操作,直到list中的所有项目都没有了
⑥创建一个列表,该列表全部由not yet evaluated的{​​{1}}个项目组成。
⑦您采用了上述list.Take(10)的第一个元素chunks,但引用了not yet evaluate的前10个元素,为空!!!
⑧您在IEnumerable实例上调用list,该实例最终将评估一个完整的Count的前十个元素