从List<MyType>转换为IEnumerable<MyType>再转回List<MyType>时出现无效强制转换,为什么?

5
所以基本上我有这个方法。
public List<Customer> FilterCustomersByStatus(List<Customer> source, string status)
{
    return (List<Customer>)source.Where(c => c.Status == status);
}

我遇到了一个错误,无法进行转换:

无法将类型为“WhereListIterator`1[AppDataAcces.Customer]”的对象强制转换为类型“System.Collections.Generic.List`1[AppDataAcces.Customer]”。

为什么会出现这个错误呢?因为基础类型是相同的,Enumerable.Where为什么要创建一个新的WhereListIterator实例?如果是这样,为什么任何人都会这样做呢?因为这是一种不必要的性能和功能损失,因为我总是需要创建一个新的列表(.ToList())。


使用Linq,它创建一个可枚举/枚举器并动态迭代您的集合,而无需创建新的集合。除非您调用.ToList(),否则它实际上不会返回List对象。它这样做的原因是为了提高性能和限制内存使用;对于包含1000000个条目的列表,Linq可以在不必每次复制列表的情况下过滤/选择所有项目。 - Chris Sinclair
你能否解释一下为什么每次都需要使用 List?特别是像你所说的那样,它包含了 500k 条目。 - Sergei Rogovtcev
我喜欢将它们保存在列表中,这样我就可以添加/修改其中的一些元素,即使我将它们保存在IEnumerable中,在显示项目时我仍然需要枚举我的对象以获取它们,尽管我正在寻找此方法的实现相关性而不是我的工作相关性。 - Freeman
添加或修改filtered列表的目的是什么?您能展示一下您代码的更大部分,并描述您正在尝试解决的场景吗? - Sergei Rogovtcev
我并没有尝试解决任何特殊情况,只是想了解为什么它被实现成这样(有什么好处)?这就是我在这里提问的原因,没有其他意思。 - Freeman
我已经更新了我的答案,并说明了为什么 - Sergei Rogovtcev
5个回答

10

Enumerable.Where 是否会创建一个新的 WhereListIterator 实例?

是的。

那么为什么有人要这样做呢?

因为它允许懒惰的流式行为。如果消费者只想要第一项或第二项,Where 不必过滤整个列表。这在 LINQ 中很正常。

因为这是不必要的性能和功能损失,我总是需要创建一个新的列表 (.ToList())

这种 "性能和功能的损失" 来自于你的设计。你不需要在筛选后使用 List<Customer>,因为没有对其进行任何修改的意义。

更新:为什么要这样实现 因为它是基于 IEnumerable 实现的,而不是基于 IList。因此它看起来像 IEnumerable,像 IEnumerable 那样工作。

另外,用这种方式实现它要容易得多。试想一下,如果你必须在 IList 上编写 Where,该怎么办?返回原始列表上的代理?这会在每次访问时造成巨大的性能损失。返回一个新的包含筛选项的列表?这与执行 Where().ToList() 相同。返回删除了所有不匹配项的原始列表?这就是 RemoveAll 的作用,为什么要再写一个方法呢。

请记住,LINQ 尝试以函数式方式运行,并尝试将对象视为不可变的。


谢谢Serg,你的回答非常明确。但是如果它在过滤后将List转换为IEnumerable(仅保留对我的项目的引用,而不是物理项目),然后我再次使用.ToList创建一个新的List,那么我的收益在哪里?请耐心等待我的解释。 - Freeman
它不会倾倒任何东西。它保持对原始IEnumerable的内部引用,并在其上构建另一个迭代器。然后在下一次调用时再构建另一个迭代器。你的好处在于,直到需要它们时,你不会处理所有的项 - 而你几乎永远不会这样做。 - Sergei Rogovtcev
如果您了解有关其工作原理的好文章,我将不胜感激。 - Freeman
Skeet的《深入浅出C#》一书对迭代器有很好的讲解。 - Sergei Rogovtcev

2
正如其他人所指出的,您需要使用 ToList 将结果转换为 List<T>
原因是 Where 是惰性求值的,所以 Where 并没有真正筛选数据。它只创建了一个 IEnumerable,在需要时筛选数据。
惰性求值有几个好处。它可能更快,允许在无限的 IEnumerable 中使用 Where 等等。 ToList 强制将结果转换为 List<T>,这似乎是您想要的。

1
嗯,Where() 是懒加载的事实在这里并不重要。它不返回 List<T> 才是重点。 - svick
我想要一个列表,我知道ToList可以实现这一点,我只是想知道为什么它不保留底层类型,所以每次都需要创建一个新的列表? - Freeman
@svick,关键在于Where不仅是惰性的,而且是流式的,你不能返回List并仍然保持流式。 - Sergei Rogovtcev
@svick 我认为 List<T> 永远不可能是惰性的。最多,如果 Linq 是 IList-only,它 可以 返回 IList<T>。至于为什么,我猜想对于长链式的 LINQ 函数,惰性求值通常更有效率。 - luiscubal

0

Where扩展方法可以过滤并返回IEnumerable<TSource>类型的数据,因此您需要调用.ToList()方法将其转换回来。

public List<Customer> FilterCustomersByStatus(List<Customer> source, string status)
{
    return source.Where(c => c.Status == status).ToList();//This will return a list of type customer
}

请问您能否解释一下 -1 的含义? - HatSoft
解释什么?我只是想知道为什么每次都生成新列表,因为性能很差,特别是在有50万个以上的条目时。 - Freeman
@Freeman 我同意性能较差,所以你应该将返回类型更改为Customer[],并在Where子句中调用.ToArray()而不是.ToList()。使用ToArray返回声明类型的数组,你将无法获得List的好处。 - HatSoft
即使我返回一个数组,我将在其中使用相同的Where方法,它是否不会返回相同的IEnumerable类型? - Freeman
@Freeman 当你调用.ToArray()时,它会创建一个Ineumerable<out T>的数组,该数组将是Customer[]。 - HatSoft
因此,它的返回类型并不总是相同的(至少对于其基本类型而言)。 - Freeman

0

IEnumerable和IList的区别在于,IEnumerable不包含任何数据,它包含一个迭代器,当您请求新数据时(例如使用foreach循环),它会遍历数据。另一方面,列表是数据的副本。在您的情况下,要创建列表,ToList()方法会遍历整个数据并将其添加到列表对象中。

根据您计划使用的方式,两者都有优缺点。例如,如果您计划多次使用整个数据,则应选择列表,但如果您计划仅使用一次或者计划使用linq再次查询它,则应选择可枚举。

编辑: 为什么Where的返回类型是WhereListIterator而不是List的答案部分原因是Linq的工作方式。例如,如果您有另一个Where或另一个Linq语句跟随第一个语句,编译器将使用整个方法链创建单个查询,然后返回最终查询的迭代器。另一方面,如果第一个Where返回一个列表,那么链中的每个Linq方法都会在数据上单独执行。


-1

试试这个:

public List<Customer> FilterCustomersByStatus(List<Customer> source, string status)
{
    return source.Where(c => c.Status == status).ToList();
}

1
主要问题是“为什么?”你甚至不试图回答。 - svick

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